/*
 * Copyright (C) 2017 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.data;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
import com.android.tv.common.util.SharedPreferencesUtils;
import com.android.tv.data.api.Channel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;

/**
 * A class to manage watched history.
 *
 * <p>When there is no access to watched table of TvProvider, this class is used to build up watched
 * history and to compute recent channels.
 *
 * <p>Note that this class is not thread safe. Please use this on one thread.
 */
public class WatchedHistoryManager {
    private static final String TAG = "WatchedHistoryManager";
    private static final boolean DEBUG = false;

    private static final int MAX_HISTORY_SIZE = 10000;
    private static final String PREF_KEY_LAST_INDEX = "last_index";
    private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10);

    private final List<WatchedRecord> mWatchedHistory = new ArrayList<>();
    private final List<WatchedRecord> mPendingRecords = new ArrayList<>();
    private long mLastIndex;
    private boolean mStarted;
    private boolean mLoaded;
    private SharedPreferences mSharedPreferences;
    private final OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener =
            new OnSharedPreferenceChangeListener() {
                @Override
                @MainThread
                public void onSharedPreferenceChanged(
                        SharedPreferences sharedPreferences, String key) {
                    if (key.equals(PREF_KEY_LAST_INDEX)) {
                        final long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
                        if (lastIndex <= mLastIndex) {
                            return;
                        }
                        // onSharedPreferenceChanged is always called in a main thread.
                        // onNewRecordAdded will be called in the same thread as the thread
                        // which created this instance.
                        mHandler.post(
                                () -> {
                                    for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
                                        WatchedRecord record =
                                                decode(
                                                        mSharedPreferences.getString(
                                                                getSharedPreferencesKey(i), null));
                                        if (record != null) {
                                            mWatchedHistory.add(record);
                                            if (mListener != null) {
                                                mListener.onNewRecordAdded(record);
                                            }
                                        }
                                    }
                                    mLastIndex = lastIndex;
                                });
                    }
                }
            };

    private final Context mContext;
    private Listener mListener;
    private final int mMaxHistorySize;
    private final Handler mHandler;
    private final Executor mExecutor;

    public WatchedHistoryManager(Context context) {
        this(context, MAX_HISTORY_SIZE, AsyncTask.THREAD_POOL_EXECUTOR);
    }

    @VisibleForTesting
    WatchedHistoryManager(Context context, int maxHistorySize, Executor executor) {
        mContext = context.getApplicationContext();
        mMaxHistorySize = maxHistorySize;
        mHandler = new Handler();
        mExecutor = executor;
    }

    /** Starts the manager. It loads history data from {@link SharedPreferences}. */
    public void start() {
        if (mStarted) {
            return;
        }
        mStarted = true;
        if (Looper.myLooper() == Looper.getMainLooper()) {
            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    loadWatchedHistory();
                    return null;
                }

                @Override
                protected void onPostExecute(Void params) {
                    onLoadFinished();
                }
            }.executeOnExecutor(mExecutor);
        } else {
            loadWatchedHistory();
            onLoadFinished();
        }
    }

    @WorkerThread
    private void loadWatchedHistory() {
        mSharedPreferences =
                mContext.getSharedPreferences(
                        SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
        mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
        if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
            for (int i = 0; i <= mLastIndex; ++i) {
                WatchedRecord record =
                        decode(mSharedPreferences.getString(getSharedPreferencesKey(i), null));
                if (record != null) {
                    mWatchedHistory.add(record);
                }
            }
        } else if (mLastIndex >= mMaxHistorySize) {
            for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
                WatchedRecord record =
                        decode(mSharedPreferences.getString(getSharedPreferencesKey(i), null));
                if (record != null) {
                    mWatchedHistory.add(record);
                }
            }
        }
    }

    private void onLoadFinished() {
        mLoaded = true;
        if (DEBUG) {
            Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
        }
        if (!mPendingRecords.isEmpty()) {
            Editor editor = mSharedPreferences.edit();
            for (WatchedRecord record : mPendingRecords) {
                mWatchedHistory.add(record);
                ++mLastIndex;
                editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
            }
            editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
            mPendingRecords.clear();
        }
        if (mListener != null) {
            mListener.onLoadFinished();
        }
        mSharedPreferences.registerOnSharedPreferenceChangeListener(
                mOnSharedPreferenceChangeListener);
    }

    @VisibleForTesting
    public boolean isLoaded() {
        return mLoaded;
    }

    /** Logs the record of the watched channel. */
    public void logChannelViewStop(Channel channel, long endTime, long duration) {
        if (duration < MIN_DURATION_MS) {
            return;
        }
        WatchedRecord record = new WatchedRecord(channel.getId(), endTime - duration, duration);
        if (mLoaded) {
            if (DEBUG) Log.d(TAG, "Log a watched record. " + record);
            mWatchedHistory.add(record);
            ++mLastIndex;
            mSharedPreferences
                    .edit()
                    .putString(getSharedPreferencesKey(mLastIndex), encode(record))
                    .putLong(PREF_KEY_LAST_INDEX, mLastIndex)
                    .apply();
            if (mListener != null) {
                mListener.onNewRecordAdded(record);
            }
        } else {
            mPendingRecords.add(record);
        }
    }

    /** Sets {@link Listener}. */
    public void setListener(Listener listener) {
        mListener = listener;
    }

    /**
     * Returns watched history in the ascending order of time. In other words, the first element is
     * the oldest and the last element is the latest record.
     */
    @NonNull
    public List<WatchedRecord> getWatchedHistory() {
        return Collections.unmodifiableList(mWatchedHistory);
    }

    @VisibleForTesting
    WatchedRecord getRecord(int reverseIndex) {
        return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
    }

    @VisibleForTesting
    WatchedRecord getRecordFromSharedPreferences(int reverseIndex) {
        long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
        long index = lastIndex - reverseIndex;
        return decode(mSharedPreferences.getString(getSharedPreferencesKey(index), null));
    }

    private String getSharedPreferencesKey(long index) {
        return Long.toString(index % mMaxHistorySize);
    }

    public static class WatchedRecord {
        public final long channelId;
        public final long watchedStartTime;
        public final long duration;

        WatchedRecord(long channelId, long watchedStartTime, long duration) {
            this.channelId = channelId;
            this.watchedStartTime = watchedStartTime;
            this.duration = duration;
        }

        @Override
        public String toString() {
            return "WatchedRecord: id="
                    + channelId
                    + ",watchedStartTime="
                    + watchedStartTime
                    + ",duration="
                    + duration;
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof WatchedRecord) {
                WatchedRecord that = (WatchedRecord) o;
                return Objects.equals(channelId, that.channelId)
                        && Objects.equals(watchedStartTime, that.watchedStartTime)
                        && Objects.equals(duration, that.duration);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hash(channelId, watchedStartTime, duration);
        }
    }

    @VisibleForTesting
    String encode(WatchedRecord record) {
        return record.channelId + " " + record.watchedStartTime + " " + record.duration;
    }

    @VisibleForTesting
    WatchedRecord decode(String encodedString) {
        try (Scanner scanner = new Scanner(encodedString)) {
            long channelId = scanner.nextLong();
            long watchedStartTime = scanner.nextLong();
            long duration = scanner.nextLong();
            return new WatchedRecord(channelId, watchedStartTime, duration);
        } catch (Exception e) {
            return null;
        }
    }

    public interface Listener {
        /** Called when history is loaded. */
        void onLoadFinished();

        void onNewRecordAdded(WatchedRecord watchedRecord);
    }
}
