package jp.sourceforge.nicoro;

import static jp.sourceforge.nicoro.Log.LOG_TAG;
import static jp.sourceforge.nicoro.PlayerConstants.REAL_PLAYER_WIDTH_PX_4_3;
import static jp.sourceforge.nicoro.PlayerConstants.REAL_PLAYER_HEIGHT_PX_4_3;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import jp.sourceforge.nicoro.nicoscript.*;

import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;

public class MessageChatController {
    private static final boolean DEBUG_LOGV = Release.IS_DEBUG & false;
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG & false;

    public static final int JUMP_MESSAGE_TIME_MS = 3000;

    public static final int COMMENT_TYPE_NORMAL = 0;
    public static final int COMMENT_TYPE_NICOS = 1;
    public static final int COMMENT_TYPE_LOCAL = 2;

    // NG共有レベル
    public static final int NG_SHARE_LEVEL_NONE = 0;
    public static final int NG_SHARE_LEVEL_LIGHT = 1;
    public static final int NG_SHARE_LEVEL_MIDDLE = 2;
    public static final int NG_SHARE_LEVEL_HIGH = 3;

    private Paint mPaintText = new Paint();
    private Random mRandom = new Random();
    private Matrix mMatrixMessage = new Matrix();

    private NicoScriptView mNicoScriptView;

    // 動作中のニコスクリプト・ニコスクリプトによって変えられる値
    private NicoScriptDefault mCommandDefault;
    private NicoScriptGyaku mCommandGyaku;
    private final ArrayList<NicoScriptReplace> mCommandReplace =
        new ArrayList<NicoScriptReplace>();
    private NicoScriptJump mCommandJump;
    private final ArrayList<NicoScriptKeywordJump> mCommandKeywordJump =
        new ArrayList<NicoScriptKeywordJump>();
    private final ArrayList<NicoScriptVote> mCommandVote =
        new ArrayList<NicoScriptVote>();
    private final ArrayList<NicoScriptScore> mCommandScore =
        new ArrayList<NicoScriptScore>();
    private final ArrayList<NicoScriptBall> mCommandBall =
        new ArrayList<NicoScriptBall>();
    private final ArrayList<NicoScriptWindow> mCommandWindow =
        new ArrayList<NicoScriptWindow>();
    private NicoScriptDoor mCommandDoor;
    private boolean mForbidCommentNicoScript;
    /**
     * 注意：Valueはvposになる
     */
    private final HashMap<String, Integer> mJumpMarker =
        new HashMap<String, Integer>();

    private int mDuration = -1;

    private WeakReference<PlayerActivity> mPlayerActivity =
        new WeakReference<PlayerActivity>(null);
    private WeakReference<AbstractPlayerFragment> mPlayerFragment =
        new WeakReference<AbstractPlayerFragment>(null);

    private HashMap<String, String> mNgUp;
    private Set<String> mNgUpKey;

    private String mReturnVideoNumber;
    private int mReturnTimeSecond = -1;
    private String mReturnMessage;
    private int mReturnStartTimeSecond;

    private MessageData mMessageData = new MessageData();
    private MessageData mMessageDataFork = new MessageData();
    private final Object mSyncMessageData = new Object();

    private ArrayList<ConfigureNgClient.NgClient> mConfigureNgClientsId;
    private ArrayList<ConfigureNgClient.NgClient> mConfigureNgClientsWord;
    private ArrayList<ConfigureNgClient.NgClient> mConfigureNgClientsCommand;

    private int mNgShareLevel = NG_SHARE_LEVEL_NONE;
    private int mNgShareLevelThreshold = Integer.MIN_VALUE;

    public MessageChatController() {
    }

    public void setAntiAlias(boolean messageAntialias) {
        mPaintText.setAntiAlias(messageAntialias);
    }

    /**
     *
     * @param duration 動画の長さ（10ms単位）
     */
    public void setDuration(int duration) {
        mDuration = duration;
    }

    public void setPlayerActivity(PlayerActivity playerActivity) {
        mPlayerActivity = new WeakReference<PlayerActivity>(playerActivity);
    }
    public void setPlayerFragment(AbstractPlayerFragment playerFragment) {
        mPlayerFragment = new WeakReference<AbstractPlayerFragment>(playerFragment);
    }

    /**
     * コメント描画<br>
     * @param canvas
     * @param vpos 時間（10ms単位）
     * @param videoWidth
     * @param videoHeight
     * @param messageDisable コメント表示／非表示
     */
    public void drawMessage(Canvas canvas,
            int vpos, int videoWidth, int videoHeight,
            boolean messageDisable) {
        // TODO MessageData引数指定なpublicメソッド削除したらもう少し細かいブロックでsynchronized
        synchronized (mSyncMessageData) {
            drawMessage(canvas, mMessageData, mMessageDataFork,
                    vpos, videoWidth, videoHeight, messageDisable);
        }
    }

    /**
     * コメント描画<br>
     * @param canvas
     * @param messageData コメント
     * @param messageDataFork 投稿者コメント
     * @param vpos 時間（10ms単位）
     * @param videoWidth
     * @param videoHeight
     * @param messageDisable コメント表示／非表示
     */
    public void drawMessage(Canvas canvas,
            MessageData messageData,
            MessageData messageDataFork,
            int vpos, int videoWidth, int videoHeight,
            boolean messageDisable) {
        if (canvas != null) {
            canvas.save(Canvas.MATRIX_SAVE_FLAG);
            setMatrixToCanvas(canvas, videoWidth, videoHeight);
        }

//    	if (DEBUG_LOGV) {
//		Log.v(LOG_TAG, Log.buf().append("drawMessage: vpos=")
//				.append(vpos).toString());
//	}

        drawMessageCommon(canvas, messageDataFork, vpos, messageDisable, true);

        if (mDuration > 0 && vpos + 500 > mDuration) {
            // 動画終了間際では残り全てのコメント表示開始
            for (Iterator<MessageChat> it = messageData.getChatsWait().iterator(); it.hasNext(); ) {
                MessageChat chat = it.next();
                // 現在時刻で上書き
                chat.setVpos(vpos + 200);
            }
        }
        drawMessageCommon(canvas, messageData, vpos, messageDisable, false);

        onUpdateVideoPosition(vpos / 100);

        if (canvas != null) {
            canvas.restore();
        }
    }

    /**
     * 生放送／実況向けコメント描画<br>
     * @param canvas
     * @param vpos
     * @param videoWidth
     * @param videoHeight
     * @param messageDisable
     * @param liveMessageLoader
     */
    public void drawMessageForLive(Canvas canvas,
            int vpos, int videoWidth, int videoHeight,
            boolean messageDisable, LiveMessageLoaderInterface liveMessageLoader) {
        synchronized (mSyncMessageData) {
            drawMessageForLive(canvas, mMessageData, null,
                    vpos, videoWidth, videoHeight, messageDisable, liveMessageLoader);
        }
    }

    /**
     * 生放送／実況向けコメント描画<br>
     * TODO 投稿者コメント指定はいらないのかも
     * @param canvas
     * @param messageData
     * @param messageDataFork
     * @param vpos
     * @param videoWidth
     * @param videoHeight
     * @param messageDisable
     * @param liveMessageLoader
     */
    public void drawMessageForLive(Canvas canvas,
            MessageData messageData,
            MessageData messageDataFork,
            int vpos, int videoWidth, int videoHeight,
            boolean messageDisable, LiveMessageLoaderInterface liveMessageLoader) {
        if (liveMessageLoader == null || liveMessageLoader.isNull()) {
            return;
        }

        // TODO 投稿者コメント

        canvas.save(Canvas.MATRIX_SAVE_FLAG);
        setMatrixToCanvas(canvas, videoWidth, videoHeight);

        final List<MessageChat> chatsWait = messageData.getChatsWait();
        assert chatsWait != null;

        while (true) {
            MessageChat chat = liveMessageLoader.poll();
            if (chat == null) {
                break;
            }

            // TODO 再生側で取り出した時刻で上書き
//    		chat.setVpos(vpos + 100);
            chat.setVpos(vpos + 200);
            chatsWait.add(chat);
        }

        drawMessageCommon(canvas, messageData, vpos, messageDisable, false);
        canvas.restore();
    }

    private void drawMessageCommon(Canvas canvas, MessageData messageData,
            int vpos, boolean messageDisable, boolean isFork) {
        final List<MessageChat> chatsWait = messageData.getChatsWait();

        assert chatsWait != null;
        assert messageData.getChatsRunningNaka() != null;
        assert messageData.getChatsRunningShita() != null;
        assert messageData.getChatsRunningUe() != null;

        // 表示が終わったコメントを非表示に
        removeMessageChatByPassage(messageData, vpos);

        // 方向設定更新
        for (MessageChat chat : messageData.getChatsRunningNaka()) {
            chat.setDir(this);
        }

        final Paint paintText = mPaintText;
        // 時間が来たコメントの表示開始
        for (Iterator<MessageChat> it = chatsWait.iterator(); it.hasNext(); ) {
            MessageChat chat = it.next();
            if (chat.isDisplayedTiming(vpos)) {
                it.remove();
                if (chat.isRemovedTiming(vpos)) {
                    // シークで既に時間外の場合は副作用があるprepareのみ行って外す
                    chat.prepareAdd(this, paintText, vpos);
                    chat.prepareRemove(this);
                } else if (!isFork && isNg(chat)) {
                    // TODO NGの場合は副作用があるprepareのみ行って外す（これでいいのか？）
                    chat.prepareAdd(this, paintText, vpos);
                    chat.prepareRemove(this);
                } else {
                    if (!isFork) {
                        chat.applyNgCommand(mConfigureNgClientsCommand);
                    }
                    addMessageChatToData(chat, messageData, vpos);
                }
            } else {
                // ソート済みのためタイミング外のものが来た時点でループ抜ける
                break;
            }
        }

        // コメントの表示
        if (messageDisable || mForbidCommentNicoScript) {
            canvas = null;
        }
        drawMessageChatMain(messageData, vpos, canvas);
    }

    private void removeMessageChatByPassage(
            MessageData messageData, int vpos) {
        final List<MessageChat> chatsRunningNaka = messageData.getChatsRunningNaka();
        final List<MessageChat> chatsRunningShita = messageData.getChatsRunningShita();
        final List<MessageChat> chatsRunningUe = messageData.getChatsRunningUe();

        for (Iterator<MessageChat> it = chatsRunningNaka.iterator(); it.hasNext(); ) {
            MessageChat chat = it.next();
            if (chat.isRemovedTiming(vpos)) {
                chat.prepareRemove(this);
                it.remove();
                if (DEBUG_LOGV) {
                    Log.v(LOG_TAG, Log.buf().append("remove: ")
                            .append(chat.toString()).toString());
                }
                continue;
            }
        }
        for (Iterator<MessageChat> it = chatsRunningShita.iterator(); it.hasNext(); ) {
            MessageChat chat = it.next();
            if (chat.isRemovedTiming(vpos)) {
                chat.prepareRemove(this);
                it.remove();
                if (DEBUG_LOGV) {
                    Log.v(LOG_TAG, Log.buf().append("remove: ")
                            .append(chat.toString()).toString());
                }
                continue;
            }
        }
        for (Iterator<MessageChat> it = chatsRunningUe.iterator(); it.hasNext(); ) {
            MessageChat chat = it.next();
            if (chat.isRemovedTiming(vpos)) {
                chat.prepareRemove(this);
                it.remove();
                if (DEBUG_LOGV) {
                    Log.v(LOG_TAG, Log.buf().append("remove: ")
                            .append(chat.toString()).toString());
                }
                continue;
            }
        }
    }

    private void addMessageChatToData(
            MessageChat chat, MessageData messageData, int vpos) {
        final List<MessageChat> chatsRunningNaka = messageData.getChatsRunningNaka();
        final List<MessageChat> chatsRunningShita = messageData.getChatsRunningShita();
        final List<MessageChat> chatsRunningUe = messageData.getChatsRunningUe();
        final Paint paintText = mPaintText;
        final Random random = mRandom;

        chat.prepareAdd(this, paintText, vpos);
        if (chat.getPos() == MessageChat.POS_NAKA) {
            int nextY = chat.computeNakaNextY(vpos, chatsRunningNaka, random);
            chat.setY(nextY, paintText);

            chat.addNakaOrder(chatsRunningNaka);
        } else if (chat.getPos() == MessageChat.POS_SHITA) {
            int nextY = chat.computeShitaNextY(chatsRunningShita, random);
            chat.setY(nextY, paintText);

            chat.addShitaOrder(chatsRunningShita);
        } else if (chat.getPos() == MessageChat.POS_UE) {
            int nextY = chat.computeUeNextY(chatsRunningUe, random);
            chat.setY(nextY, paintText);

            chat.addUeOrder(chatsRunningUe);
        } else {
            assert false;
        }

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("current-vpos=").append(vpos)
                    .append(" vpos=").append(chat.getVpos())
                    .append(" chat=").append(chat.getText()).toString());
        }
    }

    private void drawMessageChatMain(
            MessageData messageData, int vpos, Canvas canvas) {
        final List<MessageChat> chatsRunningNaka = messageData.getChatsRunningNaka();
        final List<MessageChat> chatsRunningShita = messageData.getChatsRunningShita();
        final List<MessageChat> chatsRunningUe = messageData.getChatsRunningUe();
        final Paint paintText = mPaintText;

        for (MessageChat chat : chatsRunningNaka) {
            chat.draw(vpos, canvas, paintText, this);
        }

        for (MessageChat chat : chatsRunningShita) {
            chat.draw(vpos, canvas, paintText, this);
        }

        for (MessageChat chat : chatsRunningUe) {
            chat.draw(vpos, canvas, paintText, this);
        }
    }

    private void setMatrixToCanvas(Canvas canvas,
            int videoWidth, int videoHeight) {
        setPlayerMatrixToCanvas(canvas, videoWidth, videoHeight, mMatrixMessage);
    }

    /**
     * canvas の Matrix を変更するので、必要に応じて後で元に戻すこと
     * @param canvas
     * @param videoWidth
     * @param videoHeight
     * @param matrixMessage
     */
    public static void setPlayerMatrixToCanvas(Canvas canvas,
            int videoWidth, int videoHeight, final Matrix matrixMessage) {
        matrixMessage.reset();
        final float scaleY = (float) videoHeight / (float) REAL_PLAYER_HEIGHT_PX_4_3;
//    	final float scaleX = (float) videoWidth / (float) REAL_PLAYER_WIDTH_PX_4_3;
        final float scaleX = scaleY;
        matrixMessage.setScale(
                scaleX,
                scaleY);
        // TODO 適当に横座標ずらす
        final float translateX = ((float) videoWidth / scaleX
                - (float) REAL_PLAYER_WIDTH_PX_4_3) / 2.0f;
//    	matrixMessage.postTranslate(translateX, 0.0f);
        matrixMessage.preTranslate(translateX, 0.0f);
        canvas.concat(matrixMessage);
//    	canvas.clipRect(0, 0, videoWidth, videoHeight);

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("setMatrixToCanvas: videoWidth=")
                    .append(videoWidth).append(" videoHeight=").append(videoHeight)
                    .append(" matrixMessage=").append(matrixMessage.toShortString())
                    .toString());
        }
    }

    public void setCommandDefault(NicoScriptDefault messageChat) {
        mCommandDefault = messageChat;
    }
    public void clearCommandDefault(NicoScriptDefault messageChat) {
        if (mCommandDefault == messageChat) {
            mCommandDefault = null;
        }
    }

    public int getDefaultColor() {
        if (mCommandDefault == null) {
            return MessageChat.COLOR_WHITE;
        } else {
            return mCommandDefault.getColor();
        }
    }

    public int getDefaultFontSize() {
        if (mCommandDefault == null) {
            return MessageChat.FONTSIZE_PX_MEDIUM;
        } else {
            return mCommandDefault.getFontSize();
        }
    }
    public int getDefaultFontSizeType() {
        if (mCommandDefault == null) {
            return MessageChat.FONTSIZE_TYPE_MEDIUM;
        } else {
            return mCommandDefault.getFontSizeType();
        }
    }
    public int getDefaultLineHeight() {
        if (mCommandDefault == null) {
            return MessageChat.LINEHEIGHT_PX_MEDIUM;
        } else {
            return mCommandDefault.getLineHeight();
        }
    }

    public int getDefaultPos() {
        if (mCommandDefault == null) {
            return MessageChat.POS_NAKA;
        } else {
            return mCommandDefault.getPos();
        }
    }

    public void setCommandGyaku(NicoScriptGyaku messageChat) {
        mCommandGyaku = messageChat;
    }
    public void clearCommandGyaku(NicoScriptGyaku messageChat) {
        if (mCommandGyaku == messageChat) {
            mCommandGyaku = null;
        }
    }

    public int getDir(boolean isFork) {
        if (mCommandGyaku == null) {
            return MessageChat.DIR_NORMAL;
        } else {
            return mCommandGyaku.getDir(isFork);
        }
    }

    public void forbidSeek() {
        PlayerActivity playerActivity = mPlayerActivity.get();
        if (playerActivity == null) {
            Log.w(LOG_TAG, "MessageChatController#forbidSeek: PlayerActivity weak reference return null");
            return;
        }
        playerActivity.forbidSeekByContributor();
    }

    public void allowSeek() {
        PlayerActivity playerActivity = mPlayerActivity.get();
        if (playerActivity == null) {
            Log.w(LOG_TAG, "MessageChatController#allowSeek: PlayerActivity weak reference return null");
            return;
        }
        playerActivity.allowSeekByContributor();
    }

    public void addCommandReplace(NicoScriptReplace messageChat) {
        mCommandReplace.add(messageChat);
    }
    public void removeCommandReplace(NicoScriptReplace messageChat) {
        mCommandReplace.remove(messageChat);
    }

    public NicoScriptReplace getReplaceIfMatch(MessageChat chat) {
        boolean isFork = chat.isFork();
        for (NicoScriptReplace replace : mCommandReplace) {
            if (isFork && !replace.isIncludeContributor()) {
                continue;
            }
            if (replace.match(chat.getText())) {
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append("Replace Match: ")
                            .append(chat).toString());
                }
                return replace;
            }
        }
        return null;
    }

    public void setNgUp(HashMap<String, String> ngUp) {
        mNgUp = ngUp;
        if (ngUp == null) {
            mNgUpKey = null;
        } else {
            mNgUpKey = mNgUp.keySet();
        }
    }

    public String applyNgUp(String text) {
        if (text == null || mNgUp == null) {
            return text;
        }
        assert mNgUpKey != null;
        for (String keyword : mNgUpKey) {
            String replace = mNgUp.get(keyword);
            text = text.replace(keyword, replace);
        }
        return text;
    }

    public void setNicoScriptView(NicoScriptView view) {
        mNicoScriptView = view;
    }

    public void clearCommandJump(NicoScriptJump nicoScriptJump) {
        if (mCommandJump == nicoScriptJump) {
            mCommandJump = null;
        }
    }

    public void setCommandJump(NicoScriptJump nicoScriptJump) {
        mCommandJump = nicoScriptJump;
    }

    private void jumpByNicoScript(int currentSecond,
            NicoScript.Jump commandJump) {
        if (commandJump == null) {
            Log.w(LOG_TAG, "MessageChatController#jumpByNicoScript: commandJump was cleared?");
            return;
        }

        AbstractPlayerFragment playerFragment = mPlayerFragment.get();
        if (playerFragment == null) {
            Log.w(LOG_TAG, "MessageChatController#jumpByNicoScript: PlayerFragment weak reference return null");
            return;
        }

        String jumpMessage = commandJump.getJumpMessage();
        String jumpVideoNumber = commandJump.getJumpToVideoNumber();
        int delayTime;
        if (jumpMessage != null
                && (commandJump.hasJumpMessage() || jumpVideoNumber != null)) {
            mNicoScriptView.startJumpMessage(jumpMessage);
            delayTime = JUMP_MESSAGE_TIME_MS;
        } else {
            // すぐにジャンプ
            delayTime = 0;
        }

        if (jumpVideoNumber == null) {
            String jumpLabel = commandJump.getJumpToLabel();
            int jumpSecond;
            if (jumpLabel == null) {
                jumpSecond = commandJump.getJumpToSecond();
            } else {
                jumpSecond = getJumpMarkerSecond(jumpLabel);
                if (jumpSecond < 0) {
                    Log.w(LOG_TAG, Log.buf().append("jump label ").append(jumpLabel)
                            .append(" is not found, ignore").toString());
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, mJumpMarker.toString());
                    }
                    return;
                }
            }
            playerFragment.postSeekBySecond(jumpSecond, delayTime);
        } else {
            int jumpStartTimeSecond = commandJump.getJumpStartTimeSecond();
            int returnTimeSecond = commandJump.getReturnTimeSecond();
            String returnMessage = commandJump.getReturnMessage();
            playerFragment.postJumpVideo(jumpVideoNumber, jumpStartTimeSecond,
                    returnTimeSecond, returnMessage, currentSecond,
                    delayTime);
            playerFragment.pausePlay();
        }
    }

    private int getJumpMarkerSecond(String jumpLabel) {
        Integer vpos = mJumpMarker.get(jumpLabel);
        if (vpos == null) {
            return -1;
        } else {
            return vpos / 100;
        }
    }

    public void addCommandJumpMarker(NicoScriptJumpMarker nicoScriptJumpMarker) {
        mJumpMarker.put(nicoScriptJumpMarker.getMarkerLabel(),
                nicoScriptJumpMarker.getVpos());
    }

    public void setReturnJump(String returnVideoNumber, int returnTimeSecond,
            String returnMessage, int returnStartTimeSecond) {
        mReturnVideoNumber = returnVideoNumber;
        mReturnTimeSecond = returnTimeSecond;
        mReturnMessage = returnMessage;
        mReturnStartTimeSecond = returnStartTimeSecond;
    }

    private void jumpReturnVideo() {
        AbstractPlayerFragment playerFragment = mPlayerFragment.get();
        if (playerFragment == null) {
            Log.w(LOG_TAG, "MessageChatController#jumpByNicoScript: PlayerFragment weak reference return null");
            return;
        }

        int delayTime;
        if (mReturnMessage == null) {
            // すぐにジャンプ
            delayTime = 0;
        } else {
            mNicoScriptView.startJumpMessage(mReturnMessage);
            delayTime = JUMP_MESSAGE_TIME_MS;
        }

        assert mReturnVideoNumber != null;
        playerFragment.postJumpReturnVideo(mReturnVideoNumber,
                mReturnStartTimeSecond, delayTime);
        playerFragment.pausePlay();
    }

    private void onUpdateVideoPosition(int second) {
        // 定期的に監視が必要なニコスクリプトの処理
        if (mCommandJump != null) {
            jumpByNicoScript(second, mCommandJump);
            // １回ジャンプしたらクリア
            mCommandJump = null;
        }
        if (mReturnVideoNumber != null && mReturnTimeSecond >= 0) {
            if (second >= mReturnTimeSecond) {
                jumpReturnVideo();
                // １回ジャンプしたらクリア
                mReturnVideoNumber = null;
                mReturnMessage = null;
            }
        }
    }

    /**
     * 書き込むコメント内容と動作中のニコスクリプトを照合する
     * @param body 書き込むコメント本文
     * @param vpos 書き込む時間 単位：vpos（1/100秒）
     * @return COMMENT_TYPE_*
     */
    public int collateCommentWithNicoScript(String body, int vpos) {
        for (NicoScriptReplace script : mCommandReplace) {
            if (script.match(body)) {
                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        Iterator<NicoScriptKeywordJump> it = mCommandKeywordJump.iterator();
        while (it.hasNext()) {
            NicoScriptKeywordJump script = it.next();
            if (script.match(body)) {
                // ジャンプ
                // XXX 一応３秒のタイムラグがあるが、サーバーへの書き込み完了前にジャンプしてしまう可能性
                jumpByNicoScript(vpos / 100, script);
                // １回ジャンプしたらクリア
                it.remove();

                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        for (NicoScriptVote script : mCommandVote) {
            if (script.match(body)) {

                // TODO とりあえずparseだけでメイン動作は後で実装

                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        for (NicoScriptScore script : mCommandScore) {
            if (script.match(body)) {

                // TODO とりあえずparseだけでメイン動作は後で実装

                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        for (NicoScriptBall script : mCommandBall) {
            if (script.match(body)) {

                // TODO とりあえずparseだけでメイン動作は後で実装

                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        for (NicoScriptWindow script : mCommandWindow) {
            if (script.match(body)) {

                // TODO とりあえずparseだけでメイン動作は後で実装

                if (script.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        if (mCommandDoor != null) {
            if (mCommandDoor.match(body)) {

                // TODO とりあえずparseだけでメイン動作は後で実装

                if (mCommandDoor.isSaveLocal()) {
                    return COMMENT_TYPE_LOCAL;
                } else {
                    return COMMENT_TYPE_NICOS;
                }
            }
        }

        return COMMENT_TYPE_NORMAL;
    }

    /**
     * 書き込んだコメントを待ち行列に追加<br>
     * 画面表示に反映される
     * @param no
     * @param mail
     * @param body
     * @param vpos
     * @param userId
     */
    public void addMessage(int no, String mail, String body, int vpos,
            String userId) {
        // とりあえず現在時刻を使用
        long date = System.currentTimeMillis() / 1000L;
        MessageChat chat = new MessageChat(mail, vpos, no, date, userId, 0);
        chat.setText(body);
        chat.setMySendComment();

        // TODO シークで戻る等すると自分で書いたコメントが表示されない
        // ワークエリアのコメントデータにのみ追加するため
        final Comparator<MessageChat> comp = MessageChat.getVposComparator();
        synchronized (mSyncMessageData) {
            final List<MessageChat> chatsWait = mMessageData.getChatsWait();
            final int size = chatsWait.size();
            for (int i = 0; i < size; ++i) {
                if (comp.compare(chat, chatsWait.get(i)) <= 0) {
                    chatsWait.add(i, chat);
                    break;
                }
            }
        }
    }

    public boolean isMessageDataOk() {
        return mMessageData.isMessageOk() && mMessageDataFork.isMessageOk();
    }

    /**
     * コメントのデータを設定
     * @param chats
     * @param chatsFork
     */
    public void setMessageDataChats(Collection<MessageChat> chats,
            Collection<MessageChat> chatsFork) {
        synchronized (mSyncMessageData) {
            mMessageData.setChats(chats);
            mMessageDataFork.setChats(chatsFork);
        }
    }

    /**
     * 通常コメントのデータのみ初期化
     * @param chats
     */
    public void resetMessageDataChats(Collection<MessageChat> chats) {
        synchronized (mSyncMessageData) {
            mMessageData.resetChats(chats);
        }
    }

    /**
     * コメントのデータを初期化
     * @param chats 通常コメント
     * @param chatsFork 投稿者コメント
     */
    public void resetMessageDataChats(Collection<MessageChat> chats,
            Collection<MessageChat> chatsFork) {
        synchronized (mSyncMessageData) {
            mMessageData.resetChats(chats);
            mMessageDataFork.resetChats(chatsFork);
        }
    }

    public void clearMessageData() {
        mMessageData.clear();
        mMessageDataFork.clear();
    }

    /**
     * NG設定の設定
     * @param configureNgClients
     */
    public void setNgClients(ArrayList<ConfigureNgClientInterface.NgClient> configureNgClients) {
        mConfigureNgClientsId = new ArrayList<ConfigureNgClientInterface.NgClient>();
        mConfigureNgClientsWord = new ArrayList<ConfigureNgClientInterface.NgClient>();
        mConfigureNgClientsCommand = new ArrayList<ConfigureNgClientInterface.NgClient>();
        for (ConfigureNgClientInterface.NgClient ngClient : configureNgClients) {
            switch (ngClient.type) {
                case ConfigureNgClientInterface.NG_TYPE_ID:
                    mConfigureNgClientsId.add(ngClient);
                    break;
                case ConfigureNgClientInterface.NG_TYPE_WORD:
                    mConfigureNgClientsWord.add(ngClient);
                    break;
                case ConfigureNgClientInterface.NG_TYPE_COMMAND:
                    mConfigureNgClientsCommand.add(ngClient);
                    break;
                default:
                    assert false : ngClient.type;
                    break;
            }
        }
    }

    /**
     * NG設定に当てはまるか判定
     * @param chat
     * @return
     */
    boolean isNg(MessageChat chat) {
        if (chat.getScore() <= mNgShareLevelThreshold) {
            return true;
        }

        if (mConfigureNgClientsId != null) {
            String userId = chat.getUserId();
            for (ConfigureNgClientInterface.NgClient ngClient
                    : mConfigureNgClientsId) {
                assert ngClient.source != null;
                if (ngClient.source.equals(userId)) {
                    return true;
                }
            }
        }
        if (mConfigureNgClientsWord != null) {
            String text = chat.getText();
            if (text != null) {
                text = ConfigureNgClient.convertCaseForNgWord(text);
                for (ConfigureNgClientInterface.NgClient ngClient
                        : mConfigureNgClientsWord) {
                    if (text.indexOf(ngClient.source) >= 0) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public void setNgShareLevel(int level) {
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("setNgShareLevel=").append(level)
                    .toString());
        }
        switch (level) {
            case NG_SHARE_LEVEL_NONE:
                mNgShareLevelThreshold = Integer.MIN_VALUE;
                mNgShareLevel = level;
                break;
            case NG_SHARE_LEVEL_LIGHT:
                mNgShareLevelThreshold = -10000;
                mNgShareLevel = level;
                break;
            case NG_SHARE_LEVEL_MIDDLE:
                mNgShareLevelThreshold = -4800;
                mNgShareLevel = level;
                break;
            case NG_SHARE_LEVEL_HIGH:
                mNgShareLevelThreshold = -1000;
                mNgShareLevel = level;
                break;
        }
    }

    public int getNgShareLevel() {
        return mNgShareLevel;
    }

    public void resetNicoScript() {
        mCommandDefault = null;
        mCommandGyaku = null;
        mCommandReplace.clear();
        mCommandJump = null;
        mCommandKeywordJump.clear();
        mCommandVote.clear();
        mCommandScore.clear();
        mCommandBall.clear();
        mCommandWindow.clear();
        mCommandDoor = null;
        mForbidCommentNicoScript = false;
        mJumpMarker.clear();
    }

    public void forbidComment() {
        mForbidCommentNicoScript = true;

        PlayerActivity playerActivity = mPlayerActivity.get();
        if (playerActivity == null) {
            Log.w(LOG_TAG, "MessageChatController#forbidComment: PlayerActivity weak reference return null");
            return;
        }
        playerActivity.forbidCommentByContributor();
    }

    public void allowComment() {
        mForbidCommentNicoScript = false;

        PlayerActivity playerActivity = mPlayerActivity.get();
        if (playerActivity == null) {
            Log.w(LOG_TAG, "MessageChatController#forbidComment: PlayerActivity weak reference return null");
            return;
        }
        playerActivity.allowCommentByContributor();
    }

    public void addCommandKeywordJump(NicoScriptKeywordJump messageChat) {
        mCommandKeywordJump.add(messageChat);
    }
    public void removeCommandKeywordJump(NicoScriptKeywordJump messageChat) {
        mCommandKeywordJump.remove(messageChat);
    }

    public void addCommandVote(NicoScriptVote messageChat) {
        mCommandVote.add(messageChat);
    }
    public void removeCommandVote(NicoScriptVote messageChat) {
        mCommandVote.remove(messageChat);
    }

    public void addCommandScore(NicoScriptScore messageChat) {
        mCommandScore.add(messageChat);
    }
    public void removeCommandScore(NicoScriptScore messageChat) {
        mCommandScore.remove(messageChat);
    }

    public void addCommandBall(NicoScriptBall messageChat) {
        mCommandBall.add(messageChat);
    }
    public void removeCommandBall(NicoScriptBall messageChat) {
        mCommandBall.remove(messageChat);
    }

    public void addCommandWindow(NicoScriptWindow messageChat) {
        mCommandWindow.add(messageChat);
    }
    public void removeCommandWindow(NicoScriptWindow messageChat) {
        mCommandWindow.remove(messageChat);
    }

    public void setCommandDoor(NicoScriptDoor messageChat) {
        mCommandDoor = messageChat;
    }
    public void clearCommandDoor(NicoScriptDoor messageChat) {
        if (mCommandDoor == messageChat) {
            mCommandDoor = null;
        }
    }
}
