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

import android.content.Context;
import com.android.tv.data.ChannelImpl;
import com.android.tv.data.api.Channel;
import com.android.tv.testing.utils.Utils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class RecommendationUtils {
    private static final long INVALID_CHANNEL_ID = -1;

    /** Create a mock RecommendationDataManager backed by a {@link ChannelRecordSortedMapHelper}. */
    public static RecommendationDataManager createMockRecommendationDataManager(
            final ChannelRecordSortedMapHelper channelRecordSortedMap) {
        RecommendationDataManager dataManager = Mockito.mock(RecommendationDataManager.class);
        Mockito.doAnswer(
                        new Answer<Integer>() {
                            @Override
                            public Integer answer(InvocationOnMock invocation) throws Throwable {
                                return channelRecordSortedMap.size();
                            }
                        })
                .when(dataManager)
                .getChannelRecordCount();
        Mockito.doAnswer(
                        new Answer<Collection<ChannelRecord>>() {
                            @Override
                            public Collection<ChannelRecord> answer(InvocationOnMock invocation)
                                    throws Throwable {
                                return channelRecordSortedMap.values();
                            }
                        })
                .when(dataManager)
                .getChannelRecords();
    Mockito.doAnswer(
            new Answer<ChannelRecord>() {
              @Override
              public ChannelRecord answer(InvocationOnMock invocation) throws Throwable {
                long channelId = (long) invocation.getArguments()[0];
                return channelRecordSortedMap.get(channelId);
              }
            })
        .when(dataManager)
        .getChannelRecord(ArgumentMatchers.anyLong());
        return dataManager;
    }

    public static class ChannelRecordSortedMapHelper extends TreeMap<Long, ChannelRecord> {
        private final Context mContext;
        private Recommender mRecommender;
        private Random mRandom = Utils.createTestRandom();

        public ChannelRecordSortedMapHelper(Context context) {
            mContext = context;
        }

        public void setRecommender(Recommender recommender) {
            mRecommender = recommender;
        }

        public void resetRandom(Random random) {
            mRandom = random;
        }

        /**
         * Add new {@code numberOfChannels} channels by adding channel record to {@code
         * channelRecordMap} with no history. This action corresponds to loading channels in the
         * RecommendationDataManger.
         */
        public void addChannels(int numberOfChannels) {
            for (int i = 0; i < numberOfChannels; ++i) {
                addChannel();
            }
        }

        /**
         * Add new one channel by adding channel record to {@code channelRecordMap} with no history.
         * This action corresponds to loading one channel in the RecommendationDataManger.
         *
         * @return The new channel was made by this method.
         */
        public Channel addChannel() {
            long channelId = size();
            ChannelImpl channel = new ChannelImpl.Builder().setId(channelId).build();
            ChannelRecord channelRecord = new ChannelRecord(mContext, channel, false);
            put(channelId, channelRecord);
            return channel;
        }

        /**
         * Add the watch logs which its durationTime is under {@code maxWatchDurationMs}. Add until
         * latest watch end time becomes bigger than {@code watchEndTimeMs}, starting from {@code
         * watchStartTimeMs}.
         *
         * @return true if adding watch log success, otherwise false.
         */
        public boolean addRandomWatchLogs(
                long watchStartTimeMs, long watchEndTimeMs, long maxWatchDurationMs) {
            long latestWatchEndTimeMs = watchStartTimeMs;
            long previousChannelId = INVALID_CHANNEL_ID;
            List<Long> channelIdList = new ArrayList<>(keySet());
            while (latestWatchEndTimeMs < watchEndTimeMs) {
                long channelId = channelIdList.get(mRandom.nextInt(channelIdList.size()));
                if (previousChannelId == channelId) {
                    // Time hopping with random minutes.
                    latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(mRandom.nextInt(30) + 1);
                }
        long watchedDurationMs = mRandom.nextInt((int) maxWatchDurationMs) + 1L;
                if (!addWatchLog(channelId, latestWatchEndTimeMs, watchedDurationMs)) {
                    return false;
                }
                latestWatchEndTimeMs += watchedDurationMs;
                previousChannelId = channelId;
            }
            return true;
        }

        /**
         * Add new watch log to channel that id is {@code ChannelId}. Add watch log starts from
         * {@code watchStartTimeMs} with duration {@code durationTimeMs}. If adding is finished,
         * notify the recommender that there's a new watch log.
         *
         * @return true if adding watch log success, otherwise false.
         */
        public boolean addWatchLog(long channelId, long watchStartTimeMs, long durationTimeMs) {
            ChannelRecord channelRecord = get(channelId);
            if (channelRecord == null
                    || watchStartTimeMs + durationTimeMs > System.currentTimeMillis()) {
                return false;
            }

            channelRecord.logWatchHistory(
                    new WatchedProgram(null, watchStartTimeMs, watchStartTimeMs + durationTimeMs));
            if (mRecommender != null) {
                mRecommender.onNewWatchLog(channelRecord);
            }
            return true;
        }
    }
}
