/**
 * This file implements actions to fetch and load data from the API.
 */

// NPM imports
import { normalize, arrayOf } from "normalizr";
import humps from "humps";

// Other actions
import { CALL_API } from "../middleware/api";
import { pushEntities } from "./entities";

// Models
import { artist, song, album } from "../models/api";

// Constants
export const DEFAULT_LIMIT = 32;  /** Default max number of elements to retrieve. */


/**
 * This function wraps around an API action to generate actions trigger
 * functions to load items etc.
 *
 * @param   action          API action.
 * @param   requestType     Action type to trigger on request.
 * @param   successType     Action type to trigger on success.
 * @param   failureType     Action type to trigger on failure.
 */
export default function (action, requestType, successType, failureType) {
    /** Get the name of the item associated with action */
    const itemName = action.rstrip("s");

    /**
     * Normalizr helper to normalize API response.
     *
     * @param   jsonData    The JS object returned by the API.
     * @return  A normalized object.
     */
    const _normalizeAPIResponse = function (jsonData) {
        return normalize(
            jsonData,
            {
                artist: arrayOf(artist),
                album: arrayOf(album),
                song: arrayOf(song),
            },
            {
                // Use custom assignEntity function to delete useless fields
                assignEntity: function (output, key, value) {
                    if (key == "sessionExpire") {
                        delete output.sessionExpire;
                    } else {
                        output[key] = value;
                    }
                },
            }
        );
    };

    /**
     * Callback on successful fetch of paginated items
     *
     * @param   jsonData    JS object returned from the API.
     * @param   pageNumber  Number of the page that was fetched.
     */
    const fetchPaginatedItemsSuccess = function (jsonData, pageNumber, limit) {
        const totalCount = jsonData.totalCount;
        jsonData = _normalizeAPIResponse(jsonData);

        // Compute the total number of pages
        const nPages = Math.ceil(totalCount / limit);

        // Return success actions
        return [
            // Action for the global entities store
            pushEntities(jsonData.entities, [itemName]),
            // Action for the paginated store
            {
                type: successType,
                payload: {
                    type: itemName,
                    result: jsonData.result[itemName],
                    nPages: nPages,
                    currentPage: pageNumber,
                },
            },
        ];
    };

    /**
     * Callback on successful fetch of single item
     *
     * @param   jsonData    JS object returned from the API.
     * @param   pageNumber  Number of the page that was fetched.
     */
    const fetchItemSuccess = function (jsonData) {
        jsonData = _normalizeAPIResponse(jsonData);

        return pushEntities(jsonData.entities, [itemName]);
    };

    /** Callback on request */
    const fetchItemsRequest = function () {
        // Return a request type action
        return {
            type: requestType,
            payload: {
            },
        };
    };

    /**
     * Callback on failed fetch
     *
     * @param   error   An error object, either a string or an i18nError
     *                  object.
     */
    const fetchItemsFailure = function (error) {
        // Return a failure type action
        return {
            type: failureType,
            payload: {
                error: error,
            },
        };
    };

    /**
     * Method to trigger a fetch of items.
     *
     * @param   endpoint    Ampache server base URL.
     * @param   username    Username to use for API request.
     * @param   filter      An eventual filter to apply (mapped to API filter
     *                      param)
     * @param   pageNumber  Number of the page to fetch items from.
     * @param   limit       Max number of items to fetch.
     * @param   include     [Optional] A list of includes to return as well
     *                      (mapped to API include param)
     *
     * @return  A CALL_API action to fetch the specified items.
     */
    const fetchItems = function (endpoint, username, passphrase, filter, pageNumber, limit, include = []) {
        // Compute offset in number of items from the page number
        const offset = (pageNumber - 1) * DEFAULT_LIMIT;
        // Set extra params for pagination
        let extraParams = {
            offset: offset,
            limit: limit,
        };

        // Handle filter
        if (filter) {
            extraParams.filter = filter;
        }

        // Handle includes
        if (include && include.length > 0) {
            extraParams.include = include;
        }

        // Return a CALL_API action
        return {
            type: CALL_API,
            payload: {
                endpoint: endpoint,
                dispatch: [
                    fetchItemsRequest,
                    null,
                    fetchItemsFailure,
                ],
                action: action,
                auth: passphrase,
                username: username,
                extraParams: extraParams,
            },
        };
    };

    /**
     * High level method to load paginated items from the API wihtout dealing about credentials.
     *
     * @param   pageNumber  [Optional] Number of the page to fetch items from.
     * @param   filter      [Optional] An eventual filter to apply (mapped to
     *                      API filter param)
     * @param   include     [Optional] A list of includes to return as well
     *                      (mapped to API include param)
     *
     * Dispatches the CALL_API action to fetch these items.
     */
    const loadPaginatedItems = function ({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
        return (dispatch, getState) => {
            // Get credentials from the state
            const { auth } = getState();
            // Get the fetch action to dispatch
            const fetchAction = fetchItems(
                auth.endpoint,
                auth.username,
                auth.token.token,
                filter,
                pageNumber,
                limit,
                include
            );
            // Set success callback
            fetchAction.payload.dispatch[1] = (
                jsonData => dispatch => {
                    // Dispatch all the necessary actions
                    const actions = fetchPaginatedItemsSuccess(jsonData, pageNumber, limit);
                    actions.map(action => dispatch(action));
                }
            );
            // Dispatch action
            dispatch(fetchAction);
        };
    };

    /**
     * High level method to load a single item from the API wihtout dealing about credentials.
     *
     * @param   filter      The filter to apply (mapped to API filter param)
     * @param   include     [Optional] A list of includes to return as well
     *                      (mapped to API include param)
     *
     * Dispatches the CALL_API action to fetch this item.
     */
    const loadItem = function ({ filter = null, include = [] } = {}) {
        return (dispatch, getState) => {
            // Get credentials from the state
            const { auth } = getState();
            // Get the action to dispatch
            const fetchAction = fetchItems(
                auth.endpoint,
                auth.username,
                auth.token.token,
                filter,
                1,
                DEFAULT_LIMIT,
                include
            );
            // Set success callback
            fetchAction.payload.dispatch[1] = (
                jsonData => dispatch => {
                    dispatch(fetchItemSuccess(jsonData));
                }
            );
            // Dispatch action
            dispatch(fetchAction);
        };
    };

    // Remap the above methods to methods including item name
    var returned = {};
    const camelizedAction = humps.pascalize(action);
    returned["loadPaginated" + camelizedAction] = loadPaginatedItems;
    returned["load" + camelizedAction.rstrip("s")] = loadItem;
    return returned;
}
