package jp.sourceforge.nicoro;

import static jp.sourceforge.nicoro.Log.LOG_TAG;
import static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_HIGH;
import static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_MID;
import static jp.sourceforge.nicoro.NicoroAPIManager.ECO_TYPE_LOW;
import static jp.sourceforge.nicoro.PlayerConstants.*;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import jp.gr.java_conf.shiseissi.commonlib.APILevelWrapper;
import jp.gr.java_conf.shiseissi.commonlib.ViewUtil;

public abstract class AbstractPlayerFragment extends Fragment
implements PlayerInfoDialogFragment.PlayDataAppender, PlayerInfoDialogFragment.TimeAppender,
DestroyTask.Callback, Handler.Callback {
    private static final boolean DEBUG_LOGV = Release.IS_DEBUG & true;
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;

    protected static final String INFO_TIME_DEFAULT = "000:00";

    private static final String KEY_SAVE_CURRENT_PLAY_TIME_NUM = "key_save_current_play_time_num";
    private static final String KEY_SAVE_CURRENT_PLAY_TIME_DEN = "key_save_current_play_time_den";
    private static final String KEY_SAVE_IS_PAUSE_PLAY = "key_save_is_pause_play";
    private static final String KEY_PLAY_REPEAT = "key_play_repeat";
    private static final String SHARED_PREFERENCES_PLAY_STATE = "play_state";
    private static final int PLAYER_TYPE_FFMPEG = 0;
    private static final int PLAYER_TYPE_MEDIAPLAYER = 1;
    private static final int PLAYER_TYPE_SWF = 2;

    private static final long INTERVAL_UPDATE_COMMENT_INNER_MS = 500L;

    protected Context mContext;
    protected SharedPreferences mSharedPreferences;

    protected VideoLoaderInterface mVideoLoader =
        VideoLoaderInterface.NullObject.getInstance();
    private MessageLoaderInterface mMessageLoader =
        MessageLoaderInterface.NullObject.getInstance();
    private MessageSenderInterface mMessageSender =
        MessageSenderInterface.NullObject.getInstance();

    protected ThumbInfoInterface mThumbInfo =
        ThumbInfoInterface.NullObject.getInstance();

    private ConfigureNgClientInterface mConfigureNgClient =
        ConfigureNgClient.NullObject.getInstance();

    protected NicoScriptView mNicoScriptView;

    protected Rational mRationalCurrentPlayTime = new Rational();

    protected final HandlerWrapper mHandler = new HandlerWrapper(this);

    protected MessageChatController mMessageChatController = new MessageChatController();

    protected StateManager mStateManager = new StateManager();
    protected boolean mDidStartPlay = false;

    private Rational mSaveStateCurrentPlayTime;
    private boolean mSaveIsPausePlay;
    protected boolean mIsVideoDownloadOk;
    protected boolean mIsVideoCachedOk;

    protected boolean mShowHintToast;

    protected int mLastOrientation;

    /** 再生開始位置（秒） */
    private int mStartTimeSecond;

    private boolean mPlayRepeat = false;

    private String mResStrPrefKeyNgShareLevel;

    private NewPlayerFragmentCreator mNewPlayerFragmentCreator;

    private class AsyncDestroyTask extends DestroyTask {
        AsyncDestroyTask() {
            super(AbstractPlayerFragment.this);
        }
        @Override
        protected void onPreExecuteImpl() {
        }
        @Override
        protected void onPostExecuteImpl() {
        }
    }
    private AsyncDestroyTask mDestroyTask = new AsyncDestroyTask();
    private final Object mSyncDestroyTaskOnDestroyImpl = new Object();
    private boolean mEndOnDestroyImpl = false;

    private static class BackgroundPlayerServiceConnection
    extends ServiceConnectionWrapper<AbstractPlayerFragment> {
        public BackgroundPlayerServiceConnection(AbstractPlayerFragment ref) {
            super(ref);
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            super.onServiceConnected(name, service);

            AbstractPlayerFragment fragment = getBody();
            if (fragment == null) {
                return;
            }

            fragment.mBackgroundPlayerMessenger = new Messenger(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            AbstractPlayerFragment fragment = getBody();
            if (fragment != null) {
                fragment.mBackgroundPlayerMessenger = null;
                fragment.mBackgroundPlayerServiceConnection = null;
            }

            super.onServiceDisconnected(name);
        }
    }
    private BackgroundPlayerServiceConnection mBackgroundPlayerServiceConnection;
    private Messenger mBackgroundPlayerMessenger;

    @Override
    public boolean handleMessage(Message msg) {
        PlayerActivity playerActivity = getPlayerActivity();
        Handler parentHandler = (playerActivity == null) ? null
                : playerActivity.getHandler();
        switch (msg.what) {
        case MSG_ID_MESSAGE_FINISHED:
            if (parentHandler != null) {
                parentHandler.sendEmptyMessage(MSG_ID_MESSAGE_FINISHED);
            }
            mMessageChatController.setMessageDataChats(
                    mMessageLoader.getChats(),
                    mMessageLoader.getChatsFork());
            mMessageSender.setTicket(mMessageLoader.getTicket(),
                    mMessageLoader.getNicosTicket());
            if (canStartPlay()) {
                startPlay();
            }
            break;
        case MSG_ID_MESSAGE_OCCURRED_ERROR:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_MESSAGE_OCCURRED_ERROR,
                        msg.obj).sendToTarget();
            }
            break;
        case MSG_ID_THUMBINFO_FINISHED:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_THUMBINFO_FINISHED,
                        mIsVideoDownloadOk ? 1 : 0, 0, mThumbInfo).sendToTarget();
            }
            mMessageLoader.setVideoLength((String) msg.obj);
            mMessageLoader.startLoad();
            break;
        case MSG_ID_THUMBINFO_OCCURRED_ERROR:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_THUMBINFO_OCCURRED_ERROR,
                        msg.obj).sendToTarget();
            }
            break;
        case MSG_ID_VIDEO_OCCURRED_ERROR:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_VIDEO_OCCURRED_ERROR,
                        msg.obj).sendToTarget();
            }
            break;
        case MSG_ID_VIDEO_NOTIFY_PROGRESS:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_VIDEO_NOTIFY_PROGRESS,
                        msg.arg1, msg.arg2).sendToTarget();
            }
            break;
        case MSG_ID_PLAY_ERROR:
            if (parentHandler != null) {
                parentHandler.obtainMessage(MSG_ID_PLAY_ERROR,
                        msg.obj).sendToTarget();
            }
            break;
        case MSG_ID_ENABLE_SEEK_BAR:
            if (parentHandler != null) {
                parentHandler.sendEmptyMessage(MSG_ID_ENABLE_SEEK_BAR);
            }
            break;
        case MSG_ID_INFO_PLAY_DATA_UPDATE:
            if (parentHandler != null) {
                parentHandler.sendEmptyMessage(MSG_ID_INFO_PLAY_DATA_UPDATE);
            }
            break;
        case MSG_ID_THUMBINFO_FINISHED_NEW:
            mThumbInfo = (ThumbInfo) msg.obj;
            mHandler.obtainMessage(MSG_ID_THUMBINFO_FINISHED,
                    mThumbInfo.getLength()).sendToTarget();
            break;
        case MSG_ID_THUMBINFO_OCCURRED_ERROR_NEW:
            String errorMessage = (String) msg.obj;
            mHandler.obtainMessage(MSG_ID_THUMBINFO_OCCURRED_ERROR,
                    errorMessage).sendToTarget();
            break;
        case MSG_ID_PLAY_FINISHED:
            // XXX 再生モードや動画によっては終了時間がずれるので、最後に手動で起動
            int endTime = mThumbInfo.getLengthBySecond() + 1;
            updateMessageInner(endTime * 100);

            if (playerActivity != null && playerActivity.hasPlaylist()
                    && playerActivity.movePlaylistNextTrack()) {
                // 再生リスト移動成功の場合はここでは何もせず
            } else if (mPlayRepeat) {
                if (playerActivity != null) {
                    playerActivity.rewindToBegin();
                    restartPlay();
                }
            } else {
                // ジャンプ中の場合はダイアログ表示しない
                if (!mHandler.hasMessages(MSG_ID_SEEK_BY_SECOND)
                        && !mHandler.hasMessages(MSG_ID_JUMP_VIDEO)
                        && (mNewPlayerFragmentCreator == null
                                || !mNewPlayerFragmentCreator.isRunning())) {
                    // ダイアログ表示は少し遅らせる
                    if (parentHandler != null) {
                        parentHandler.sendEmptyMessageDelayed(MSG_ID_PLAY_FINISHED_DIALOG,
                                1000);
                    }
                }
            }
            break;
        case MSG_ID_VIDEO_DOWNLOAD_FINISHED:
            if (parentHandler != null) {
                parentHandler.sendEmptyMessage(MSG_ID_VIDEO_DOWNLOAD_FINISHED);
            }
            mIsVideoDownloadOk = true;
            break;
        case MSG_ID_SEEK_BY_SECOND: {
            int second = msg.arg1;
            seekBySecond(second);
        } break;
        case MSG_ID_JUMP_VIDEO:
            Bundle args = getArguments();
            int forceEco = args.getInt(
                    INTENT_NAME_FORCE_ECO, ECO_TYPE_HIGH);

            Bundle newArgs = msg.peekData();
            assert newArgs != null;
            newArgs.putInt(INTENT_NAME_FORCE_ECO,
                    forceEco);

            if (mNewPlayerFragmentCreator == null) {
                mNewPlayerFragmentCreator = new NewPlayerFragmentCreator(
                        mContext, mHandler,
                        MSG_ID_NEW_PLAYER_FRAGMENT, MSG_ID_PLAY_ERROR);
            }
            mNewPlayerFragmentCreator.execute(newArgs);
            break;
        case MSG_ID_UPDATE_COMMENT_INNER:
            if (!mStateManager.wasDestroyed() && !mStateManager.isStarting()) {
                // 表示中でないときはここでコメントの内部状態更新
                int vpos = getVpos();
                updateMessageInner(vpos);

                // 繰り返し
                mHandler.sendEmptyMessageDelayed(
                        MSG_ID_UPDATE_COMMENT_INNER,
                        INTERVAL_UPDATE_COMMENT_INNER_MS);
            }
            break;
        case MSG_ID_MESSAGE_SEND_FINISHED: {
            Bundle data = msg.peekData();
            if (data != null) {
                int no = data.getInt(KEY_CHAT_NO);
                String mail = data.getString(KEY_CHAT_MAIL);
                String body = data.getString(KEY_CHAT_BODY);
                int vpos = data.getInt(KEY_CHAT_VPOS);
                String userId = getArguments().getString(
                        INTENT_NAME_USER_ID);
                mMessageChatController.addMessage(no, mail, body, vpos, userId);
            }
        } break;
        case MSG_ID_MESSAGE_SEND_OCCURRED_ERROR:
            Util.showErrorToast(mContext, R.string.toast_send_comment_error);
            break;
        case MSG_ID_GET_CONFIGURE_NG_CLIENT_FINISHED:
            ArrayList<ConfigureNgClientInterface.NgClient> configureNgClients =
                mConfigureNgClient.getNgClients();
            mMessageChatController.setNgClients(configureNgClients);
            break;
        case MSG_ID_GET_CONFIGURE_NG_CLIENT_OCCURRED_ERROR:
            Util.showErrorToast(mContext, R.string.toast_get_configure_ng_client_error);
            break;
        case MSG_ID_NEW_PLAYER_FRAGMENT:
            if (playerActivity != null) {
                if (parentHandler != null) {
                    parentHandler.removeMessages(MSG_ID_INFO_TIME_UPDATE);
                    parentHandler.removeMessages(MSG_ID_NOTIFICATION_TIME_UPDATE);
                }
                playerActivity.setPlayerFragment(null);

                Bundle result = msg.peekData();
                String flvUrl = result.getString(INTENT_NAME_VIDEO_URL);
                AbstractPlayerFragment newFragment =
                    playerActivity.createPlayerFragment(flvUrl);
                newFragment.setArguments(result);

                replacePlayerFragment(newFragment, true);

                finishFragment();
            }
            break;
        default:
            assert false : msg.what;
            break;
        }
        return true;
    }

    /**
     * すべての再生準備が整ったか確認する（動画ファイルのダウンロード、コメントファイルのダウンロード等）
     * @return
     */
    protected abstract boolean canStartPlay();

    /**
     * 動画の再生位置を取得する
     * @param rational 位置情報（秒単位の分数）
     */
    public abstract void getCurrentPositionVideoPlay(Rational rational);
    /**
     * 音声の再生位置を取得する
     * @param rational 位置情報（秒単位の分数）
     */
    protected abstract void getCurrentPositionAudioPlay(Rational rational);
    /**
     * 動画のデコード位置を取得する
     * @param rational 位置情報（秒単位の分数）
     */
    protected abstract void getCurrentPositionVideoDecode(Rational rational);
    /**
     * 音声のデコード位置を取得する
     * @param rational 位置情報（秒単位の分数）
     */
    protected abstract void getCurrentPositionAudioDecode(Rational rational);

    /**
     * 再生状態と一時停止状態を切り替える
     * @return 成否
     */
    protected abstract boolean switchPausePlay();
    /**
     * 再生を一時停止する
     */
    protected abstract void pausePlay();
    /**
     * 一時停止していた再生を再開する
     */
    protected abstract void restartPlay();
    /**
     * 再生が一時停止中か確認する
     * @return
     */
    protected abstract boolean isPausePlay();

    /**
     * 秒単位でシークする
     * @param second
     */
    protected abstract void seekBySecond(int second);

    protected abstract VideoLoaderInterface.EventListener createVideoLoaderEventListener();

    protected abstract int getVpos();

    @Override
    public StringBuilder appendMovieType(StringBuilder builder) {
        int ecoType = mVideoLoader.getEcoType();
        if (ecoType == ECO_TYPE_LOW) {
            builder.append("low-")
                .append(mVideoLoader.getMovieType());
        } else if (ecoType == ECO_TYPE_MID) {
            builder.append("mid-")
                .append(mVideoLoader.getMovieType());
        } else {
            String movieType = mThumbInfo.getMovieType();
            if (movieType != null) {
                builder.append(movieType);
            }
        }
        return builder;
    }

    @Override
    public StringBuilder appendTotalPlayTime(StringBuilder builder) {
        return builder.append(mThumbInfo.getFormattedLengthForPlayer());
    }

    protected abstract void startPlay();

    protected void startPlay(ThumbInfoInterface thumbInfo) {
        PlayerActivity playerActivity = getPlayerActivity();
        if (playerActivity != null) {
            playerActivity.startPlay(thumbInfo);

            Handler parentHandler = playerActivity.getHandler();
            parentHandler.sendEmptyMessage(MSG_ID_INFO_TIME_UPDATE);
//            parentHandler.sendEmptyMessage(MSG_ID_INFO_CLOCK_UPDATE);
            parentHandler.sendEmptyMessage(MSG_ID_NOTIFICATION_TIME_UPDATE);
        }

        if (!thumbInfo.isNull()) {
            mMessageChatController.setDuration(thumbInfo.getLengthByVpos());
        }

        if (mStartTimeSecond > 0) {
            seekBySecond(mStartTimeSecond);
            mStartTimeSecond = 0;
        }

        mDidStartPlay = true;
    }

    protected void postStartPlayIfIsRestored() {
        if (mStateManager.wasRestored()) {
            // 再開時は一時停止にしておく
            pausePlay();
            setButtonPauseImage();
            int second = -1;
            if (mSaveStateCurrentPlayTime != null) {
                second = (int) (mSaveStateCurrentPlayTime.num / mSaveStateCurrentPlayTime.den);
                if (second != 0) {  // 0のときはシーク不要
                    seekBySecond(second);
                }
            }
            PlayerActivity playerActivity = getPlayerActivity();
            if (playerActivity != null) {
                playerActivity.postStartPlay(second, mThumbInfo.getLengthBySecond());
            }
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onCreate: savedInstanceState=")
                    .append(savedInstanceState).toString());
        }
        super.onCreate(savedInstanceState);
        mStateManager.onCreate(savedInstanceState);

        Activity activity = getActivity();
        mContext = activity.getApplicationContext();

        mSharedPreferences = Util.getDefaultSharedPreferencesMultiProcess(
                mContext);
        SharedPreferences prefPlayState = getSharedPreferencesPlayState(
                mContext);

        BackgroundPlayerService.stopService(mContext);
        SharedPreferences.Editor prefEditorPlayState =
            NicoroApplication.getInstance(activity).getPrefEditorPlayState();
        if (mStateManager.wasRestored()) {
            // 復帰時は専用Preferencesからデータ読み込み
            prefEditorPlayState.commit();
            if (prefPlayState.contains(KEY_SAVE_CURRENT_PLAY_TIME_NUM)
                    && prefPlayState.contains(KEY_SAVE_CURRENT_PLAY_TIME_DEN)) {
                mSaveStateCurrentPlayTime = new Rational(
                        prefPlayState.getLong(KEY_SAVE_CURRENT_PLAY_TIME_NUM, 0),
                        prefPlayState.getInt(KEY_SAVE_CURRENT_PLAY_TIME_DEN, 1));
            }
            mSaveIsPausePlay = prefPlayState.getBoolean(KEY_SAVE_IS_PAUSE_PLAY,
                    false);
        }
        // 念のため専用Preferencesクリア
        prefEditorPlayState.clear().commit();

        mDidStartPlay = false;
        mIsVideoDownloadOk = false;
        mIsVideoCachedOk = false;

        mMessageChatController.clearMessageData();

        Bundle args = getArguments();
        if (args == null && savedInstanceState != null) {
            args = savedInstanceState.getBundle(Util.KEY_ARGUMENTS);
            setArguments(args);
        }
        assert args != null;
        loadThumbInfo(args);

        String userSession = NicoroConfig.getCookieUserSession(mSharedPreferences);
        if (!mMessageLoader.isNull() && mMessageLoader.getChats() != null) {
            mHandler.sendEmptyMessage(MSG_ID_MESSAGE_FINISHED);
        } else {
            mMessageLoader = createMessageLoader(args, mContext, userSession);
            mMessageLoader.setEventListener(new MessageLoaderInterface.EventListener() {
                @Override
                public void onFinished(MessageLoaderInterface messageLoader) {
                    Handler handler = mHandler;
                    handler.sendEmptyMessage(MSG_ID_MESSAGE_FINISHED);
                }
                @Override
                public void onOccurredError(MessageLoaderInterface messageLoader,
                        String errorMessage) {
                    Handler handler = mHandler;
                    handler.obtainMessage(MSG_ID_MESSAGE_OCCURRED_ERROR,
                            errorMessage).sendToTarget();
                }
            });
        }

        int authflag = mSharedPreferences.getInt(NicoroConfig.AUTHFLAG, 0);
        int premium = (authflag == 3) ? 1 : 0;
        mMessageSender = createMessageSender(args, mContext, userSession, premium);
        mMessageSender.setEventListener(new MessageSenderInterface.EventListener() {
            @Override
            public void onOccurredError(String errorMessage) {
                Log.e(LOG_TAG, errorMessage);
                Handler handler = mHandler;
                handler.sendEmptyMessage(MSG_ID_MESSAGE_SEND_OCCURRED_ERROR);
            }

            @Override
            public void onFinished(int no, String mail, String body, int vpos) {
                Handler handler = mHandler;
                Message msg = handler.obtainMessage(MSG_ID_MESSAGE_SEND_FINISHED);
                Bundle data = new Bundle(3);
                data.putInt(KEY_CHAT_NO, no);
                data.putString(KEY_CHAT_MAIL, mail);
                data.putString(KEY_CHAT_BODY, body);
                data.putInt(KEY_CHAT_VPOS, vpos);
                msg.setData(data);
                msg.sendToTarget();
            }
        });

        loadConfigureNgClient(userSession);

        boolean messageAntialias = mSharedPreferences.getBoolean(
                getString(R.string.pref_key_message_antialias), false);
        mMessageChatController.setAntiAlias(messageAntialias);

        mShowHintToast = mSharedPreferences.getBoolean(
                getString(R.string.pref_key_show_hint_toast), true);

        Configuration config = getResources().getConfiguration();
        mLastOrientation = config.orientation;

        mMessageChatController.setPlayerActivity(getPlayerActivity());
        mMessageChatController.setPlayerFragment(this);

        HashMap<String, String> ngUp = Util.getSerializable(args,
                INTENT_NAME_NG_UP);
        if (ngUp != null) {
            mMessageChatController.setNgUp(ngUp);
        }

        mResStrPrefKeyNgShareLevel = getString(R.string.pref_key_ng_share_level);
        int ngShareLevel = Util.parseInt(mSharedPreferences.getString(
                mResStrPrefKeyNgShareLevel, null),
                MessageChatController.NG_SHARE_LEVEL_NONE);
        mMessageChatController.setNgShareLevel(ngShareLevel);

        mStartTimeSecond = args.getInt(INTENT_NAME_START_TIME_SECOND);

        String returnVideoNumber = args.getString(INTENT_NAME_RETURN_VIDEO_NUMBER);
        if (returnVideoNumber != null) {
            int returnTimeSecond = args.getInt(INTENT_NAME_RETURN_TIME_SECOND, -1);
            String returnMessage = args.getString(INTENT_NAME_RETURN_MESSAGE);
            int returnStartTimeSecond = args.getInt(INTENT_NAME_RETURN_START_TIME_SECOND, 0);
            if (returnTimeSecond >= 0) {
                returnTimeSecond += mStartTimeSecond;
            }
            mMessageChatController.setReturnJump(returnVideoNumber,
                    returnTimeSecond, returnMessage,
                    returnStartTimeSecond);
        }

        if (savedInstanceState != null) {
            mPlayRepeat = savedInstanceState.getBoolean(KEY_PLAY_REPEAT);
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        View rootView = getView();
        mNicoScriptView = ViewUtil.findViewById(rootView, R.id.nicoscript_view);
        if (getPlayerActivity().getEnableHardwareAccelerated()) {
            // XXX 常にsoftware layer使用
            APILevelWrapper api = APILevelWrapper.createInstance();
            api.setLayerType(mNicoScriptView, View.LAYER_TYPE_SOFTWARE, null);
        }
        mMessageChatController.setNicoScriptView(mNicoScriptView);

        setSystemUiVisibility(rootView, mSharedPreferences, getResources());
    }

    @Override
    public void onStart() {
        super.onStart();
        mStateManager.onStart();
    }


    @Override
    public void onResume() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onResume").toString());
        }
        super.onResume();
        mStateManager.onResume();

        if (mBackgroundPlayerServiceConnection != null) {
            mBackgroundPlayerServiceConnection.unbindService(mContext);
            mBackgroundPlayerServiceConnection = null;
            mBackgroundPlayerMessenger = null;
        }
        BackgroundPlayerService.stopService(mContext);

        if (canStartPlay() && !mDidStartPlay) {
            startPlay();
        }
    }

    public void savePlayState() {
        if (mDidStartPlay) {
            if (mSaveStateCurrentPlayTime == null) {
                mSaveStateCurrentPlayTime = new Rational();
            }
            getCurrentPositionVideoPlay(mSaveStateCurrentPlayTime);
            mSaveIsPausePlay = isPausePlay();

            if (mBackgroundPlayerMessenger != null) {
                Message msg = Message.obtain();
                BackgroundPlayerService.putPlayState(msg,
                        mSaveStateCurrentPlayTime, mSaveIsPausePlay);
                try {
                    mBackgroundPlayerMessenger.send(msg);
                } catch (RemoteException e) {
                    Log.w(LOG_TAG, e.toString(), e);
                }
            }
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onSaveInstanceState: outState=")
                    .append(outState).toString());
        }
        super.onSaveInstanceState(outState);
        mStateManager.onSaveInstanceState();

        savePlayState();

        outState.putBoolean(KEY_PLAY_REPEAT, mPlayRepeat);

        outState.putBundle(Util.KEY_ARGUMENTS, getArguments());
    }

    @Override
    public void onPause() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onPause").toString());
        }
        super.onPause();
        mStateManager.onPause();

        boolean startBackgroundPlayer = false;
        if (mDidStartPlay) {
            startBackgroundPlayer = true;
            Activity a = getActivity();
            if (a != null && a.isFinishing()) {
                startBackgroundPlayer = false;
            }
        }
        if (startBackgroundPlayer) {
            BackgroundPlayerService.startService(mContext);
            mBackgroundPlayerServiceConnection = new BackgroundPlayerServiceConnection(this);
            if (!mBackgroundPlayerServiceConnection.bindService(mContext,
                    BackgroundPlayerService.createIntent(mContext),
                    Context.BIND_AUTO_CREATE)) {
                mBackgroundPlayerServiceConnection = null;
            }
        }
    }

    @Override
    public void onStop() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onStop").toString());
        }
        super.onStop();
        mStateManager.onStop();

        if (mDidStartPlay) {
            Handler handler = mHandler;
            if (!handler.hasMessages(MSG_ID_UPDATE_COMMENT_INNER)) {
                handler.sendEmptyMessage(MSG_ID_UPDATE_COMMENT_INNER);
            }
        }
    }

    @Override
    public void onDestroyView() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onDestroyView").toString());
        }
        super.onDestroyView();
    }

    @Override
    public void onDestroy() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("#onDestroy").toString());
        }
        mStateManager.onDestroy();

        if (mBackgroundPlayerServiceConnection != null) {
            mBackgroundPlayerServiceConnection.unbindService(mContext);
            mBackgroundPlayerServiceConnection = null;
            mBackgroundPlayerMessenger = null;
        }
        BackgroundPlayerService.stopService(mContext);

        mDestroyTask.onDestroy();

        super.onDestroy();
    }

    @Override
    public void onDestroyImplPre() {
        mHandler.release();
    }

    @Override
    public void onDestroyImpl() {
        releaseLoader();
        synchronized (mSyncDestroyTaskOnDestroyImpl) {
            mEndOnDestroyImpl = true;
            mSyncDestroyTaskOnDestroyImpl.notifyAll();
        }
    }

    @Override
    public void onDestroyImplPost() {
        if (mNewPlayerFragmentCreator != null) {
            mNewPlayerFragmentCreator.stop();
        }
        mVideoLoader = VideoLoaderInterface.NullObject.getInstance();
        mMessageLoader = MessageLoaderInterface.NullObject.getInstance();
        mMessageSender = MessageSenderInterface.NullObject.getInstance();
        mThumbInfo = ThumbInfoInterface.NullObject.getInstance();
        mConfigureNgClient = ConfigureNgClientInterface.NullObject.getInstance();
        mMessageChatController.clearMessageData();
        mMessageChatController.resetMessageDataChats(null, null);
        mMessageChatController.resetNicoScript();
    }

    public void finishFragment() {
        mDestroyTask.finishActivity();
    }

    @Override
    public void finishReally() {
        // 空実装
    }

    public void waitOnDestroyImpl() {
        if (mDestroyTask.wasTaskEnded()) {
            return;
        }
        synchronized (mSyncDestroyTaskOnDestroyImpl) {
            while (!mEndOnDestroyImpl) {
                try {
                    mSyncDestroyTaskOnDestroyImpl.wait();
                } catch (InterruptedException e) {
                }
            }
        }
    }

    @Override
    public boolean wasDestroyed() {
        return mStateManager.wasDestroyed();
    }

    public boolean wasCreated() {
        return mStateManager.wasCreated();
    }

    public boolean didStartPlay() {
        return mDidStartPlay;
    }

    private void releaseLoader() {
        if (mVideoLoader.isNull() && mMessageLoader.isNull()
                && mMessageSender.isNull() && mThumbInfo.isNull()
                && mConfigureNgClient.isNull()) {
            // すべて解放済みなら処理無し
            return;
        }

        // XXX 本当は***Loader類のabort対応が必要
        ExecutorService executorService = Executors.newCachedThreadPool();
        CountDownLatch latch = new CountDownLatch(5);
        mVideoLoader.finishAsync(executorService, latch);
        mMessageLoader.finishAsync(executorService, latch);
        mMessageSender.finishAsync(executorService, latch);
        mThumbInfo.finishAsync(executorService, latch);
        mConfigureNgClient.finishAsync(executorService, latch);
        while (true) {
            try {
                latch.await();
                break;
            } catch (InterruptedException e) {
                Log.d(LOG_TAG, e.toString(), e);
            }
        }
        executorService.shutdown();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (mLastOrientation != newConfig.orientation) {
            onOrientationChanged(newConfig.orientation);
            mLastOrientation = newConfig.orientation;
        }
    }

    abstract protected void onOrientationChanged(int orientation);

    public abstract void onParentScreenLayout(int width, int height);

    protected void onVideoViewSizeChanged(int width, int height) {
        PlayerActivity activity = getPlayerActivity();
        if (activity == null) {
            return;
        }
        activity.onVideoViewSizeChanged(width, height);
    }

    protected static VideoLoaderInterface createVideoLoader(Bundle args, Context context) {
        VideoLoaderInterface videoLoader = VideoLoaderInterface.NullObject.getInstance();
        String url = args.getString(INTENT_NAME_VIDEO_URL);
        String cookie = args.getString(INTENT_NAME_COOKIE);
        WatchVideo video = args.getParcelable(INTENT_NAME_WATCH_VIDEO);
        String cookieUserSession = NicoroConfig.getCookieUserSession(context);

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(" url=").append(url).append(" cookie=")
                    .append(cookie).append(" video=").append(video)
                    .append(" cookieUserSession=").append(cookieUserSession)
                    .toString());
        }
        if (url != null && cookie != null) {
            videoLoader = new VideoLoader(url, cookie, video.v(),
                    context, cookieUserSession,
//                  android.os.Process.THREAD_PRIORITY_BACKGROUND);
                    android.os.Process.THREAD_PRIORITY_BACKGROUND
                    + android.os.Process.THREAD_PRIORITY_MORE_FAVORABLE * 2);
        }
        return videoLoader;
    }

    protected static MessageLoaderInterface createMessageLoader(Bundle args, Context context,
            String userSession) {
        MessageLoaderInterface messageLoader = MessageLoaderInterface.NullObject.getInstance();
        String messageUrl = args.getString(
                INTENT_NAME_MESSAGE_URL);
        String threadId = args.getString(
                INTENT_NAME_THREAD_ID);
        String userId = args.getString(
                INTENT_NAME_USER_ID);
        String threadKey = args.getString(
                INTENT_NAME_THREAD_KEY);
        String force184 = args.getString(
                INTENT_NAME_FORCE_184);
        String nicosId = args.getString(INTENT_NAME_NICOS_ID);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(" messageUrl=").append(messageUrl)
                    .append(" threadId=").append(threadId)
                    .append(" userId=").append(userId)
                    .append(" threadKey=").append(threadKey)
                    .append(" force184=").append(force184)
                    .append(" nicosId=").append(nicosId).toString());
        }
        if (messageUrl != null && threadId != null) {
            messageLoader = new MessageLoader(messageUrl, threadId, userSession,
                    userId, threadKey, force184,
                    context, nicosId);
        }
        return messageLoader;
    }

    protected static MessageSenderInterface createMessageSender(Bundle args, Context context,
            String userSession, int premium) {
        MessageSenderInterface messageSender = MessageSenderInterface.NullObject.getInstance();

        String messageUrl = args.getString(
                INTENT_NAME_MESSAGE_URL);
        String threadId = args.getString(
                INTENT_NAME_THREAD_ID);
        String nicosId = args.getString(INTENT_NAME_NICOS_ID);
        String userId = args.getString(
                INTENT_NAME_USER_ID);

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(" messageUrl=").append(messageUrl)
                    .append(" threadId=").append(threadId)
                    .append(" userId=").append(userId)
                    .append(" nicosId=").append(nicosId).toString());
        }

        if (messageUrl != null && threadId != null) {
            messageSender = new MessageSender(messageUrl, threadId, nicosId,
                    userSession, userId, premium, context);
        }
        return messageSender;
    }

    protected void loadThumbInfo(Bundle args) {
        WatchVideo video = args.getParcelable(INTENT_NAME_WATCH_VIDEO);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(" video=").append(video).toString());
        }
        loadThumbInfo(video.id());
    }
    protected void loadThumbInfo(String videoNumber) {
        if (videoNumber == null) {
            return;
        }
        Handler handler = mHandler;
        if (!mThumbInfo.isNull() && videoNumber.equals(mThumbInfo.getVideoNumber())) {
            // ThumbInfo is set already
            handler.obtainMessage(MSG_ID_THUMBINFO_FINISHED,
                    mThumbInfo.getLength()).sendToTarget();
            return;
        }
        Activity activity = getActivity();
        if (activity == null) {
            return;
        }
        ThumbInfoCacher cacher = NicoroApplication.getInstance(activity
                ).getThumbInfoCacher();
        ThumbInfo thumbInfo = cacher.getThumbInfo(videoNumber);
        if (thumbInfo == null) {
            cacher.loadThumbInfo(videoNumber,
                    new CallbackMessage<ThumbInfo, String>(
                            handler,
                            MSG_ID_THUMBINFO_FINISHED_NEW,
                            MSG_ID_THUMBINFO_OCCURRED_ERROR_NEW));
        } else {
            mThumbInfo = thumbInfo;
            handler.obtainMessage(MSG_ID_THUMBINFO_FINISHED,
                    thumbInfo.getLength()).sendToTarget();
        }
    }

    private static ConfigureNgClientInterface createConfigureNgClient(Context context,
            String userSession) {
        ConfigureNgClientInterface configureNgClient =
            new ConfigureNgClient(userSession, context);
        return configureNgClient;
    }

    protected void seekBySecondCommon(int second) {
        // コメントデータ初期化
        mMessageChatController.resetNicoScript();
        mMessageChatController.resetMessageDataChats(
                mMessageLoader.getChats(),
                mMessageLoader.getChatsFork());
    }

    protected StringBuilder appendCurrentPlayTimeCommon(StringBuilder builder,
            final long posNum, final int posDen) {
        if (posDen != 0) {
            int minutes = (int) (posNum / ((long) posDen * 60));
            int seconds = (int) (posNum / posDen % 60);
            return Util.appendPlayTime(builder, minutes, seconds);
        } else {
            return builder.append(INFO_TIME_DEFAULT);
        }
    }

    protected void setButtonPauseImage() {
        // TODO 直接PlayerActivity使って良いかどうか
        PlayerActivity activity = getPlayerActivity();
        if (activity != null) {
            activity.setButtonPauseImage();
        }
    }

    protected PlayerActivity getPlayerActivity() {
        Activity activity = getActivity();
        if (activity == null) {
            Log.w(LOG_TAG, "AbstractPlayerFragment#getPlayerActivity() is called when activity is null");
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, "stack trace", new Throwable());
            }
        }
        return (PlayerActivity) activity;
    }

    public ThumbInfoInterface getThumbInfo() {
        return mThumbInfo;
    }

    protected boolean getMessageDisable() {
        PlayerActivity activity = getPlayerActivity();
        if (activity == null) {
            return false;
        } else {
            return activity.getMessageDisable();
        }
    }

    public void moveThumbInfoFrom(AbstractPlayerFragment src) {
        mThumbInfo = src.mThumbInfo;
        src.mThumbInfo = ThumbInfoInterface.NullObject.getInstance();
    }

    public void moveVideoLoaderFrom(AbstractPlayerFragment src) {
        mVideoLoader = src.mVideoLoader;
        src.mVideoLoader = VideoLoaderInterface.NullObject.getInstance();
    }

    public void moveMessagesFrom(AbstractPlayerFragment src) {
        mMessageLoader = src.mMessageLoader;
        src.mMessageLoader = MessageLoaderInterface.NullObject.getInstance();
    }

    public void moveConfigureNgClientFrom(AbstractPlayerFragment src) {
        mConfigureNgClient = src.mConfigureNgClient;
        src.mConfigureNgClient = ConfigureNgClient.NullObject.getInstance();
    }

    protected void initializeVideoLoader() {
        Bundle args = getArguments();
        if (!mVideoLoader.isNull()) {
            if (TextUtils.equals(args.getString(INTENT_NAME_VIDEO_URL),
                    mVideoLoader.getUrl())) {
                // VideoLoader is set already
                VideoLoaderInterface.EventListener listener = createVideoLoaderEventListener();
                mVideoLoader.setEventListener(listener);
                if (mVideoLoader.hasCache()) {
                    listener.onCached(mVideoLoader);
                }
                if (mVideoLoader.isCacheCompleted()) {
                    listener.onFinished(mVideoLoader);
                }
                return;
            }
        }

        mVideoLoader = createVideoLoader(args, mContext);
        mVideoLoader.setEventListener(createVideoLoaderEventListener());
        // 最初にダウンロード要求
        mVideoLoader.startLoad();
    }

    protected void replacePlayerFragment(AbstractPlayerFragment playerFragment,
            boolean showProgress) {
        PlayerActivity playerActivity = getPlayerActivity();
        if (playerActivity == null) {
            return;
        }
        playerActivity.replacePlayerFragment(playerFragment, showProgress);
    }

    protected static AbstractPlayerFragment createPlayerFragment(int playerType) {
        switch (playerType) {
            case PLAYER_TYPE_FFMPEG:
                return new FFmpegPlayerFragment();
            case PLAYER_TYPE_MEDIAPLAYER:
                return new MediaPlayerFragment();
            case PLAYER_TYPE_SWF:
                return new SwfPlayerFragment();
            default:
                assert false : "invalid argument: " + playerType;
                return null;
        }
    }

    public void postSeekBySecond(int seekSecond, int delayTime) {
        Handler handler = mHandler;
        Message msg = handler.obtainMessage(MSG_ID_SEEK_BY_SECOND, seekSecond, 0);
        handler.sendMessageDelayed(msg, delayTime);
    }

    public void postJumpVideo(String videoNumber, int startTimeSecond,
            int returnTimeSecond, String returnMessage, int returnStartTimeSecond,
            int delayTime) {
        Handler handler = mHandler;

        Bundle args = getArguments();
        WatchVideo video = args.getParcelable(INTENT_NAME_WATCH_VIDEO);
        String returnVideoNumber = video.v();

        Bundle newArgs = new Bundle();
        newArgs.putString(INTENT_NAME_VIDEO_NUMBER,
                videoNumber);
        newArgs.putInt(INTENT_NAME_START_TIME_SECOND,
                startTimeSecond);
        newArgs.putString(INTENT_NAME_RETURN_VIDEO_NUMBER,
                returnVideoNumber);
        newArgs.putInt(INTENT_NAME_RETURN_TIME_SECOND,
                returnTimeSecond);
        newArgs.putString(INTENT_NAME_RETURN_MESSAGE,
                returnMessage);
        newArgs.putInt(INTENT_NAME_RETURN_START_TIME_SECOND,
                returnStartTimeSecond);

        Message msg = handler.obtainMessage(MSG_ID_JUMP_VIDEO);
        msg.setData(newArgs);
        handler.sendMessageDelayed(msg, delayTime);
    }

    public void postJumpReturnVideo(String videoNumber, int startTimeSecond,
            int delayTime) {
        Handler handler = mHandler;

        Bundle newArgs = new Bundle();
        newArgs.putString(INTENT_NAME_VIDEO_NUMBER,
                videoNumber);
        newArgs.putInt(INTENT_NAME_START_TIME_SECOND,
                startTimeSecond);

        Message msg = handler.obtainMessage(MSG_ID_JUMP_VIDEO);
        msg.setData(newArgs);
        handler.sendMessageDelayed(msg, delayTime);
    }

    protected void updateMessageInner(int vpos) {
        // TODO プレイヤーによってはコメント描画は別スレッドで動作（このメソッドはメインスレッドから呼び出しなのに）
        if (mMessageChatController.isMessageDataOk()) {
            mMessageChatController.drawMessage(null,
                    vpos, 0, 0,
                    false);
        }
    }

    public void sendComment(String mail, String body) {
        int vpos = getVpos();
        int commentType = mMessageChatController.collateCommentWithNicoScript(body, vpos);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("Comment=").append(body)
                    .append(" CommentType=").append(commentType)
                    .toString());
        }
        if (commentType == MessageChatController.COMMENT_TYPE_LOCAL) {
            // localは表示のみ反映
            // TODO no適当
            int no = 0;
            String userId = getArguments().getString(
                    INTENT_NAME_USER_ID);
            mMessageChatController.addMessage(no, mail, body, vpos, userId);
        } else {
            boolean toNicos = (commentType == MessageChatController.COMMENT_TYPE_NICOS);

            if (!mMessageSender.isStarted()) {
                mMessageSender.startLoad();
            }

            if (!mMessageSender.send(mail, body, vpos, toNicos)) {
                Handler handler = mHandler;
                handler.sendEmptyMessage(MSG_ID_MESSAGE_SEND_OCCURRED_ERROR);
            }
        }
    }

    protected void loadConfigureNgClient(String userSession) {
        if (!mConfigureNgClient.isNull() && mConfigureNgClient.getNgClients() != null) {
            // ConfigureNgClient is set already
            mHandler.sendEmptyMessage(MSG_ID_GET_CONFIGURE_NG_CLIENT_FINISHED);
        } else {
            mConfigureNgClient = createConfigureNgClient(mContext, userSession);
            mConfigureNgClient.setEventListener(new ConfigureNgClientInterface.EventListener() {
                @Override
                public void onOccurredError(ConfigureNgClientInterface client, String errorMessage) {
                    Log.e(LOG_TAG, errorMessage);
                    Handler handler = mHandler;
                    handler.sendEmptyMessage(MSG_ID_GET_CONFIGURE_NG_CLIENT_OCCURRED_ERROR);
                }

                @Override
                public void onFinished(ConfigureNgClientInterface client) {
                    Handler handler = mHandler;
                    handler.sendEmptyMessage(MSG_ID_GET_CONFIGURE_NG_CLIENT_FINISHED);
                }
            });
            mConfigureNgClient.startLoad();
        }
    }

    /**
     * 動画／生放送のタイトルを取得
     * @return タイトル。未取得の場合はnullの場合あり
     */
    public String getTitle() {
        return mThumbInfo.getParsedTitle();
    }

    /**
     * NG共有レベルの設定＆設定値の保存
     * @param ngShareLevel
     */
    public void setNgShareLevel(int ngShareLevel) {
        mSharedPreferences.edit().putString(mResStrPrefKeyNgShareLevel,
                Integer.toString(ngShareLevel)).commit();
        mMessageChatController.setNgShareLevel(ngShareLevel);
    }

    public int getNgShareLevel() {
        return mMessageChatController.getNgShareLevel();
    }

    public void setPlayRepeat(boolean enable) {
        mPlayRepeat = enable;
    }

    public boolean isPlayRepeatEnabled() {
        return mPlayRepeat;
    }

    public static void setSystemUiVisibility(View view, SharedPreferences sp,
            Resources res) {
        int visibility;
        if (sp.getBoolean(res.getString(
                R.string.pref_key_low_profile_player),
                res.getBoolean(R.bool.pref_default_low_profile_player))) {
            visibility = View.SYSTEM_UI_FLAG_LOW_PROFILE;
        } else {
            visibility = View.SYSTEM_UI_FLAG_VISIBLE;
        }

        APILevelWrapper api = APILevelWrapper.createInstance();
        api.setSystemUiVisibility(view, visibility);
    }

    public static SharedPreferences getSharedPreferencesPlayState(Context context) {
        return context.getSharedPreferences(
                SHARED_PREFERENCES_PLAY_STATE,
                Context.MODE_PRIVATE);
    }

    public static SharedPreferences.Editor putCurrentPlayTime(
            SharedPreferences.Editor editor, Rational time) {
        return editor.putLong(KEY_SAVE_CURRENT_PLAY_TIME_NUM, time.num)
            .putInt(KEY_SAVE_CURRENT_PLAY_TIME_DEN, time.den);
    }

    public static SharedPreferences.Editor putIsPausePlay(
            SharedPreferences.Editor editor, boolean isPausePlay) {
        return editor.putBoolean(KEY_SAVE_IS_PAUSE_PLAY, isPausePlay);
    }
}
