/**
 * This implements the global entities reducer.
 */

// NPM imports
import Immutable from "immutable";

// Local imports
import { createReducer } from "../utils";

// Models
import { stateRecord } from "../models/entities";

// Actions
import {
    API_REQUEST,
    API_FAILURE,
    PUSH_ENTITIES,
    INCREMENT_REFCOUNT,
    DECREMENT_REFCOUNT,
    INVALIDATE_STORE,
} from "../actions";


/**
 * Helper methods
 */

/**
 * Update the reference counter for a given item.
 *
 * Do not do any garbage collection.
 *
 * @param   state       The state object to update.
 * @param   keyPath     The keyPath to update, from the refCount key.
 * @param   incr        The increment (or decrement) for the reference counter.
 *
 * @return  An updated state.
 */
function updateRefCount(state, keyPath, incr) {
    // Prepend refCounts to keyPath
    const refCountKeyPath = Array.concat(["refCounts"], keyPath);
    // Get updated value
    let newRefCount = state.getIn(refCountKeyPath) + incr;
    if (isNaN(newRefCount)) {
        // If NaN, reference does not exist, so set it to ±1
        newRefCount = Math.sign(incr);
    }
    // Update state
    return state.setIn(refCountKeyPath, newRefCount);
}


/**
 * Update the reference counter of a given entity, taking into account the
 * nested objects.
 *
 * Do not do any garbage collection.
 *
 * @param   state       The state object to update.
 * @param   itemName    The type of the entity object.
 * @param   id          The id of the entity.
 * @param   entity      The entity object, as Immutable.
 * @param   incr        The increment (or decrement) for the reference counter.
 *
 * @return  An updated state.
 */
function updateEntityRefCount(state, itemName, id, entity, incr) {
    let newState = state;
    let albums = null;
    let tracks = null;
    switch (itemName) {
        case "artist":
            // Update artist refCount
            newState = updateRefCount(newState, ["artist", id], incr);
            // Update nested albums refCount
            albums = entity.get("albums");
            if (Immutable.List.isList(albums)) {
                albums.forEach(function (id) {
                    newState = updateRefCount(newState, ["album", id], incr);
                });
            }
            // Update nested tracks refCount
            tracks = entity.get("songs");
            if (Immutable.List.isList(tracks)) {
                tracks.forEach(function (id) {
                    newState = updateRefCount(newState, ["song", id], incr);
                });
            }
            break;
        case "album":
            // Update album refCount
            newState = updateRefCount(newState, ["album", id], incr);
            // Update nested artist refCount
            newState = updateRefCount(newState, ["artist", entity.get("artist")], incr);
            // Update nested tracks refCount
            tracks = entity.get("tracks");
            if (Immutable.List.isList(tracks)) {
                tracks.forEach(function (id) {
                    newState = updateRefCount(newState, ["song", id], incr);
                });
            }
            break;
        case "song":
            // Update track refCount
            newState = updateRefCount(newState, ["song", id], incr);
            // Update nested artist refCount
            newState = updateRefCount(newState, ["artist", entity.get("artist")], incr);
            // Update nested album refCount
            newState = updateRefCount(newState, ["album", entity.get("album")], incr);
            break;
        default:
            // Just update the entity, no nested entities
            newState = updateRefCount(newState, [itemName, id], incr);
            break;
    }
    return newState;
}


/**
 *
 */
function garbageCollection(state) {
    let newState = state;
    state.refCounts.forEach(function (refCounts, itemName) {
        refCounts.forEach(function (refCount, id) {
            if (refCount < 1) {
                // Garbage collection
                newState = newState.deleteIn(["entities", itemName, id]);
                newState = newState.deleteIn(["refCounts", itemName, id]);
            }
        });
    });
    return newState;
}


/**
 * Initial state
 */
var initialState = new stateRecord();


/**
 * Reducer
 */
export default createReducer(initialState, {
    [API_REQUEST]: (state) => {
        return (
            state
            .set("isFetching", true)
            .set("error", null)
        );
    },
    [API_FAILURE]: (state, payload) => {
        return (
            state
            .set("isFetching", false)
            .set("error", payload.error)
        );
    },
    [PUSH_ENTITIES]: (state, payload) => {
        let newState = state;

        // Unset error and isFetching
        newState = state.set("isFetching", false).set("error", payload.error);

        // Merge entities
        newState = newState.mergeDeepIn(["entities"], payload.entities);

        // Increment reference counter
        payload.refCountType.forEach(function (itemName) {
            const entities = payload.entities[itemName];
            for (let id in entities) {
                const entity = newState.getIn(["entities", itemName, id]);
                newState = updateEntityRefCount(newState, itemName, id, entity, 1);
            }
        });

        return newState;
    },
    [INCREMENT_REFCOUNT]: (state, payload) => {
        let newState = state;

        // Increment reference counter
        for (let itemName in payload.entities) {
            const entities = payload.entities[itemName];
            entities.forEach(function (id) {
                const entity = newState.getIn(["entities", itemName, id]);
                newState = updateEntityRefCount(newState, itemName, id, entity, 1);
            });
        }

        return newState;
    },
    [DECREMENT_REFCOUNT]: (state, payload) => {
        let newState = state;

        // Decrement reference counter
        for (let itemName in payload.entities) {
            const entities = payload.entities[itemName];
            entities.forEach(function (id) {
                const entity = newState.getIn(["entities", itemName, id]);
                newState = updateEntityRefCount(newState, itemName, id, entity, -1);
            });
        }

        // Perform garbage collection
        newState = garbageCollection(newState);

        return newState;
    },
    [INVALIDATE_STORE]: () => {
        return new stateRecord();
    },
});
