/*
 * 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.dvr.data;

import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.text.TextUtils;

import com.android.tv.data.BaseProgramImpl;
import com.android.tv.data.api.BaseProgram;
import com.android.tv.data.api.Program;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.provider.DvrContract.SeriesRecordings;
import com.android.tv.util.Utils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Objects;

/**
 * Schedules the recording of a Series of Programs.
 *
 * <p>Contains the data needed to create new ScheduleRecordings as the programs become available in
 * the EPG.
 */
public class SeriesRecording implements Parcelable {
    /** Indicates that the ID is not assigned yet. */
    public static final long ID_NOT_SET = 0;

    /** The default priority of this recording. */
    public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            flag = true,
            value = {OPTION_CHANNEL_ONE, OPTION_CHANNEL_ALL})
    public @interface ChannelOption {}
    /** An option which indicates that the episodes in one channel are recorded. */
    public static final int OPTION_CHANNEL_ONE = 0;
    /** An option which indicates that the episodes in all the channels are recorded. */
    public static final int OPTION_CHANNEL_ALL = 1;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            flag = true,
            value = {STATE_SERIES_NORMAL, STATE_SERIES_STOPPED})
    public @interface SeriesState {}

    /** The state indicates that the series recording is a normal one. */
    public static final int STATE_SERIES_NORMAL = 0;

    /** The state indicates that the series recording is stopped. */
    public static final int STATE_SERIES_STOPPED = 1;

    /** Compare priority in descending order. */
    public static final Comparator<SeriesRecording> PRIORITY_COMPARATOR =
            (SeriesRecording lhs, SeriesRecording rhs) -> {
                int value = Long.compare(rhs.mPriority, lhs.mPriority);
                if (value == 0) {
                    // New recording has the higher priority.
                    value = Long.compare(rhs.mId, lhs.mId);
                }
                return value;
            };

    /** Compare ID in ascending order. */
    public static final Comparator<SeriesRecording> ID_COMPARATOR =
            (SeriesRecording lhs, SeriesRecording rhs) -> Long.compare(lhs.mId, rhs.mId);

    /**
     * Creates a new Builder with the values set from the series information of {@link
     * BaseProgramImpl}.
     */
    public static Builder builder(String inputId, BaseProgram p) {
        return new Builder()
                .setInputId(inputId)
                .setSeriesId(p.getSeriesId())
                .setChannelId(p.getChannelId())
                .setTitle(p.getTitle())
                .setDescription(p.getDescription())
                .setLongDescription(p.getLongDescription())
                .setCanonicalGenreIds(p.getCanonicalGenreIds())
                .setPosterUri(p.getPosterArtUri())
                .setPhotoUri(p.getThumbnailUri());
    }

    /** Creates a new Builder with the values set from an existing {@link SeriesRecording}. */
    public static Builder buildFrom(SeriesRecording r) {
        return new Builder()
                .setId(r.mId)
                .setInputId(r.getInputId())
                .setChannelId(r.getChannelId())
                .setPriority(r.getPriority())
                .setTitle(r.getTitle())
                .setDescription(r.getDescription())
                .setLongDescription(r.getLongDescription())
                .setSeriesId(r.getSeriesId())
                .setStartFromEpisode(r.getStartFromEpisode())
                .setStartFromSeason(r.getStartFromSeason())
                .setChannelOption(r.getChannelOption())
                .setCanonicalGenreIds(r.getCanonicalGenreIds())
                .setPosterUri(r.getPosterUri())
                .setPhotoUri(r.getPhotoUri())
                .setState(r.getState());
    }

    /**
     * Use this projection if you want to create {@link SeriesRecording} object using {@link
     * #fromCursor}.
     */
    public static final String[] PROJECTION = {
        // Columns must match what is read in fromCursor()
        SeriesRecordings._ID,
        SeriesRecordings.COLUMN_INPUT_ID,
        SeriesRecordings.COLUMN_CHANNEL_ID,
        SeriesRecordings.COLUMN_PRIORITY,
        SeriesRecordings.COLUMN_TITLE,
        SeriesRecordings.COLUMN_SHORT_DESCRIPTION,
        SeriesRecordings.COLUMN_LONG_DESCRIPTION,
        SeriesRecordings.COLUMN_SERIES_ID,
        SeriesRecordings.COLUMN_START_FROM_EPISODE,
        SeriesRecordings.COLUMN_START_FROM_SEASON,
        SeriesRecordings.COLUMN_CHANNEL_OPTION,
        SeriesRecordings.COLUMN_CANONICAL_GENRE,
        SeriesRecordings.COLUMN_POSTER_URI,
        SeriesRecordings.COLUMN_PHOTO_URI,
        SeriesRecordings.COLUMN_STATE
    };
    /** Creates {@link SeriesRecording} object from the given {@link Cursor}. */
    public static SeriesRecording fromCursor(Cursor c) {
        int index = -1;
        return new Builder()
                .setId(c.getLong(++index))
                .setInputId(c.getString(++index))
                .setChannelId(c.getLong(++index))
                .setPriority(c.getLong(++index))
                .setTitle(c.getString(++index))
                .setDescription(c.getString(++index))
                .setLongDescription(c.getString(++index))
                .setSeriesId(c.getString(++index))
                .setStartFromEpisode(c.getInt(++index))
                .setStartFromSeason(c.getInt(++index))
                .setChannelOption(channelOption(c.getString(++index)))
                .setCanonicalGenreIds(c.getString(++index))
                .setPosterUri(c.getString(++index))
                .setPhotoUri(c.getString(++index))
                .setState(seriesRecordingState(c.getString(++index)))
                .build();
    }

    /**
     * Returns the ContentValues with keys as the columns specified in {@link SeriesRecordings} and
     * the values from {@code r}.
     */
    public static ContentValues toContentValues(SeriesRecording r) {
        ContentValues values = new ContentValues();
        if (r.getId() != ID_NOT_SET) {
            values.put(SeriesRecordings._ID, r.getId());
        } else {
            values.putNull(SeriesRecordings._ID);
        }
        values.put(SeriesRecordings.COLUMN_INPUT_ID, r.getInputId());
        values.put(SeriesRecordings.COLUMN_CHANNEL_ID, r.getChannelId());
        values.put(SeriesRecordings.COLUMN_PRIORITY, r.getPriority());
        values.put(SeriesRecordings.COLUMN_TITLE, r.getTitle());
        values.put(SeriesRecordings.COLUMN_SHORT_DESCRIPTION, r.getDescription());
        values.put(SeriesRecordings.COLUMN_LONG_DESCRIPTION, r.getLongDescription());
        values.put(SeriesRecordings.COLUMN_SERIES_ID, r.getSeriesId());
        values.put(SeriesRecordings.COLUMN_START_FROM_EPISODE, r.getStartFromEpisode());
        values.put(SeriesRecordings.COLUMN_START_FROM_SEASON, r.getStartFromSeason());
        values.put(SeriesRecordings.COLUMN_CHANNEL_OPTION, channelOption(r.getChannelOption()));
        values.put(
                SeriesRecordings.COLUMN_CANONICAL_GENRE,
                Utils.getCanonicalGenre(r.getCanonicalGenreIds()));
        values.put(SeriesRecordings.COLUMN_POSTER_URI, r.getPosterUri());
        values.put(SeriesRecordings.COLUMN_PHOTO_URI, r.getPhotoUri());
        values.put(SeriesRecordings.COLUMN_STATE, seriesRecordingState(r.getState()));
        return values;
    }

    private static String channelOption(@ChannelOption int option) {
        switch (option) {
            case OPTION_CHANNEL_ONE:
                return SeriesRecordings.OPTION_CHANNEL_ONE;
            case OPTION_CHANNEL_ALL:
                return SeriesRecordings.OPTION_CHANNEL_ALL;
        }
        return SeriesRecordings.OPTION_CHANNEL_ONE;
    }

    @ChannelOption
    private static int channelOption(String option) {
        switch (option) {
            case SeriesRecordings.OPTION_CHANNEL_ONE:
                return OPTION_CHANNEL_ONE;
            case SeriesRecordings.OPTION_CHANNEL_ALL:
                return OPTION_CHANNEL_ALL;
        }
        return OPTION_CHANNEL_ONE;
    }

    private static String seriesRecordingState(@SeriesState int state) {
        switch (state) {
            case STATE_SERIES_NORMAL:
                return SeriesRecordings.STATE_SERIES_NORMAL;
            case STATE_SERIES_STOPPED:
                return SeriesRecordings.STATE_SERIES_STOPPED;
        }
        return SeriesRecordings.STATE_SERIES_NORMAL;
    }

    @SeriesState
    private static int seriesRecordingState(String state) {
        switch (state) {
            case SeriesRecordings.STATE_SERIES_NORMAL:
                return STATE_SERIES_NORMAL;
            case SeriesRecordings.STATE_SERIES_STOPPED:
                return STATE_SERIES_STOPPED;
        }
        return STATE_SERIES_NORMAL;
    }

    /** Builder for {@link SeriesRecording}. */
    public static class Builder {
        private long mId = ID_NOT_SET;
        private long mPriority = DvrScheduleManager.DEFAULT_SERIES_PRIORITY;
        private String mTitle;
        private String mDescription;
        private String mLongDescription;
        private String mInputId;
        private long mChannelId;
        private String mSeriesId;
        private int mStartFromSeason = SeriesRecordings.THE_BEGINNING;
        private int mStartFromEpisode = SeriesRecordings.THE_BEGINNING;
        private int mChannelOption = OPTION_CHANNEL_ONE;
        private int[] mCanonicalGenreIds;
        private String mPosterUri;
        private String mPhotoUri;
        private int mState = SeriesRecording.STATE_SERIES_NORMAL;

        /** @see #getId() */
        public Builder setId(long id) {
            mId = id;
            return this;
        }

        /** @see #getPriority() () */
        public Builder setPriority(long priority) {
            mPriority = priority;
            return this;
        }

        /** @see #getTitle() */
        public Builder setTitle(String title) {
            mTitle = title;
            return this;
        }

        /** @see #getDescription() */
        public Builder setDescription(String description) {
            mDescription = description;
            return this;
        }

        /** @see #getLongDescription() */
        public Builder setLongDescription(String longDescription) {
            mLongDescription = longDescription;
            return this;
        }

        /** @see #getInputId() */
        public Builder setInputId(String inputId) {
            mInputId = inputId;
            return this;
        }

        /** @see #getChannelId() */
        public Builder setChannelId(long channelId) {
            mChannelId = channelId;
            return this;
        }

        /** @see #getSeriesId() */
        public Builder setSeriesId(String seriesId) {
            mSeriesId = seriesId;
            return this;
        }

        /** @see #getStartFromSeason() */
        public Builder setStartFromSeason(int startFromSeason) {
            mStartFromSeason = startFromSeason;
            return this;
        }

        /** @see #getChannelOption() */
        public Builder setChannelOption(@ChannelOption int option) {
            mChannelOption = option;
            return this;
        }

        /** @see #getStartFromEpisode() */
        public Builder setStartFromEpisode(int startFromEpisode) {
            mStartFromEpisode = startFromEpisode;
            return this;
        }

        /** @see #getCanonicalGenreIds() */
        public Builder setCanonicalGenreIds(String genres) {
            mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres);
            return this;
        }

        /** @see #getCanonicalGenreIds() */
        public Builder setCanonicalGenreIds(int[] canonicalGenreIds) {
            mCanonicalGenreIds = canonicalGenreIds;
            return this;
        }

        /** @see #getPosterUri() */
        public Builder setPosterUri(String posterUri) {
            mPosterUri = posterUri;
            return this;
        }

        /** @see #getPhotoUri() */
        public Builder setPhotoUri(String photoUri) {
            mPhotoUri = photoUri;
            return this;
        }

        /** @see #getState() */
        public Builder setState(@SeriesState int state) {
            mState = state;
            return this;
        }

        /** Creates a new {@link SeriesRecording}. */
        public SeriesRecording build() {
            return new SeriesRecording(
                    mId,
                    mPriority,
                    mTitle,
                    mDescription,
                    mLongDescription,
                    mInputId,
                    mChannelId,
                    mSeriesId,
                    mStartFromSeason,
                    mStartFromEpisode,
                    mChannelOption,
                    mCanonicalGenreIds,
                    mPosterUri,
                    mPhotoUri,
                    mState);
        }
    }

    public static SeriesRecording fromParcel(Parcel in) {
        return new Builder()
                .setId(in.readLong())
                .setPriority(in.readLong())
                .setTitle(in.readString())
                .setDescription(in.readString())
                .setLongDescription(in.readString())
                .setInputId(in.readString())
                .setChannelId(in.readLong())
                .setSeriesId(in.readString())
                .setStartFromSeason(in.readInt())
                .setStartFromEpisode(in.readInt())
                .setChannelOption(in.readInt())
                .setCanonicalGenreIds(in.createIntArray())
                .setPosterUri(in.readString())
                .setPhotoUri(in.readString())
                .setState(in.readInt())
                .build();
    }

    public static final Parcelable.Creator<SeriesRecording> CREATOR =
            new Parcelable.Creator<SeriesRecording>() {
                @Override
                public SeriesRecording createFromParcel(Parcel in) {
                    return SeriesRecording.fromParcel(in);
                }

                @Override
                public SeriesRecording[] newArray(int size) {
                    return new SeriesRecording[size];
                }
            };

    private long mId;
    private final long mPriority;
    private final String mTitle;
    private final String mDescription;
    private final String mLongDescription;
    private final String mInputId;
    private final long mChannelId;
    private final String mSeriesId;
    private final int mStartFromSeason;
    private final int mStartFromEpisode;
    @ChannelOption private final int mChannelOption;
    private final int[] mCanonicalGenreIds;
    private final String mPosterUri;
    private final String mPhotoUri;
    @SeriesState private int mState;

    /** The input id of this SeriesRecording. */
    public String getInputId() {
        return mInputId;
    }

    /**
     * The channelId to match. The channel ID might not be valid when the channel option is "ALL".
     */
    public long getChannelId() {
        return mChannelId;
    }

    /** The id of this SeriesRecording. */
    public long getId() {
        return mId;
    }

    /** Sets the ID. */
    public void setId(long id) {
        mId = id;
    }

    /**
     * The priority of this recording.
     *
     * <p>The highest number is recorded first. If there is a tie in mPriority then the higher mId
     * wins.
     */
    public long getPriority() {
        return mPriority;
    }

    /** The series title. */
    public String getTitle() {
        return mTitle;
    }

    /** The series description. */
    public String getDescription() {
        return mDescription;
    }

    /** The long series description. */
    public String getLongDescription() {
        return mLongDescription;
    }

    /**
     * SeriesId when not null is used to match programs instead of using title and channelId.
     *
     * <p>SeriesId is an opaque but stable string.
     */
    public String getSeriesId() {
        return mSeriesId;
    }

    /**
     * If not == {@link SeriesRecordings#THE_BEGINNING} and seasonNumber == startFromSeason then
     * only record episodes with a episodeNumber >= this
     */
    public int getStartFromEpisode() {
        return mStartFromEpisode;
    }

    /**
     * If not == {@link SeriesRecordings#THE_BEGINNING} then only record episodes with a
     * seasonNumber >= this
     */
    public int getStartFromSeason() {
        return mStartFromSeason;
    }

    /** Returns the channel recording option. */
    @ChannelOption
    public int getChannelOption() {
        return mChannelOption;
    }

    /** Returns the canonical genre ID's. */
    public int[] getCanonicalGenreIds() {
        return mCanonicalGenreIds;
    }

    /** Returns the poster URI. */
    public String getPosterUri() {
        return mPosterUri;
    }

    /** Returns the photo URI. */
    public String getPhotoUri() {
        return mPhotoUri;
    }

    /** Returns the state of series recording. */
    @SeriesState
    public int getState() {
        return mState;
    }

    /** Checks whether the series recording is stopped or not. */
    public boolean isStopped() {
        return mState == STATE_SERIES_STOPPED;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SeriesRecording)) return false;
        SeriesRecording that = (SeriesRecording) o;
        return mPriority == that.mPriority
                && mChannelId == that.mChannelId
                && mStartFromSeason == that.mStartFromSeason
                && mStartFromEpisode == that.mStartFromEpisode
                && Objects.equals(mId, that.mId)
                && Objects.equals(mTitle, that.mTitle)
                && Objects.equals(mDescription, that.mDescription)
                && Objects.equals(mLongDescription, that.mLongDescription)
                && Objects.equals(mSeriesId, that.mSeriesId)
                && mChannelOption == that.mChannelOption
                && Arrays.equals(mCanonicalGenreIds, that.mCanonicalGenreIds)
                && Objects.equals(mPosterUri, that.mPosterUri)
                && Objects.equals(mPhotoUri, that.mPhotoUri)
                && mState == that.mState;
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                mPriority,
                mChannelId,
                mStartFromSeason,
                mStartFromEpisode,
                mId,
                mTitle,
                mDescription,
                mLongDescription,
                mSeriesId,
                mChannelOption,
                Arrays.hashCode(mCanonicalGenreIds),
                mPosterUri,
                mPhotoUri,
                mState);
    }

    @Override
    public String toString() {
        return "SeriesRecording{"
                + "inputId="
                + mInputId
                + ", channelId="
                + mChannelId
                + ", id='"
                + mId
                + '\''
                + ", priority="
                + mPriority
                + ", title='"
                + mTitle
                + '\''
                + ", description='"
                + mDescription
                + '\''
                + ", longDescription='"
                + mLongDescription
                + '\''
                + ", startFromSeason="
                + mStartFromSeason
                + ", startFromEpisode="
                + mStartFromEpisode
                + ", channelOption="
                + mChannelOption
                + ", canonicalGenreIds="
                + Arrays.toString(mCanonicalGenreIds)
                + ", posterUri="
                + mPosterUri
                + ", photoUri="
                + mPhotoUri
                + ", state="
                + mState
                + '}';
    }

    private SeriesRecording(
            long id,
            long priority,
            String title,
            String description,
            String longDescription,
            String inputId,
            long channelId,
            String seriesId,
            int startFromSeason,
            int startFromEpisode,
            int channelOption,
            int[] canonicalGenreIds,
            String posterUri,
            String photoUri,
            int state) {
        this.mId = id;
        this.mPriority = priority;
        this.mTitle = title;
        this.mDescription = description;
        this.mLongDescription = longDescription;
        this.mInputId = inputId;
        this.mChannelId = channelId;
        this.mSeriesId = seriesId;
        this.mStartFromSeason = startFromSeason;
        this.mStartFromEpisode = startFromEpisode;
        this.mChannelOption = channelOption;
        this.mCanonicalGenreIds = canonicalGenreIds;
        this.mPosterUri = posterUri;
        this.mPhotoUri = photoUri;
        this.mState = state;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int paramInt) {
        out.writeLong(mId);
        out.writeLong(mPriority);
        out.writeString(mTitle);
        out.writeString(mDescription);
        out.writeString(mLongDescription);
        out.writeString(mInputId);
        out.writeLong(mChannelId);
        out.writeString(mSeriesId);
        out.writeInt(mStartFromSeason);
        out.writeInt(mStartFromEpisode);
        out.writeInt(mChannelOption);
        out.writeIntArray(mCanonicalGenreIds);
        out.writeString(mPosterUri);
        out.writeString(mPhotoUri);
        out.writeInt(mState);
    }

    /** Returns an array containing all of the elements in the list. */
    public static SeriesRecording[] toArray(Collection<SeriesRecording> series) {
        return series.toArray(new SeriesRecording[series.size()]);
    }

    /**
     * Returns {@code true} if the {@code program} is part of the series and meets the season and
     * episode constraints.
     */
    public boolean matchProgram(Program program) {
        return matchProgram(program, mChannelOption);
    }

    /**
     * Returns {@code true} if the {@code program} is part of the series and meets the season and
     * episode constraints. It checks the channel option only if {@code checkChannelOption} is
     * {@code true}.
     */
    public boolean matchProgram(Program program, @ChannelOption int channelOption) {
        String seriesId = program.getSeriesId();
        long channelId = program.getChannelId();
        String seasonNumber = program.getSeasonNumber();
        String episodeNumber = program.getEpisodeNumber();
        if (!mSeriesId.equals(seriesId)
                || (channelOption == SeriesRecording.OPTION_CHANNEL_ONE
                        && mChannelId != channelId)) {
            return false;
        }
        // Season number and episode number matches if
        // start_season_number < program_season_number
        // || (start_season_number == program_season_number
        // && start_episode_number <= program_episode_number).
        if (mStartFromSeason == SeriesRecordings.THE_BEGINNING || TextUtils.isEmpty(seasonNumber)) {
            return true;
        } else {
            int intSeasonNumber;
            try {
                intSeasonNumber = Integer.valueOf(seasonNumber);
            } catch (NumberFormatException e) {
                return true;
            }
            if (intSeasonNumber > mStartFromSeason) {
                return true;
            } else if (intSeasonNumber < mStartFromSeason) {
                return false;
            }
        }
        if (mStartFromEpisode == SeriesRecordings.THE_BEGINNING
                || TextUtils.isEmpty(episodeNumber)) {
            return true;
        } else {
            int intEpisodeNumber;
            try {
                intEpisodeNumber = Integer.valueOf(episodeNumber);
            } catch (NumberFormatException e) {
                return true;
            }
            return intEpisodeNumber >= mStartFromEpisode;
        }
    }
}
