package yukihane.inqubus.manager;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.commons.lang.StringUtils;
import saccubus.worker.WorkerListener;
import saccubus.worker.impl.MessageReportable;
import saccubus.worker.impl.PercentageReportable;
import saccubus.worker.impl.convert.Convert;
import saccubus.worker.impl.convert.ConvertProgress;
import saccubus.worker.impl.convert.ConvertResult;
import saccubus.worker.impl.download.Download;
import saccubus.worker.impl.download.DownloadProgress;
import saccubus.worker.impl.download.DownloadResult;
import saccubus.worker.profile.ConvertProfile;
import saccubus.worker.profile.DownloadProfile;

/**
 * GUIからのタスク(ダウンロード/変換)処理要求を管理するクラスです.
 * タスク用Executor Serviceへの投入や、キャンセル要求の処理を行います.
 * @author yuki
 */
public class TaskManage {

    private static final Logger logger = LoggerFactory.getLogger(TaskManage.class);
    private final ExecutorService downloadExecutorService;
    private final ExecutorService convertExecutorService;
    private final Map<Integer, ManageTarget<DownloadResult>> downloadTargets = new HashMap<>();
    private final Map<Integer, ManageTarget<ConvertResult>> convertTargets = new HashMap<>();
    private final TaskManageListener clientListener;
    private final int waitDownload;

    public TaskManage() {
        this(1, 30, 1, null);
    }

    public TaskManage(int maxDownload, int maxConvert) {
        this(maxDownload, 30, maxConvert, null);
    }

    /**
     * タスク管理オブジェクトを構築します.
     * @param maxDownload 最大同時ダウンロード数.
     * @param waitDownload 前回ダウンロードからの最小待ち時間(秒).
     * @param maxConvert 最大同時変換数.
     * @param listener イベントリスナ. 通知不要の場合はnull.
     */
    public TaskManage(int maxDownload, int waitDownload, int maxConvert, TaskManageListener listener) {
        downloadExecutorService = Executors.newFixedThreadPool(maxDownload);
        convertExecutorService = Executors.newFixedThreadPool(maxConvert);
        this.waitDownload = waitDownload;
        this.clientListener = listener;
    }

    public synchronized boolean add(RequestProcess request) {
        final DownloadProfile dp = request.getDownloadProfile();
        final ConvertProfile cp = request.getConvertProfile();
        if (dp != null && (dp.getVideoProfile().isDownload() || dp.getCommentProfile().isDownload())) {
            // ダウンロードするものがあればまずダウンロード処理
            final Download task = new Download(dp, request.getVideoId(),
                    new DownloadListener(request.getRowId()), waitDownload);
            final Future<DownloadResult> future = downloadExecutorService.submit(task);
            downloadTargets.put(request.getRowId(), new ManageTarget<>(request, future));
            return true;

        } else if (cp != null && cp.isConvert()) {
            final Convert task = new Convert(cp, dp.getVideoProfile().getLocalFile(), dp.getCommentProfile().
                    getLocalFile(), new ConvertListener(request.getRowId()));
            final Future<ConvertResult> future = convertExecutorService.submit(task);
            convertTargets.put(request.getRowId(), new ManageTarget<>(request, future));
            return true;
        }
        return false;
    }

    public synchronized boolean cancel(int rowId) {
        // FIXME 実行前にキャンセルした場合にはcancelledイベントが飛ばないのでMapからリクエストを削除できない
        final ManageTarget<DownloadResult> down = downloadTargets.get(rowId);
        if (down != null) {
            return down.getFuture().cancel(true);
        }
        final ManageTarget<ConvertResult> conv = convertTargets.get(rowId);
        if (conv != null) {
            return conv.getFuture().cancel(true);
        }

        return false;
    }

    private class DownloadListener extends TaskManageInnerListener<DownloadResult, DownloadProgress> {

        private DownloadListener(int rowId) {
            super(rowId);
        }

        @Override
        public void done(DownloadResult result) {
            super.done(result);
            synchronized (TaskManage.this) {
                final ManageTarget<DownloadResult> mt = removeRequest(getRowId());
                final RequestProcess request = mt.getRequest();
                if (request.getConvertProfile().isConvert()) {
                    final DownloadProfile dp = request.getDownloadProfile();
                    final File video = (dp.getVideoProfile().isDownload()) ? result.getDownloadVideo() : dp.
                            getVideoProfile().getLocalFile();
                    final File comment = (dp.getCommentProfile().isDownload()) ? result.getDownloadComment() : dp.
                            getCommentProfile().getLocalFile();
                    final ConvertProfile cp = request.getConvertProfile();
                    final Convert task = new Convert(cp, video, comment, new ConvertListener(getRowId()));
                    final Future<ConvertResult> future = convertExecutorService.submit(task);
                    convertTargets.put(request.getRowId(), new ManageTarget<>(request, future));
                }
            }
        }

        @Override
        protected TaskKind getKind() {
            return TaskKind.DOWNLOAD;
        }

        @Override
        protected ManageTarget<DownloadResult> removeRequest(int rowId) {
            return downloadTargets.remove(rowId);
        }
    }

    private class ConvertListener extends TaskManageInnerListener<ConvertResult, ConvertProgress> {

        private ConvertListener(int rowId) {
            super(rowId);
        }

        @Override
        protected TaskKind getKind() {
            return TaskKind.CONVERT;
        }

        @Override
        public void done(ConvertResult result) {
            super.done(result);
            synchronized (TaskManage.this) {
                removeRequest(getRowId());
            }
        }

        @Override
        protected ManageTarget<ConvertResult> removeRequest(int rowId) {
            return convertTargets.remove(rowId);
        }
    }

    abstract class TaskManageInnerListener<T, V extends PercentageReportable & MessageReportable> implements WorkerListener<T, V> {

        private final int rowId;

        protected TaskManageInnerListener(int rowId) {
            this.rowId = rowId;
        }

        protected int getRowId() {
            return rowId;
        }

        private void notify(TaskStatus status) {
            notify(status, -1.0, "");
        }

        private void notify(TaskStatus status, double percentage, String message) {
            if (getListener() == null) {
                return;
            }
            getListener().process(rowId, getKind(), status, percentage, message);
        }

        private TaskManageListener getListener() {
            return clientListener;
        }

        protected abstract TaskKind getKind();

        @Override
        public void process(V progress) {
            logger.trace("process: {}", progress);
            notify(TaskStatus.DOING, progress.getPercentage(), progress.getMessage());
        }

        @Override
        public final void cancelled() {
            logger.debug("cancelled: {}", toString());
            synchronized (TaskManage.this) {
                removeRequest(rowId);
            }
            notify(TaskStatus.CANCELLED);
        }

        /**
         * この処理をオーバライドしてキューからリクエストを削除する必要があります.
         * @param result 処理結果.
         */
        @Override
        public void done(T result) {
            logger.debug("done: {}", result);
            notify(TaskStatus.DONE);
        }

        @Override
        public final void error(Throwable th) {
            logger.debug("error", th);
            synchronized (TaskManage.this) {
                removeRequest(rowId);
            }

            String message = th.getMessage();
            if(StringUtils.isBlank(message)) {
                message = "予期しないエラーが発生しました";
            }
            notify(TaskStatus.ERROR, 0.0, message);
        }

        protected abstract ManageTarget<T> removeRequest(int rowId);
    }

    private static class ManageTarget<T> {

        private final RequestProcess request;
        private final Future<T> future;

        ManageTarget(RequestProcess request, Future<T> future) {
            this.request = request;
            this.future = future;
        }

        Future<T> getFuture() {
            return future;
        }

        RequestProcess getRequest() {
            return request;
        }
    }
}
