package dareka.processor.impl;

import dareka.common.Logger;
import dareka.processor.util.Mp4Parser;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ReEncodingInfo {
    
    // 再エンコードされた動画の特徴が変化して正しい判定結果が
    // 得られなくなった場合 ENGINE_VERSION を変更して，
    // 動画IDとバージョンから信用できない判定結果を削除する
    public final static int ENGINE_VERSION = 1;
    
    public final static int REQUIRED_HEADER_LENGTH = 0x200;
    
    private static boolean loaded = false;
    private final static ConcurrentHashMap<String, Boolean> cache =
            new ConcurrentHashMap<String, Boolean>();
    private final static File dataFile = new File("data/reencoded.csv");
    private final static Pattern VERSION_LINE_PATTERN = Pattern.compile("version,(\\d+)");
    private final static Pattern DATA_LINE_PATTERN = Pattern.compile("(\\w+),(true|false)");
    
    /**
     * 再エンコードされているか，既に確認済みの結果を得る．
     * @param videoId
     * @return 再エンコードされているか．不明時はnullを返す．
     */
    public static Boolean get(String videoId) {
        load();
        return cache.get(videoId);
    }
    
    protected static void put(String videoId, boolean reencoded) {
        load();
        Boolean prev = cache.putIfAbsent(videoId, reencoded);
        if (prev == null) {
            append(videoId, reencoded);
        }
    }
    
    /**
     * 再エンコードされているかを確認する．結果はキャッシュされる．
     * キャッシュ済みの時はキャッシュから結果を返す．
     * 判定に失敗した場合はnullを返す．
     * @param videoId
     * @param header データの先頭から．最低0x200バイト必要．
     *                データの長さをlimitに設定して呼び出すこと．
     * @return 再エンコードされているか．不明時はnullを返す．
     */
    public static Boolean check(String videoId, ByteBuffer header) {
        Boolean c = get(videoId);
        if (c != null) {
            return c;
        }
        Boolean result = check(header);
        if (result == null) {
            return null;
        }
        put(videoId, result);
        return result;
    }
    
    /**
     * 再エンコードされているかを確認する．結果はキャッシュされる．
     * キャッシュ済みの時はキャッシュから結果を返す．
     * 判定に失敗した場合はnullを返す．
     * @param videoId
     * @param file 動画ファイル
     * @return 再エンコードされているか．不明時はnullを返す．
     */
    public static Boolean check(String videoId, File file) {
        Boolean c = get(videoId);
        if (c != null) {
            return c;
        }
        Boolean result = check(file);
        if (result == null) {
            return null;
        }
        put(videoId, result);
        return result;
    }
    
    /**
     * 再エンコードされているかを確認する．
     * 判定に失敗した場合はnullを返す．
     * @param header データの先頭から．最低0x200バイト必要．
     *                データの長さをlimitに設定して呼び出すこと．
     * @return 再エンコードされているか．不明時はnullを返す．
     */
    public static Boolean check(ByteBuffer header) {
        if (header == null || header.limit() < REQUIRED_HEADER_LENGTH) {
            return null;
        }
        Mp4Parser mp4 = new Mp4Parser(header);
        return checkCore(mp4);
    }
    
    /**
     * 再エンコードされているかを確認する．
     * 判定に失敗した場合はnullを返す．
     * @param file 動画ファイル
     * @return 再エンコードされているか．不明時はnullを返す．
     */
    public static Boolean check(File file) {
        if (file == null || !file.exists()) {
            return null;
        }
        Mp4Parser mp4 = new Mp4Parser(file, REQUIRED_HEADER_LENGTH);
        return checkCore(mp4);
    }
    
    private static Boolean checkCore(Mp4Parser mp4) {
        if (mp4.isInvalid()) {
            return null;
        }
        
        String videoHandler = mp4.getVideoHandler();
        Date videoCreationTime = mp4.getVideoCreationTime();
        
        if (videoHandler == null || videoCreationTime == null) {
            return false;
        }
        if (videoHandler.equals("VideoHandler")
                && videoCreationTime.equals(new Date(0))) {
            // Re-encoded
            return true;
        }
        return false;
    }
    
    private static void load() {
        if (!loaded) {
            loadCore();
        }
    }
    private static synchronized void loadCore() {
        if (loaded) return;
        cache.clear();
        if (!dataFile.exists()) {
            try {
                File parent = dataFile.toPath().getParent().toFile();
                if (!parent.exists()) {
                    parent.mkdir();
                }
                dataFile.createNewFile();
            } catch (IOException e) {
                Logger.error(e);
                return;
            }
        }
        int version = 0;
        try (BufferedReader br = new BufferedReader(new FileReader(dataFile))) {
            String line;
            if ((line = br.readLine()) != null) {
                Matcher m;
                if ((m = VERSION_LINE_PATTERN.matcher(line)).matches()) {
                    version = Integer.parseInt(m.group(1));
                } else if ((m = DATA_LINE_PATTERN.matcher(line)).matches()) {
                    // version 0 対応はそのうち消す
                    cache.put(m.group(1), Boolean.parseBoolean(m.group(2)));
                }
            }
            while ((line = br.readLine()) != null) {
                Matcher m = DATA_LINE_PATTERN.matcher(line);
                if (m.matches()) {
                    cache.put(m.group(1), Boolean.parseBoolean(m.group(2)));
                }
            }
        } catch (IOException e) {
        }
        
        if (version != ENGINE_VERSION) {
            save();
        }
        loaded = true;
    }
    
    private static synchronized void save() {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(dataFile, false))) {
            bw.write("version," + ENGINE_VERSION);
            bw.newLine();
            
            for (ConcurrentHashMap.Entry<String, Boolean> e : cache.entrySet()) {
                String videoId = e.getKey();
                boolean reencoded = e.getValue();
                bw.write(videoId + "," + reencoded);
                bw.newLine();
            }
        } catch (IOException e) {
        }
    }
    
    private static synchronized void append(String videoId, boolean reencoded) {
        try (BufferedWriter bw = new BufferedWriter(new FileWriter(dataFile, true))) {
            bw.write(videoId + "," + reencoded);
            bw.newLine();
        } catch (IOException e) {
        }
    }
}
