/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tv.testinput;

import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.media.PlaybackParams;
import android.media.tv.TvContract;
import android.media.tv.TvContract.Programs;
import android.media.tv.TvContract.RecordedPrograms;
import android.media.tv.TvInputManager;
import android.media.tv.TvInputService;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Surface;
import com.android.tv.input.TunerHelper;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.testing.testinput.ChannelState;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/** Simple TV input service which provides test channels. */
public class TestTvInputService extends TvInputService {
    private static final String TAG = "TestTvInputService";
    private static final int REFRESH_DELAY_MS = 1000 / 5;
    private static final boolean DEBUG = false;

    // Consider the command delivering time from TV app.
    private static final long MAX_COMMAND_DELAY = TimeUnit.SECONDS.toMillis(3);

    private final TestInputControl mBackend = TestInputControl.getInstance();

    private TunerHelper mTunerHelper;

    public static String buildInputId(Context context) {
        return TvContract.buildInputId(new ComponentName(context, TestTvInputService.class));
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mBackend.init(this, buildInputId(this));
        mTunerHelper = new TunerHelper(getResources().getInteger(R.integer.tuner_count));
    }

    @Override
    public Session onCreateSession(String inputId) {
        Log.v(TAG, "Creating session for " + inputId);
        // onCreateSession always succeeds because this session can be used to play the recorded
        // program.
        return new SimpleSessionImpl(this);
    }

    @TargetApi(Build.VERSION_CODES.N)
    @Override
    public RecordingSession onCreateRecordingSession(String inputId) {
        Log.v(TAG, "Creating recording session for " + inputId);
        if (!mTunerHelper.tunerAvailableForRecording()) {
            return null;
        }
        return new SimpleRecordingSessionImpl(this, inputId);
    }

    /** Simple session implementation that just display some text. */
    private class SimpleSessionImpl extends Session {
        private static final int MSG_SEEK = 1000;
        private static final int SEEK_DELAY_MS = 300;

        private final Paint mTextPaint = new Paint();
        private final DrawRunnable mDrawRunnable = new DrawRunnable();
        private Surface mSurface = null;
        private Uri mChannelUri = null;
        private ChannelInfo mChannel = null;
        private ChannelState mCurrentState = null;
        private String mCurrentVideoTrackId = null;
        private String mCurrentAudioTrackId = null;

        private long mRecordStartTimeMs;
        private long mPausedTimeMs;
        // The time in milliseconds when the current position is lastly updated.
        private long mLastCurrentPositionUpdateTimeMs;
        // The current playback position.
        private long mCurrentPositionMs;
        // The current playback speed rate.
        private float mSpeed;

        private final Handler mHandler =
                new Handler(Looper.myLooper()) {
                    @Override
                    public void handleMessage(Message msg) {
                        if (msg.what == MSG_SEEK) {
                            // Actually, this input doesn't play any videos, it just shows the
                            // image.
                            // So we should simulate the playback here by changing the current
                            // playback
                            // position periodically in order to test the time shift.
                            // If the playback is paused, the current playback position doesn't need
                            // to be
                            // changed.
                            if (mPausedTimeMs == 0) {
                                long currentTimeMs = System.currentTimeMillis();
                                mCurrentPositionMs +=
                                        (long)
                                                ((currentTimeMs - mLastCurrentPositionUpdateTimeMs)
                                                        * mSpeed);
                                mCurrentPositionMs =
                                        Math.max(
                                                mRecordStartTimeMs,
                                                Math.min(mCurrentPositionMs, currentTimeMs));
                                mLastCurrentPositionUpdateTimeMs = currentTimeMs;
                            }
                            sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
                        }
                        super.handleMessage(msg);
                    }
                };

        SimpleSessionImpl(Context context) {
            super(context);
            mTextPaint.setColor(Color.BLACK);
            mTextPaint.setTextSize(150);
            mHandler.post(mDrawRunnable);
            if (DEBUG) {
                Log.v(TAG, "Created session " + this);
            }
        }

        private void setAudioTrack(String selectedAudioTrackId) {
            Log.i(TAG, "Set audio track to " + selectedAudioTrackId);
            mCurrentAudioTrackId = selectedAudioTrackId;
            notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, mCurrentAudioTrackId);
        }

        private void setVideoTrack(String selectedVideoTrackId) {
            Log.i(TAG, "Set video track to " + selectedVideoTrackId);
            mCurrentVideoTrackId = selectedVideoTrackId;
            notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, mCurrentVideoTrackId);
        }

        @Override
        public void onRelease() {
            if (DEBUG) {
                Log.v(TAG, "Releasing session " + this);
            }
            mTunerHelper.stopTune(mChannelUri);
            mDrawRunnable.cancel();
            mHandler.removeCallbacks(mDrawRunnable);
            mSurface = null;
            mChannelUri = null;
            mChannel = null;
            mCurrentState = null;
        }

        @Override
        public boolean onSetSurface(Surface surface) {
            synchronized (mDrawRunnable) {
                mSurface = surface;
            }
            if (surface != null) {
                if (DEBUG) {
                    Log.v(TAG, "Surface set");
                }
            } else {
                if (DEBUG) {
                    Log.v(TAG, "Surface unset");
                }
            }

            return true;
        }

        @Override
        public void onSurfaceChanged(int format, int width, int height) {
            super.onSurfaceChanged(format, width, height);
            Log.d(TAG, "format=" + format + " width=" + width + " height=" + height);
        }

        @Override
        public void onSetStreamVolume(float volume) {
            // No-op
        }

        @Override
        public boolean onTune(Uri channelUri) {
            Log.i(TAG, "Tune to " + channelUri);
            mTunerHelper.stopTune(mChannelUri);
            mChannelUri = channelUri;
            ChannelInfo info = mBackend.getChannelInfo(channelUri);
            synchronized (mDrawRunnable) {
                if (info == null
                        || mChannel == null
                        || mChannel.originalNetworkId != info.originalNetworkId) {
                    mCurrentState = null;
                }
                mChannel = info;
                mCurrentVideoTrackId = null;
                mCurrentAudioTrackId = null;
            }
            if (mChannel == null) {
                Log.i(TAG, "Channel not found for " + channelUri);
                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
            } else if (!mTunerHelper.tune(channelUri, false)) {
                Log.i(TAG, "No available tuner for " + channelUri);
                notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
            } else {
                Log.i(TAG, "Tuning to " + mChannel);
            }
            notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_AVAILABLE);
            mRecordStartTimeMs =
                    mCurrentPositionMs =
                            mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
            mPausedTimeMs = 0;
            mHandler.sendEmptyMessageDelayed(MSG_SEEK, SEEK_DELAY_MS);
            mSpeed = 1;
            return true;
        }

        @Override
        public void onSetCaptionEnabled(boolean enabled) {
            // No-op
        }

        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
            Log.d(TAG, "onKeyDown (keyCode=" + keyCode + ", event=" + event + ")");
            return true;
        }

        @Override
        public boolean onKeyUp(int keyCode, KeyEvent event) {
            Log.d(TAG, "onKeyUp (keyCode=" + keyCode + ", event=" + event + ")");
            return true;
        }

        @Override
        public long onTimeShiftGetCurrentPosition() {
            Log.d(TAG, "currentPositionMs=" + mCurrentPositionMs);
            return mCurrentPositionMs;
        }

        @Override
        public long onTimeShiftGetStartPosition() {
            return mRecordStartTimeMs;
        }

        @Override
        public void onTimeShiftPause() {
            mCurrentPositionMs =
                    mPausedTimeMs = mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
        }

        @Override
        public void onTimeShiftResume() {
            mSpeed = 1;
            mPausedTimeMs = 0;
            mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
        }

        @Override
        public void onTimeShiftSeekTo(long timeMs) {
            mLastCurrentPositionUpdateTimeMs = System.currentTimeMillis();
            mCurrentPositionMs =
                    Math.max(
                            mRecordStartTimeMs, Math.min(timeMs, mLastCurrentPositionUpdateTimeMs));
        }

        @Override
        public void onTimeShiftSetPlaybackParams(PlaybackParams params) {
            mSpeed = params.getSpeed();
        }

        private final class DrawRunnable implements Runnable {
            private volatile boolean mIsCanceled = false;

            @Override
            public void run() {
                if (mIsCanceled) {
                    return;
                }
                if (DEBUG) {
                    Log.v(TAG, "Draw task running");
                }
                boolean updatedState = false;
                ChannelState oldState;
                ChannelState newState = null;
                Surface currentSurface;
                ChannelInfo currentChannel;

                synchronized (this) {
                    oldState = mCurrentState;
                    currentSurface = mSurface;
                    currentChannel = mChannel;
                    if (currentChannel != null) {
                        newState = mBackend.getChannelState(currentChannel.originalNetworkId);
                        if (oldState == null || newState.getVersion() > oldState.getVersion()) {
                            mCurrentState = newState;
                            updatedState = true;
                        }
                    } else {
                        mCurrentState = null;
                    }

                    if (currentSurface != null) {
                        String now = new Date(mCurrentPositionMs).toString();
                        String name = currentChannel == null ? "Null" : currentChannel.name;
                        try {
                            Canvas c = currentSurface.lockCanvas(null);
                            c.drawColor(0xFF888888);
                            c.drawText(name, 100f, 200f, mTextPaint);
                            c.drawText(now, 100f, 400f, mTextPaint);
                            // Assuming c.drawXXX will never fail.
                            currentSurface.unlockCanvasAndPost(c);
                        } catch (IllegalArgumentException e) {
                            // The surface might have been abandoned. Ignore the exception.
                        }
                        if (DEBUG) {
                            Log.v(TAG, "Post to canvas");
                        }
                    } else {
                        if (DEBUG) {
                            Log.v(TAG, "No surface");
                        }
                    }
                }
                if (updatedState) {
                    update(oldState, newState, currentChannel);
                }

                if (!mIsCanceled) {
                    mHandler.postDelayed(this, REFRESH_DELAY_MS);
                }
            }

            private void update(
                    ChannelState oldState, ChannelState newState, ChannelInfo currentChannel) {
                Log.i(TAG, "Updating channel " + currentChannel.number + " state to " + newState);
                notifyTracksChanged(newState.getTrackInfoList());
                if (oldState == null || oldState.getTuneStatus() != newState.getTuneStatus()) {
                    if (newState.getTuneStatus() == ChannelState.TUNE_STATUS_VIDEO_AVAILABLE) {
                        notifyVideoAvailable();
                        // TODO handle parental controls.
                        notifyContentAllowed();
                        setAudioTrack(newState.getSelectedAudioTrackId());
                        setVideoTrack(newState.getSelectedVideoTrackId());
                    } else {
                        notifyVideoUnavailable(newState.getTuneStatus());
                    }
                }
            }

            public void cancel() {
                mIsCanceled = true;
            }
        }
    }

    private class SimpleRecordingSessionImpl extends RecordingSession {
        private final String[] PROGRAM_PROJECTION = {
            Programs.COLUMN_TITLE,
            Programs.COLUMN_EPISODE_TITLE,
            Programs.COLUMN_SHORT_DESCRIPTION,
            Programs.COLUMN_POSTER_ART_URI,
            Programs.COLUMN_THUMBNAIL_URI,
            Programs.COLUMN_CANONICAL_GENRE,
            Programs.COLUMN_CONTENT_RATING,
            Programs.COLUMN_START_TIME_UTC_MILLIS,
            Programs.COLUMN_END_TIME_UTC_MILLIS,
            Programs.COLUMN_VIDEO_WIDTH,
            Programs.COLUMN_VIDEO_HEIGHT,
            Programs.COLUMN_SEASON_DISPLAY_NUMBER,
            Programs.COLUMN_SEASON_TITLE,
            Programs.COLUMN_EPISODE_DISPLAY_NUMBER,
        };

        private final String mInputId;
        private long mStartTime;
        private long mEndTime;
        private Uri mChannelUri;
        private Uri mProgramHintUri;

        public SimpleRecordingSessionImpl(Context context, String inputId) {
            super(context);
            mInputId = inputId;
        }

        @Override
        public void onTune(Uri uri) {
            Log.i(TAG, "SimpleReccordingSesesionImpl: onTune()");
            mTunerHelper.stopRecording(mChannelUri);
            mChannelUri = uri;
            ChannelInfo channel = mBackend.getChannelInfo(uri);
            if (channel == null) {
                notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN);
            } else if (!mTunerHelper.tune(uri, true)) {
                notifyError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
            } else {
                notifyTuned(uri);
            }
        }

        @Override
        public void onStartRecording(Uri programHintUri) {
            Log.i(TAG, "SimpleReccordingSesesionImpl: onStartRecording()");
            mStartTime = System.currentTimeMillis();
            mProgramHintUri = programHintUri;
        }

        @Override
        public void onStopRecording() {
            Log.i(TAG, "SimpleReccordingSesesionImpl: onStopRecording()");
            mEndTime = System.currentTimeMillis();
            final long startTime = mStartTime;
            final long endTime = mEndTime;
            final Uri programHintUri = mProgramHintUri;
            final Uri channelUri = mChannelUri;
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... arg0) {
                    long time = System.currentTimeMillis();
                    if (programHintUri != null) {
                        // Retrieves program info from mProgramHintUri
                        try (Cursor c =
                                getContentResolver()
                                        .query(
                                                programHintUri,
                                                PROGRAM_PROJECTION,
                                                null,
                                                null,
                                                null)) {
                            if (c != null && c.getCount() > 0) {
                                storeRecordedProgram(c, startTime, endTime);
                                return null;
                            }
                        } catch (Exception e) {
                            Log.w(TAG, "Error querying " + this, e);
                        }
                    }
                    // Retrieves the current program
                    try (Cursor c =
                            getContentResolver()
                                    .query(
                                            TvContract.buildProgramsUriForChannel(
                                                    channelUri,
                                                    startTime,
                                                    endTime - startTime < MAX_COMMAND_DELAY
                                                            ? startTime
                                                            : endTime - MAX_COMMAND_DELAY),
                                            PROGRAM_PROJECTION,
                                            null,
                                            null,
                                            null)) {
                        if (c != null && c.getCount() == 1) {
                            storeRecordedProgram(c, startTime, endTime);
                            return null;
                        }
                    } catch (Exception e) {
                        Log.w(TAG, "Error querying " + this, e);
                    }
                    storeRecordedProgram(null, startTime, endTime);
                    return null;
                }

                private void storeRecordedProgram(Cursor c, long startTime, long endTime) {
                    ContentValues values = new ContentValues();
                    values.put(RecordedPrograms.COLUMN_INPUT_ID, mInputId);
                    values.put(RecordedPrograms.COLUMN_CHANNEL_ID, ContentUris.parseId(channelUri));
                    values.put(
                            RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS, endTime - startTime);
                    if (c != null) {
                        int index = 0;
                        c.moveToNext();
                        values.put(Programs.COLUMN_TITLE, c.getString(index++));
                        values.put(Programs.COLUMN_EPISODE_TITLE, c.getString(index++));
                        values.put(Programs.COLUMN_SHORT_DESCRIPTION, c.getString(index++));
                        values.put(Programs.COLUMN_POSTER_ART_URI, c.getString(index++));
                        values.put(Programs.COLUMN_THUMBNAIL_URI, c.getString(index++));
                        values.put(Programs.COLUMN_CANONICAL_GENRE, c.getString(index++));
                        values.put(Programs.COLUMN_CONTENT_RATING, c.getString(index++));
                        values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, c.getLong(index++));
                        values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, c.getLong(index++));
                        values.put(Programs.COLUMN_VIDEO_WIDTH, c.getLong(index++));
                        values.put(Programs.COLUMN_VIDEO_HEIGHT, c.getLong(index++));
                        values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, c.getString(index++));
                        values.put(Programs.COLUMN_SEASON_TITLE, c.getString(index++));
                        values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, c.getString(index++));
                    } else {
                        values.put(RecordedPrograms.COLUMN_TITLE, "No program info");
                        values.put(RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS, startTime);
                        values.put(RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, endTime);
                    }
                    Uri uri =
                            getContentResolver()
                                    .insert(TvContract.RecordedPrograms.CONTENT_URI, values);
                    notifyRecordingStopped(uri);
                }
            }.execute();
        }

        @Override
        public void onRelease() {
            Log.i(TAG, "SimpleReccordingSesesionImpl: onRelease()");
            mTunerHelper.stopRecording(mChannelUri);
            mChannelUri = null;
        }
    }
}
