/*
 * Copyright (C) 2018, Brian He
 *
 * 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.github.brianspace.moviebrowser.models;

import android.support.annotation.NonNull;
import android.util.Log;
import com.github.brianspace.common.observable.CollectionObservableBase;
import com.github.brianspace.common.observable.ICollectionObserver.Action;
import com.github.brianspace.moviebrowser.BuildConfig;
import com.github.brianspace.moviebrowser.repository.IMovieDbService;
import com.github.brianspace.moviebrowser.repository.data.MovieData;
import com.github.brianspace.moviebrowser.repository.data.PagingEnvelope;
import io.reactivex.Completable;
import io.reactivex.Single;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;

/**
 * Base class for movie list.
 */
public abstract class MovieCollection extends CollectionObservableBase implements IMovieCollection {
    // region Private Constants

    /**
     * Tag for logging.
     */
    private static final String LOG_TAG = MovieCollection.class.getSimpleName();

    // endregion

    // region Protected Fields

    /**
     * Interface for accessing TMDb Web API.
     */
    protected final IMovieDbService movieDbService;
    /**
     * Interface for model entity store.
     */
    protected final IEntityStore entityStore;

    /**
     * The data layer list of the pages of movie data.
     */
    protected final List<PagingEnvelope<MovieData>> resultList = new ArrayList<>();
    /**
     * List of model layer movie objects.
     */
    protected final List<Movie> movies = new ArrayList<>();
    /**
     * State of data loading.
     */
    protected boolean isLoading;

    // endregion

    // region Private Fields

    /**
     * Result returned by load().
     */
    private Completable loadCompletable;

    /**
     * Result returned by refresh().
     */
    private Completable refreshCompletable;

    /**
     * Result returned by loadNextPage().
     */
    private Completable nextPageCompletable;

    /**
     * RxJava mapping function to extract the data layer pages of movies into model layer movie list.
     */
    private final Function<PagingEnvelope<MovieData>, Object> resultHandler =
            new Function<PagingEnvelope<MovieData>, Object>() {
                @Override
                public Object apply(final PagingEnvelope<MovieData> moviesResult) {
                    if (moviesResult.getResults().isEmpty()) {
                        return Irrelevant.INSTANCE;
                    }

                    onMoviePage(moviesResult);

                    isLoading = false;
                    return Irrelevant.INSTANCE;
                }

                private void onMoviePage(final PagingEnvelope<MovieData> moviesResult) {
                    resultList.add(moviesResult);
                    final List<Object> appendList = new ArrayList<>();
                    for (final MovieData movie : moviesResult.getResults()) {
                        // Validate
                        if (movie.isValid()) {
                            // De-duplicate.
                            final Movie model = entityStore.findMovieById(movie.getId());
                            if (model == null || movies.indexOf(model) < 0) {
                                final Movie movieModel = entityStore.getMovieModel(movie);
                                movies.add(0, movieModel);
                                appendList.add(0, movieModel);
                            }
                        } else if (BuildConfig.DEBUG) {
                            Log.w(LOG_TAG, "Invalid movie data for: \n" + movie.toString());
                        }
                    }

                    if (!appendList.isEmpty()) {
                        setChanged();
                        notifyObservers(Action.AppendRange, null, appendList);
                    }
                }
            };

    // endregion

    // region Constructors

    /**
     * Constructor for model layer internal usage.
     *
     * @param movieDbService interface for accessing TMDb Web API.
     * @param entityStore interface for model entity store.
     */
    protected MovieCollection(@NonNull final IMovieDbService movieDbService,
            @NonNull final IEntityStore entityStore) {
        this.movieDbService = movieDbService;
        this.entityStore = entityStore;
    }

    // endregion

    // region Public Overrides

    @Override
    public boolean isLoaded() {
        return !movies.isEmpty();
    }

    @Override
    public boolean isLoading() {
        return isLoading;
    }

    @NonNull
    @Override
    public List<Movie> getMovies() {
        return movies;
    }

    @NonNull
    @Override
    public Completable load() {
        if (isLoading && loadCompletable != null) {
            return loadCompletable;
        }

        if (isLoaded()) {
            return Completable.complete();
        }

        isLoading = true;
        loadCompletable = getFirstPage()
                .doFinally(() -> isLoading = false)
                .map(resultHandler)
                .toCompletable();
        return loadCompletable;
    }

    @NonNull
    @Override
    public Completable refresh() {
        if (isLoading && refreshCompletable != null) {
            return refreshCompletable;
        }

        isLoading = true;
        refreshCompletable = getFirstPage()
                .doFinally(() -> isLoading = false)
                .map(moviesResult -> {
                    resultList.clear();
                    movies.clear();
                    setChanged();
                    notifyObservers(Action.Clear, null, null);
                    return moviesResult;
                })
                .map(resultHandler)
                .toCompletable();
        return refreshCompletable;
    }

    @Override
    public boolean hasNexPage() {
        final int resultSize = resultList.size();
        if (resultSize == 0) {
            return true;
        }

        final PagingEnvelope<MovieData> prevResult = resultList.get(resultSize - 1);
        return prevResult.getPage() < prevResult.getTotalPages();
    }

    @NonNull
    @Override
    public Completable loadNextPage() {
        if (isLoading && nextPageCompletable != null) {
            return nextPageCompletable;
        }

        final int resultSize = resultList.size();
        final PagingEnvelope<MovieData> prevResult = resultSize > 0 ? resultList.get(resultSize - 1) : null;
        isLoading = true;
        nextPageCompletable = getNextPage(prevResult)
                .subscribeOn(Schedulers.io())
                .doFinally(() -> isLoading = false)
                .map(resultHandler)
                .toCompletable();
        return nextPageCompletable;
    }

    // endregion

    // region Protected Methods

    /**
     * Get the first page of movie list.
     * Subclass should implement to return the first page of movies.
     *
     * @return RxJava Observable of a page of movie data list.
     */
    protected abstract Single<PagingEnvelope<MovieData>> getFirstPage();

    /**
     * Get the next page of movie list.
     * Subclass should implement to return the next page of movies based on the previous page.
     *
     * @return RxJava Observable of a page of movie data list.
     */
    protected abstract Single<PagingEnvelope<MovieData>> getNextPage(PagingEnvelope<MovieData> prev);

    // endregion
}