import debounce from 'lodash/debounce';
import mergeWith from 'lodash/mergeWith';
import {combineReducers} from 'redux';
import {createSelector} from 'reselect';
import {
    AVAILABLE_PER_PAGE,
    LOAD,
    LOAD_ERROR,
    LOAD_SUCCESS,
    SORT,
    PAGINATION,
    FILTER,
    PARAMS,
    ALL_PAGES
} from './utils';

const defaultConfig = {
    prefix: '',
    filterFn: null, // (filter, params) => (item) => item.name.indexOf(filter) > -1
    sortFn: null, // (by, params) => (a, b) => by ? a[by].localeCompare(b[by])
    loadFn: null,
    pagination: {page: 1, perPage: AVAILABLE_PER_PAGE[0], availablePerPage: AVAILABLE_PER_PAGE},
    sort: {by: '', reverse: false},
    filter: '',
    params: {},
    items: [],
    debounce: 250,
    server: false
};

export const createSetup = config => {
    return new Setup(config);
};

export class Setup {
    constructor(config = {}) {
        /** @type {defaultConfig} */
        this.config = mergeWith({}, defaultConfig, config, (objValue, srcValue) => {
            if (Array.isArray(objValue)) {
                return srcValue;
            }
        });

        if (!this.config.prefix) {
            throw new Error('Missing mandatory option: prefix');
        }
        if (!this.config.server && !this.config.items && !this.config.loadFn) {
            throw new Error('Missing mandatory option: items or loadFn');
        }
        if (this.config.server && !this.config.loadFn) {
            throw new Error('Missing mandatory option: loadFn');
        }

        //TODO Use createSelector
        //TODO Handle concurrency
        this._load = this.config.debounce ? debounce(this._load, this.config.debounce) : this._load;

        // Safety
        this.load = this.load.bind(this);
        this.setFilter = this.setFilter.bind(this);
        this.setPage = this.setPage.bind(this);
        this.setPerPage = this.setPerPage.bind(this);
        this.setSort = this.setSort.bind(this);
        this.setParams = this.setParams.bind(this);
    }

    // Utils

    prefix() {
        return this.config.prefix;
    }

    _hasState(state) {
        return undefined !== state;
    }

    _prefixAction(action) {
        return `SETUP/${this.config.prefix}/${action}`;
    }

    _createAction(action, payload, error) {
        return {
            type: this._prefixAction(action),
            payload,
            error
        };
    }

    // Actions

    _createLoadedDispatch(action) {
        return dispatch => {
            dispatch(action);
            if (this.config.server) {
                dispatch(this.load());
            }
        };
    }

    /**
     * Debounced load function
     * @param {function} dispatch
     * @param {function} getState
     * @private
     */
    _load(dispatch, getState) {
        if (!this.config.loadFn) {
            return;
        }

        const state = getState();

        const options = {
            params: this.getParams(state),
            filter: this.getFilter(state),
            sort: this.getSort(state),
            pagination: this.getPagination(state)
        };

        dispatch(this._createAction(LOAD));

        new Promise(res => res(this.config.loadFn(options))) // safety for sync loadFn's
            .then(payload => {
                dispatch(this._createAction(LOAD_SUCCESS, payload));
                return payload;
            })
            .catch(e => {
                dispatch(this._createAction(LOAD_ERROR, e, true));
                throw e;
            });
    }

    load() {
        return (dispatch, getState) => {
            this._load(dispatch, getState);
        };
    }

    setFilter(filter) {
        return this._createLoadedDispatch(this._createAction(FILTER, filter));
    }

    setSort(by, reverse) {
        return this._createLoadedDispatch(this._createAction(SORT, {by, reverse}));
    }

    setPage(page) {
        return this._createLoadedDispatch(this._createAction(PAGINATION, {page}));
    }

    setPerPage(perPage) {
        return this._createLoadedDispatch(this._createAction(PAGINATION, {perPage}));
    }

    setParams(params) {
        return this._createLoadedDispatch(this._createAction(PARAMS, params));
    }

    // Selectors

    _filterItems(items, filter, params) {
        let processed = [...items];
        if (this.config.filterFn) {
            processed = processed.filter(this.config.filterFn(filter, params));
        }
        return processed;
    }

    _processItems(filteredItems, pagination, sort, params) {
        let processed = [...filteredItems];
        const {page, perPage} = pagination;
        const {by, reverse} = sort;
        if (!this.config.server) {
            if (this.config.sortFn && by) {
                processed = processed.sort(this.config.sortFn(by, params));
            }
            if (reverse) {
                processed = processed.reverse();
            }
            if (perPage !== ALL_PAGES) {
                const start = (page - 1) * perPage;
                processed = processed.slice(start, start + perPage);
            }
        }
        return processed;
    }

    getState(state) {
        return state[this.config.prefix];
    }

    getAllItems(state) {
        return this.getState(state).items;
    }

    getFilteredTotal(state) {
        return (
            (this.config.server && this.getState(state).filteredTotal) ||
            this.getFilteredItems(state).length
        );
    }

    getTotal(state) {
        return (this.config.server && this.getState(state).total) || this.getAllItems(state).length;
    }

    getSort(state) {
        return this.getState(state).sort;
    }

    getFilter(state) {
        return this.getState(state).filter;
    }

    getError(state) {
        return this.getState(state).error;
    }

    getLoading(state) {
        return this.getState(state).loading;
    }

    getFetched(state) {
        return this.getState(state).fetched;
    }

    getPagination(state) {
        return this.getState(state).pagination;
    }

    getParams(state) {
        return this.getState(state).params;
    }

    getFilteredItems(state) {
        if (!this._getFilteredItems) {
            // lazily create selector and remember it
            this._getFilteredItems = createSelector(
                [this.getAllItems.bind(this), this.getFilter.bind(this), this.getParams.bind(this)],
                this._filterItems.bind(this)
            );
        }
        return this._getFilteredItems(state);
    }

    getItems(state) {
        if (!this._getItems) {
            // lazily create selector and remember it
            this._getItems = createSelector(
                [
                    this.getFilteredItems.bind(this),
                    this.getPagination.bind(this),
                    this.getSort.bind(this),
                    this.getParams.bind(this)
                ],
                this._processItems.bind(this)
            );
        }
        return this._getItems(state);
    }

    // Reducer

    _errorReducer(state = null, action) {
        switch (action.type) {
            case this._prefixAction(LOAD):
            case this._prefixAction(LOAD_SUCCESS):
                return null;
            case this._prefixAction(LOAD_ERROR):
                return action.payload;
            default:
                return state;
        }
    }

    _loadingReducer(state = false, action) {
        switch (action.type) {
            case this._prefixAction(LOAD):
                return true;
            case this._prefixAction(LOAD_ERROR):
            case this._prefixAction(LOAD_SUCCESS):
                return false;
            default:
                return state;
        }
    }

    _fetchedReducer(state = false, action) {
        switch (action.type) {
            case this._prefixAction(LOAD):
                return true;
            default:
                return state;
        }
    }

    _paramsReducer(state, action) {
        if (!this._hasState(state)) {
            state = {...this.config.params};
        }
        switch (action.type) {
            case this._prefixAction(PARAMS):
                return {...state, ...action.payload};
            default:
                return state;
        }
    }

    /**
     * TODO Combine Reducers
     * @param state
     * @param action
     * @return {*}
     * @private
     */
    _paginationReducer(state, action) {
        if (!this._hasState(state)) {
            state = {...this.config.pagination};
        }
        switch (action.type) {
            case this._prefixAction(PARAMS):
            case this._prefixAction(SORT):
            case this._prefixAction(FILTER):
                return {
                    ...state,
                    page: 1
                };
            case this._prefixAction(PAGINATION):
                const page =
                    action.payload.perPage && action.payload.perPage !== state.perPage
                        ? 1 // TODO Recalculate
                        : action.payload.page || state.page;
                return {
                    ...state,
                    page,
                    perPage: action.payload.perPage || state.perPage
                };
            default:
                return state;
        }
    }

    _filterReducer(state, action) {
        if (!this._hasState(state)) {
            state = this.config.filter;
        }
        switch (action.type) {
            case this._prefixAction(FILTER):
                return action.payload;
            default:
                return state;
        }
    }

    _sortReducer(state, action) {
        if (!this._hasState(state)) {
            state = {...this.config.sort};
        }
        switch (action.type) {
            case this._prefixAction(SORT):
                return {...state, by: action.payload.by, reverse: action.payload.reverse};
            default:
                return state;
        }
    }

    _itemsReducer(state, action) {
        if (!this._hasState(state) && this.config.items) {
            state = [...this.config.items];
        }
        switch (action.type) {
            case this._prefixAction(LOAD_SUCCESS):
                if (!Array.isArray(action.payload)) {
                    return [...action.payload.items];
                }
                return [...action.payload];
            default:
                return state;
        }
    }

    _totalReducer(state, action) {
        if (!this._hasState(state) && this.config.items) {
            state = this.config.items.length;
        }
        switch (action.type) {
            case this._prefixAction(LOAD_SUCCESS):
                if (!Array.isArray(action.payload)) {
                    return action.payload.total;
                }
                return action.payload.length;
            default:
                return state;
        }
    }

    _filteredTotalReducer(state, action) {
        if (!this._hasState(state) && this.config.items) {
            state = this.config.items.length;
        }
        switch (action.type) {
            case this._prefixAction(LOAD_SUCCESS):
                if (!Array.isArray(action.payload)) {
                    return action.payload.filteredTotal;
                }
                return action.payload.length;
            default:
                return state;
        }
    }

    createReducer() {
        return combineReducers({
            fetched: this._fetchedReducer.bind(this),
            params: this._paramsReducer.bind(this),
            error: this._errorReducer.bind(this),
            loading: this._loadingReducer.bind(this),
            pagination: this._paginationReducer.bind(this),
            filter: this._filterReducer.bind(this),
            sort: this._sortReducer.bind(this),
            items: this._itemsReducer.bind(this),
            total: this._totalReducer.bind(this),
            filteredTotal: this._filteredTotalReducer.bind(this)
        });
    }
}

export default createSetup;
