package dareka.processor.impl;

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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventListener;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import dareka.NLConfig;
import dareka.common.CloseUtil;
import dareka.common.Config;
import dareka.common.ConfigObserver;
import dareka.common.Logger;
import dareka.common.TextUtil;
import dareka.extensions.CompleteCache;
import dareka.extensions.NLFilterListener;
import dareka.extensions.SystemEventListener;
import dareka.processor.util.GetThumbInfoUtil;

/**
 * [nl] パッケージスコープで共有情報を保持するクラス<br>
 * パッケージスコープ外からは許可したクラスのみインスタンスを取得できる
 * @since NicoCache_nl+110424mod
 */
public class NLShared implements ConfigObserver {
    
    // パッケージスコープ外でインスタンス化を許容するクラス名のパターン
    private static final Pattern FRIENDS_CLASS_PATTERN = Pattern.compile(
            "dareka\\.(NLMain|processor.+)");
    
    private NLShared() {
        NLConfig.addObserver(this);
    }
    
    public void update(Config config) {
        vid2cidFile = NLConfig.getFile("vid2cidFile");
        vid2cidLastModified = 0L; // 強制読み直し
    }
    
    private static final boolean DEBUG = Boolean.getBoolean("dareka.debug");
    
    /** デバッグモードの時は通常ログとしても出力する */
    public void debugWithThread2(String message) {
        if (DEBUG) {
            Logger.info(message);
        }
        Logger.debugWithThread(message);
    }
    
    /** パッケージスコープ内でインスタンスを利用する場合はこちらを使う */
    static final NLShared INSTANCE = new NLShared();
    
    /**
     * {@link #FRIENDS_CLASS_PATTERN} で許可されたクラスにのみインスタンスを返す。
     * 呼び出し元チェックにコストがかかるので、複数必要な場合は保持して使い回すこと。
     * 
     * @return NLSharedのインスタンス
     * @throws RuntimeException 許可されていないクラスからの呼び出し
     */
    public static NLShared getInstance() {
        String callerName = "(no name)";
        StackTraceElement[] ste = new Throwable().getStackTrace();
        if (ste.length > 1) {
            callerName = ste[1].getClassName();
        }
        if (!FRIENDS_CLASS_PATTERN.matcher(callerName).matches()) {
            throw new RuntimeException("you are not friends: " + callerName);
        }
        return INSTANCE;
    }
    
    
    /**
     * ニコ生からのsmidとリクエスト時間を保持
     * @since NicoCache_nl (9).03
     */
    private static ConcurrentHashMap<String, Long> liveIdList =
        new ConcurrentHashMap<String, Long>();
    
    public void setLiveMovie(String smid) {
        liveIdList.put(smid, new Long(System.currentTimeMillis()));
    }
    
    public boolean isLiveMovie(String smid) {
        // 古いのは削除
        long now = System.currentTimeMillis();
        for (String oldId : liveIdList.keySet()) {
            Long setTime = liveIdList.get(oldId);
            // とりあえず30秒無効にしてみる
            if (setTime != null && now > setTime.longValue() + 30000L) {
                liveIdList.remove(oldId);
            }
        }
        return liveIdList.containsKey(smid);
    }
    
    
    /**
     * キャッシュ途中で削除する為のリスト
     * @since NicoCache_nl (9).10
     */
    private static Set<String> deleteSet =
        Collections.synchronizedSet(new HashSet<String>());
    
    public boolean isInDeleteSet(String id) {
        return deleteSet.contains(id);
    }
    
    public void addDeleteSet(String id) {
        deleteSet.add(id);
    }
    
    public void removeDeleteSet(String id) {
        deleteSet.remove(id);
    }
    
    
    /**
     * キャッシュ完了時に呼び出されるExtension →CacheManagerあたりが適切？
     * @since NicoCache_nl (9).10
     */
    private static List<CompleteCachePair> completeCaches =
        Collections.synchronizedList(new ArrayList<CompleteCachePair>());
    
    class CompleteCachePair {
        public int priority;
        public CompleteCache completeCache;
        CompleteCachePair(int p, CompleteCache c) {
            this.priority = p;
            this.completeCache = c;
        }
    }
    
    public void addCompleteCache(CompleteCache c) {
        completeCaches.add(new CompleteCachePair(0, c));
    }
    
    public ArrayList<CompleteCache> getCompleteEntries() {
        // リストを返す前にupdateを呼び出してソートする
        for (CompleteCachePair p : completeCaches) {
            p.completeCache.update();
            p.priority = p.completeCache.getPriority();
            if (p.priority > 0) p.priority = 0; // 仕様上0が最高値なので
        }
        
        // コピーしたのをソートしてるから、マルチスレッド対策は特に要らないはず
        Object[] objArray = completeCaches.toArray();
        Arrays.sort(objArray, new Comparator<Object>() {
            public int compare(Object o1, Object o2) {
                return ((CompleteCachePair) o1).priority - ((CompleteCachePair) o2).priority;
            }
        });
        
        ArrayList<CompleteCache> list = new ArrayList<CompleteCache>();
        for (Object o : objArray) {
            list.add(((CompleteCachePair) o).completeCache);
        }
        return list;
    }
    
    
    /**
     *  スレッドIDからsmidへの対応を保持
     *  @since NicoCache_nl+110424mod
     */
    private static Map<String, String> thread2smidCache =
//      Collections.synchronizedMap(new LRUMap<String, String>());
            new java.util.concurrent.ConcurrentHashMap<String, String>();
    
    /** スレッドに関連付けられたsmidを取得、関連が無ければnullを返す */
    public String thread2smid(String thread) {
        if (!TextUtil.isThreadId(thread)) {
            return null;
        }
        if (!thread2smidCache.containsKey(thread)) {
            // キャッシュに無ければthumbinfoを取得してみる
            try {
                GetThumbInfoUtil.get(thread);
            } catch (IOException e) {
                Logger.debugWithThread(e);
            }
        }
        return thread2smidCache.get(thread);
    }
    
    /** スレッドとsmidを関連付ける */
    public void putThread2Smid(String thread, String smid) {
        if (TextUtil.isThreadId(thread) && TextUtil.isVideoId(smid)) {
            thread2smidCache.put(thread, smid);
            Logger.debugWithThread("thread2smid: " + thread + " => " + smid +
                    ", size=" + thread2smidCache.size());
        }
    }
    
    
    /**
     * 動画鯖のID(数値=vid)から実際にキャッシュするID(数値=cid)への対応を保持
     * @since NicoCache_nl+110424mod
     */
    private static Map<String, String> vid2cid = newTreeMap();
    private static File vid2cidFile;
    private static long vid2cidLastModified;
    
    private static final Pattern VID2CID_LINE_PATTERN = Pattern.compile(
            "\\s*\"(\\d+)\"\\s*:\\s*\"(\\d+)\",?");
    
    private static TreeMap<String, String> newTreeMap() {
        return new TreeMap<String, String>(new Comparator<String>() {
            public int compare(String s1, String s2) {
                // Integerの範囲に収まらない(10桁以上の)入力があるので
                return (int)(Long.parseLong(s1) - Long.parseLong(s2));
            }
        });
    }
    
    /**
     * vid に関連付けられた cid を取得、無ければ vid をそのまま返す
     * @param vid 動画鯖のID(数値)
     * @return 実際にキャッシュするID(数値)
     */
    public String vid2cid(String vid) {
        loadJson();
        String cid = vid2cid.get(vid);
        if (cid != null) {
            Logger.debugWithThread(
                    "vid2cid(get): " + vid + " => " + cid);
            return cid;
        }
        return vid;
    }
    
    /**
     * vid と cid を関連付ける(結果は vid2cidFile に保存する)
     */
    public void put_vid2cid(String vid, String cid) {
        if (!validateVID(vid)) {
            Logger.warning("invalid vid: " + vid);
            return;
        }
        debugWithThread2("vid2cid(put): " + vid + " => " + cid);
        
        synchronized (vid2cid) {
            String prevId = vid2cid.put(vid, cid);
            if (prevId == null || !prevId.equals(cid)) {
                saveJson();
            }
        }
    }
    
    /** videoIdとして有効な文字列か？ */
    public boolean validateVID(String vid) {
        try {
            // 動画IDは9桁まで有効
            if (Integer.parseInt(vid) < 1000000000) {
                return true;
            }
        } catch (NumberFormatException e) {
            Logger.debugWithThread(e);
        }
        return false;
    }
    
    // TODO ファイルを与えてマップを返すように
    private void loadJson() {
        if (vid2cidFile == null || !vid2cidFile.isFile() ||
                vid2cidFile.lastModified() <= vid2cidLastModified) {
            return;
        }
        debugWithThread2("loading " + vid2cidFile.getPath());
        BufferedReader br = null;
        try {
            TreeMap<String, String> map = newTreeMap();
            br = new BufferedReader(new FileReader(vid2cidFile));
            String line; Matcher m;
            while ((line = br.readLine()) != null) {
                if ((m = VID2CID_LINE_PATTERN.matcher(line)).lookingAt() &&
                        !NLConfig.matches("deletedVideoId", m.group(1))) {
                    map.put(m.group(1), m.group(2));
                }
            }
            vid2cidLastModified = vid2cidFile.lastModified();
            vid2cid = map;
        } catch (IOException e) {
            Logger.error(e);
        } finally {
            CloseUtil.close(br);
        }
    }
    
    // TODO ファイルとマップを与えるように
    private void saveJson() {
        if (vid2cidFile == null) {
            return;
        }
        debugWithThread2("saving " + vid2cidFile.getPath());
        if (vid2cidFile.exists()) {
            File bakFile = new File(vid2cidFile.getPath() + ".bak");
            bakFile.delete();
            vid2cidFile.renameTo(bakFile);
        }
        BufferedWriter bw = null;
        boolean written = false;
        try {
            bw = new BufferedWriter(new FileWriter(vid2cidFile));
            for (Map.Entry<String, String> e : vid2cid.entrySet()) {
                if (written) {
                    bw.append(",\r\n");
                } else {
                    bw.append("{\r\n");
                    written = true;
                }
                bw.write("\"");
                bw.write(e.getKey());
                bw.write("\": \"");
                bw.write(e.getValue());
                bw.write("\"");
            }
            bw.write("\r\n}");
        } catch (IOException e) {
            Logger.error(e);
        } finally {
            CloseUtil.close(bw);
            if (written) {
                vid2cidLastModified = vid2cidFile.lastModified();
            }
        }
    }
    
    
    /**
     * イベント通知インターフェース
     * @since NicoCache_nl+111130mod
     */
    private static List<SystemEventListener> systemEventListeners =
        Collections.synchronizedList(new ArrayList<SystemEventListener>());
    
    public void addEventListener(EventListener listener) {
        boolean added = false;
        if (listener instanceof SystemEventListener) {
            Logger.debug("adding SystemEventListener: " + listener);
            added |= systemEventListeners.add((SystemEventListener) listener);
        }
        if (listener instanceof NLFilterListener) {
            Logger.debug("adding NLFilterListener: " + listener);
            added |= EasyRewriter.addListener((NLFilterListener) listener);
        }
        if (!added) {
            Logger.debug("unknown EventListener: " + listener);
        }
    }
    
    public int countSystemEventListeners() {
        return systemEventListeners.size();
    }
    
    /**
     * システムイベントを全体に通知する
     * @param id イベントID
     * @param source イベント発生元オブジェクト
     * @param checkResultOK 戻り値をチェックして途中で処理を中断するか？
     * @return イベント処理結果
     */
    public int notifySystemEvent(int id, NLEventSource source, boolean checkResultOK) {
        for (SystemEventListener listener : systemEventListeners) {
            try {
                int result = listener.onSystemEvent(id, source);
                if (checkResultOK && result != SystemEventListener.RESULT_OK) {
                    return result;
                }
            } catch (Throwable e) {
                Logger.error(e);
            }
        }
        return SystemEventListener.RESULT_OK;
    }
    
    /**
     *  ht2認証情報に付随する動画情報を保持
     *  @since NicoCache_nl+150304mod+170104mod
     */
    private Ht2Manager ht2manager = new Ht2Manager();
    public Ht2Manager getHt2Manager() {
        return ht2manager;
    }
}
