import $ from 'jquery';

import DataProvider from 'bitbucket/internal/impl/data-provider/data-provider';

import errors from '../javascript-errors/javascript-errors';
import validator from '../json-validation/json-validation';

export default class PagedDataProvider extends DataProvider {

    /**
     * @param {Object?} options - The options for the Data Provider
     * @param {Object?} options.filter - a set of parameters to filter the DataProvider
     * @param {?Object} initialData - The initial data for this provider
     */
    constructor(options = {}, ...args) {
        super(options, ...args);
        // the untransformed last response fetched.
        this._lastPageData = null;

        if (options.filterDescriptor) {
            this._validateFilter = validator(options.filterDescriptor);
        }

        this.filter = options.filter || {};
        this._validateFilter(this.filter);
    }

    /**
     * @type {boolean}
     * Whether the data provider has fetched the last page of data yet. Returns false before any data has been fetched.
     */
    get reachedEnd() {
        return this._lastPageData ? this._reachedEnd(this._lastPageData) : false;
    }

    /**
     * Validates that the filter matches the shape expected by a subclass. By default, providers are assumed not to
     * be filterable, and throw if any filter properties are set.
     *
     * @param {Object} f - a filter
     */
    _validateFilter(f) {
        if (Object.keys(f).length) {
            throw new Error('No filter is expected on this data-provider.');
        }
    }

    /**
     * Set a filter property and reset the data provider
     *
     * @param {string} key - The key to set on this filter
     * @param {*} val - The value to set the key to
     */
    setFilter(key, val) {
        if (this.filter) {
            this.filter[key] = val;
        }
        if (this._validateFilter) {
            this._validateFilter(this.filter);
        }
    }

    /**
     * Fetch the next page of data. Duplicates lot's of logic in `fetch()`, but ignores `currentData` for caching.
     * @returns {Promise} A promise that resolves to the transformed data
     */
    fetchNext() {
        if (this.reachedEnd) {
            throw new Error('Nothing left to fetch.');
        }

        // If a fetch is attempted before the previous fetch has returned,
        // then abort the previous fetch and continue with the new request.
        this.abort();

        // If there is initial data, upgrade the currentData to initialData
        if (this.initialData) {
            this._lastPageData = this.initialData;
            this.currentData = this.transform(this.initialData);
            // initialData is single use, delete it after it has been consumed
            delete this.initialData;
            return $.Deferred().resolve(this.currentData);
        }

        this.trigger('data-requested');

        this._requestPromise = this._fetchNext(this._lastPageData);
        if (!this._requestPromise.abort) {
            throw new Error('no abort method on PagedDataProvider#_requestPromise.');
        }

        this._requestPromise.then(page => {
            this._lastPageData = page;
        });
        return this._requestPromise
            .then(this.transform, (...args) => $.Deferred().reject(this.errorTransform(...args)))
            .done(transformedData => {
                this.trigger('data-loaded', transformedData);
                this.currentData = transformedData;
            })
            .fail(error => {
                this.trigger('data-request-failed', error);
            })
            .always(() => {
                this._requestPromise = null;
            });
    }

    reset() {
        this._lastPageData = null;
        super.reset();
    }

    /*eslint-disable valid-jsdoc*/ // Disable JSDoc validation for interface
    /**
     * Given the last page retrieved, call `this._fetch(url)` with the correct URL for the next page of data.
     * @param {*} lastPageData - the data returned by the last request, or null if no request has been made
     * @returns {Promise} - must be abortable.
     * @protected
     */
    _fetchNext(lastPageData) { // eslint-disable-line no-unused-vars
        throw new errors.NotImplementedError();
    }

    /**
     * Whether the data provider has fetched the last page of data yet.
     *
     * @param {*} lastPageData - the data returned by the last request
     * @returns {boolean}
     * @protected
     */
    _reachedEnd(lastPageData) { // eslint-disable-line no-unused-vars
        throw new errors.NotImplementedError();
    }

    /*eslint-enable valid-jsdoc*/
}
