/* $Id$ */
package saccubus.worker.impl.convert;

import static org.apache.commons.io.FilenameUtils.getBaseName;
import static org.apache.commons.lang.StringUtils.*;
import static saccubus.worker.impl.convert.ConvertStatus.*;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import saccubus.conv.ConvertToVideoHook;
import saccubus.conv.CommentType;
import saccubus.util.FfmpegUtil;
import saccubus.worker.Worker;
import saccubus.worker.WorkerListener;
import saccubus.worker.profile.ConvertProfile;
import saccubus.worker.profile.ConvertProfile.HideCondition;
import saccubus.worker.profile.FfmpegProfile;
import saccubus.worker.profile.GeneralProfile;
import saccubus.worker.profile.OutputProfile;
import yukihane.inqubus.util.OutputNamePattern;
import yukihane.mediainfowrapper.Info;
import yukihane.mediainfowrapper.MediaInfo;
import yukihane.mediainfowrapper.Size;
import yukihane.swf.Cws2Fws;

/**
 * 動画を(コメント付きに)変換するワーカクラス.
 * @author yuki
 */
public class Convert extends Worker<ConvertResult, ConvertProgress> {

    private static final Logger logger = LoggerFactory.getLogger(Convert.class);
    private final ConvertProfile profile;
    private final File videoFile;
    private final File commentFile;

    public Convert(ConvertProfile profile, File video, File comment) {
        this(profile, video, comment, null);
    }

    /**
     * 変換ワーカコンストラクタ.
     * @param profile 変換用プロファイル.
     * @param video 変換元動画.
     * @param comment 変換元コメント. コメントを付与しない場合はnull.
     * @param output 変換後出力動画.
     * @throws IOException 変換失敗.
     */
    public Convert(ConvertProfile profile, File video, File comment,
            WorkerListener<ConvertResult, ConvertProgress> listener) {
        super(listener);
        this.profile = profile;
        this.videoFile = video;
        this.commentFile = comment;
        logger.info("convert video:{}, comment:{}", videoFile, commentFile);
    }

    @Override
    protected ConvertResult work() throws Exception {
        if (!profile.isConvert()) {
            return new ConvertResult(true, "");
        }

        final GeneralProfile gene = profile.getGeneralProfile();
        final OutputProfile outprof = profile.getOutputProfile();
        final OutputNamePattern outputPattern = new OutputNamePattern(outprof.getFileName());
        final String id = outprof.getVideoId();
        outputPattern.setId(isNotEmpty(id) ? id : "");
        final String title = outprof.getTitile();
        outputPattern.setTitle(isNotEmpty(title) ? title : "");
        final String fileName = getBaseName(videoFile.getPath());
        outputPattern.setFileName(fileName);
        final File outputFile = new File(outprof.getDir(),
                outputPattern.createFileName() + profile.getFfmpegOption().getExtOption());

        final Map<CommentType, File> tmpComments = new EnumMap<>(CommentType.class);
        try {

            if (profile.isCommentOverlay()) {
                for (CommentType ct : CommentType.values()) {
                    tmpComments.put(ct, File.createTempFile("vhk", ".tmp", profile.getTempDir()));
                }

                final HideCondition hide = profile.getNgSetting();

                for (CommentType ct : CommentType.values()) {
                    publish(new ConvertProgress(PROCESS, -1.0, ct.toString() + "の中間ファイルへの変換中"));
                    ConvertToVideoHook.convert(EnumSet.of(ct), commentFile, tmpComments.get(ct),
                            hide.getId(), hide.getWord());
                }
            }

            checkStop();
            publish(new ConvertProgress(PROCESS, -1.0, "動画の変換を開始"));

            final int code = convert(outputFile, new EnumMap<>(tmpComments));
            if (code != 0) {
                throw new IOException("ffmpeg実行失敗(code " + code + "): " + outputFile.getPath());
            }
            publish(new ConvertProgress(PROCESS, 100.0, "変換が正常に終了しました。"));
            return new ConvertResult(true, outputFile.getName());
        } finally {
            for(File f : tmpComments.values()) {
                if(f != null && f.exists()) {
                    f.delete();
                }
            }
        }
    }

    private int convert(File outputFile, Map<CommentType,File> tmpComments) throws InterruptedException, IOException {
        File fwsFile = null;
        try {
            final File tmpCws = File.createTempFile("cws", ".swf", profile.getTempDir());
            fwsFile = Cws2Fws.createFws(videoFile, tmpCws);
            tmpCws.delete();
            final File target = (fwsFile != null) ? fwsFile : videoFile;

            final List<String> arguments = createArguments(target, outputFile, tmpComments);
            final FfmpegUtil util = new FfmpegUtil(profile.getFfmpeg(), target);
            int duration;
            try {
                duration = util.getDuration();
            } catch (IOException ex) {
                logger.info("動画再生時間を取得できませんでした: {}", target);
                duration = Integer.MAX_VALUE;
            }
            return executeFfmpeg(arguments, duration);
        } finally {
            if (fwsFile != null && fwsFile.exists()) {
                fwsFile.delete();
            }
        }
    }

    private List<String> createArguments(final File targetVideoFile, File output, Map<CommentType,File> comments)
            throws IOException, UnsupportedEncodingException {
        final ConvertProfile prof = profile;
        final FfmpegProfile ffop = prof.getFfmpegOption();

        final List<String> cmdList = new ArrayList<>();
        cmdList.add(prof.getFfmpeg().getPath());
        cmdList.add("-y");
        final String[] mainOptions = ffop.getMainOption().split(" +");
        for (String opt : mainOptions) {
            if (isNotBlank(opt)) {
                cmdList.add(opt);
            }
        }
        final String[] inOptions = ffop.getInOption().split(" +");
        for (String opt : inOptions) {
            if (isNotBlank(opt)) {
                cmdList.add(opt);
            }
        }
        cmdList.add("-i");
        cmdList.add(targetVideoFile.getPath());
        final String[] outOptions = ffop.getOutOption().split(" +");
        for (String opt : outOptions) {
            if (isNotBlank(opt)) {
                cmdList.add(opt);
            }
        }
        final Info info = MediaInfo.getInfo(profile.getMediaInfo(), targetVideoFile);
        // 4:3 なら1.33, 16:9 なら1.76
        final boolean isHD = ((double) info.getWidth() / (double) info.getHeight() > 1.5);
        if (ffop.isResize()) {
            final Size scaled = (ffop.isAdjustRatio()) ? MediaInfo.adjustSize(info, ffop.getResizeWidth(), ffop.
                    getResizeHeight()) : new Size(info.getWidth(), info.getHeight());
            cmdList.add("-s");
            cmdList.add(scaled.getWidth() + "x" + scaled.getHeight());
        }
        final List<String> avfilterArgs = createAvfilterOptions(ffop.getAvfilterOption());
        if (!prof.isVhookDisabled()) {
            final String vhookArg = getVhookArg(prof, comments, isHD);
            if (isNotBlank(vhookArg)) {
                avfilterArgs.add(vhookArg);
            }
        }
        if (!avfilterArgs.isEmpty()) {
            cmdList.add("-vfilters");
            final String args = join(avfilterArgs, ", ");
            cmdList.add(args);
        }
        cmdList.add(output.getPath());

        logger.info("arg: {}", cmdList);
        return cmdList;
    }
    private static final Pattern PATTERN_TIME = Pattern.compile("time=(\\d+):(\\d+):(\\d+)");

    private int executeFfmpeg(final List<String> cmdList, int duration) throws InterruptedException, IOException {
        Process process = null;
        try {
            logger.info("Processing FFmpeg...");
            process = Runtime.getRuntime().exec(cmdList.toArray(new String[0]));
            BufferedReader ebr = new BufferedReader(new InputStreamReader(
                    process.getErrorStream()));
            String msg;
            while ((msg = ebr.readLine()) != null) {
                if (msg.startsWith("frame=")) {
                    final Matcher m = PATTERN_TIME.matcher(msg);
                    double per = -1.0;
                    if (m.find()) {
                        final double hour = Integer.parseInt(m.group(1));
                        final double min = Integer.parseInt(m.group(2));
                        final double sec = Integer.parseInt(m.group(3));
                        final double time = ((hour * 60) + min) * 60 + sec;
                        per = 100.0 * time / duration;
                        if (logger.isTraceEnabled()) {
                            logger.trace("time:{}, duration:{}", time, duration);
                            logger.trace(msg);
                        }
                    }
                    publish(new ConvertProgress(PROCESS, per, msg));
                } else if (!msg.endsWith("No accelerated colorspace conversion found")) {
                    logger.warn(msg);
                } else {
                    logger.info(msg);
                }

                checkStop();
            }

            process.waitFor();
            return process.exitValue();
        } finally {
            if (process != null) {
                process.destroy();
            }
        }
    }

    private static List<String> createAvfilterOptions(String avfilterOption) {
        final List<String> avfilterArgs = new ArrayList<>();
        if (isNotBlank(avfilterOption)) {
            avfilterArgs.add(avfilterOption);
        }
        return avfilterArgs;
    }

    private static String getVhookArg(ConvertProfile prof, Map<CommentType, File> comments, boolean isHD) throws
            UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        sb.append("vhext=");
        sb.append(prof.getVhook().getPath().replace("\\", "/"));
        if (prof.isCommentOverlay()) {
            for(Entry<CommentType, File> e : comments.entrySet()) {
                sb.append("|");
                sb.append(e.getKey().getVhookOptionPrefix());
                sb.append(URLEncoder.encode(e.getValue().getPath().replace("\\", "/"), "Shift_JIS"));
            }
        }
        sb.append("|");
        sb.append("--font:");
        sb.append(URLEncoder.encode(
                prof.getFont().getPath().replace("\\", "/"), "Shift_JIS"));
        sb.append("|");
        sb.append("--font-index:");
        sb.append(prof.getFontIndex());
        sb.append("|");
        sb.append("--show-user:");
        final int dispNum = (prof.getMaxNumOfComment() < 0) ? 30 : prof.getMaxNumOfComment();
        sb.append(dispNum);
        sb.append("|");
        sb.append("--shadow:");
        sb.append(prof.getShadowIndex());
        sb.append("|");
        if (prof.isShowConverting()) {
            sb.append("--enable-show-video");
            sb.append("|");
        }
        if (!prof.isDisableFontSizeArrange()) {
            sb.append("--enable-fix-font-size");
            sb.append("|");
        }
        if (prof.isCommentOpaque()) {
            sb.append("--enable-opaque-comment");
            sb.append("|");
        }
        if (isHD) {
            sb.append("--aspect-mode:1");
            sb.append("|");
        }
        return sb.toString();
    }

    protected void checkStop() throws InterruptedException {
        if (Thread.interrupted()) {
            throw new InterruptedException("中止要求を受け付けました");
        }
    }
}
