/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

import difference from 'lodash/difference';

export const LOAD_LIST = Symbol('LOAD_LIST');
export const ADD_CLUSTER = Symbol('ADD_CLUSTER');
export const ADD_CLUSTERS = Symbol('ADD_CLUSTERS');
export const REMOVE_CLUSTERS = Symbol('REMOVE_CLUSTERS');
export const UPDATE_CLUSTER = Symbol('UPDATE_CLUSTER');
export const UPSERT_CLUSTERS = Symbol('UPSERT_CLUSTERS');
export const ADD_CACHE = Symbol('ADD_CACHE');
export const UPDATE_CACHE = Symbol('UPDATE_CACHE');
export const UPSERT_CACHES = Symbol('UPSERT_CACHES');
export const REMOVE_CACHE = Symbol('REMOVE_CACHE');

import {
    REMOVE_CLUSTER_ITEMS_CONFIRMED
} from './store/actionTypes';

const defaults = {clusters: new Map(), caches: new Map(), spaces: new Map()};
const mapByID = (items) => {
    return Array.isArray(items) ? new Map(items.map((item) => [item._id, item])) : new Map(items);
};

export const reducer = (state = defaults, action) => {
    switch (action.type) {
        case LOAD_LIST: {
            return {
                clusters: mapByID(action.list.clusters),
                domains: mapByID(action.list.domains),
                caches: mapByID(action.list.caches),
                spaces: mapByID(action.list.spaces),
                plugins: mapByID(action.list.plugins)
            };
        }
        case ADD_CLUSTER: {
            return Object.assign({}, state, {
                clusters: new Map([...state.clusters.entries(), [action.cluster._id, action.cluster]])
            });
        }
        case ADD_CLUSTERS: {
            return Object.assign({}, state, {
                clusters: new Map([...state.clusters.entries(), ...action.clusters.map((c) => [c._id, c])])
            });
        }
        case REMOVE_CLUSTERS: {
            return Object.assign({}, state, {
                clusters: new Map([...state.clusters.entries()].filter(([id, value]) => !action.clusterIDs.includes(id)))
            });
        }
        case UPDATE_CLUSTER: {
            const id = action._id || action.cluster._id;
            return Object.assign({}, state, {
                // clusters: new Map(state.clusters).set(id, Object.assign({}, state.clusters.get(id), action.cluster))
                clusters: new Map(Array.from(state.clusters.entries()).map(([_id, cluster]) => {
                    return _id === id
                        ? [action.cluster._id || _id, Object.assign({}, cluster, action.cluster)]
                        : [_id, cluster];
                }))
            });
        }
        case UPSERT_CLUSTERS: {
            return action.clusters.reduce((state, cluster) => reducer(state, {
                type: state.clusters.has(cluster._id) ? UPDATE_CLUSTER : ADD_CLUSTER,
                cluster
            }), state);
        }
        case ADD_CACHE: {
            return Object.assign({}, state, {
                caches: new Map([...state.caches.entries(), [action.cache._id, action.cache]])
            });
        }
        case UPDATE_CACHE: {
            const id = action.cache._id;
            return Object.assign({}, state, {
                caches: new Map(state.caches).set(id, Object.assign({}, state.caches.get(id), action.cache))
            });
        }
        case UPSERT_CACHES: {
            return action.caches.reduce((state, cache) => reducer(state, {
                type: state.caches.has(cache._id) ? UPDATE_CACHE : ADD_CACHE,
                cache
            }), state);
        }
        case REMOVE_CACHE:
            return state;
        default:
            return state;
    }
};


export const RECEIVE_CLUSTER_EDIT = Symbol('RECEIVE_CLUSTER_EDIT');
export const RECEIVE_CACHE_EDIT = Symbol('RECEIVE_CACHE_EDIT');
export const RECEIVE_IGFSS_EDIT = Symbol('RECEIVE_IGFSS_EDIT');
export const RECEIVE_IGFS_EDIT = Symbol('RECEIVE_IGFS_EDIT');
export const RECEIVE_MODELS_EDIT = Symbol('RECEIVE_MODELS_EDIT');
export const RECEIVE_MODEL_EDIT = Symbol('RECEIVE_MODEL_EDIT');

export const editReducer = (state = {originalCluster: null}, action) => {
    switch (action.type) {
        case RECEIVE_CLUSTER_EDIT:
            return {
                ...state,
                originalCluster: action.cluster
            };
        case RECEIVE_CACHE_EDIT: {
            return {
                ...state,
                originalCache: action.cache
            };
        }
        case RECEIVE_IGFSS_EDIT:
            return {
                ...state,
                originalIGFSs: action.igfss
            };
        case RECEIVE_IGFS_EDIT: {
            return {
                ...state,
                originalIGFS: action.igfs
            };
        }
        case RECEIVE_MODELS_EDIT:
            return {
                ...state,
                originalModels: action.models
            };
        case RECEIVE_MODEL_EDIT: {
            return {
                ...state,
                originalModel: action.model
            };
        }
        default:
            return state;
    }
};

export const SHOW_CONFIG_LOADING = Symbol('SHOW_CONFIG_LOADING');
export const HIDE_CONFIG_LOADING = Symbol('HIDE_CONFIG_LOADING');
const loadingDefaults = {isLoading: false, loadingText: 'Loading...'};

export const loadingReducer = (state = loadingDefaults, action) => {
    switch (action.type) {
        case SHOW_CONFIG_LOADING:
            return {...state, isLoading: true, loadingText: action.loadingText};
        case HIDE_CONFIG_LOADING:
            return {...state, isLoading: false};
        default:
            return state;
    }
};

export const setStoreReducerFactory = (actionTypes) => (state = new Set(), action = {}) => {
    switch (action.type) {
        case actionTypes.SET:
            return new Set(action.items.map((i) => i._id));
        case actionTypes.RESET:
            return new Set();
        case actionTypes.UPSERT:
            return action.items.reduce((acc, item) => {acc.add(item._id); return acc;}, new Set(state));
        case actionTypes.REMOVE:
            return action.items.reduce((acc, item) => {acc.delete(item); return acc;}, new Set(state));
        default:
            return state;
    }
};

export const mapStoreReducerFactory = (actionTypes) => (state = new Map(), action = {}) => {
    switch (action.type) {
        case actionTypes.SET:
            return new Map(action.items.map((i) => [i._id, i]));
        case actionTypes.RESET:
            return new Map();
        case actionTypes.UPSERT:
            if (!action.items.length) return state;
            return action.items.reduce((acc, item) => {acc.set(item._id, item); return acc;}, new Map(state));
        case actionTypes.REMOVE:
            if (!action.ids.length) return state;
            return action.ids.reduce((acc, id) => {acc.delete(id); return acc;}, new Map(state));
        default:
            return state;
    }
};

export const mapCacheReducerFactory = (actionTypes) => {
    const mapStoreReducer = mapStoreReducerFactory(actionTypes);
    return (state = {value: mapStoreReducer(), pristine: true}, action) => {
        switch (action.type) {
            case actionTypes.SET:
            case actionTypes.REMOVE:
            case actionTypes.UPSERT:
                return {
                    value: mapStoreReducer(state.value, action),
                    pristine: false
                };
            case actionTypes.RESET:
                return {
                    value: mapStoreReducer(state.value, action),
                    pristine: true
                };
            default:
                return state;
        }
    };
};

export const basicCachesActionTypes = {
    SET: 'SET_BASIC_CACHES',
    RESET: 'RESET_BASIC_CACHES',
    LOAD: 'LOAD_BASIC_CACHES',
    UPSERT: 'UPSERT_BASIC_CACHES',
    REMOVE: 'REMOVE_BASIC_CACHES'
};

export const mapStoreActionTypesFactory = (NAME) => ({
    SET: `SET_${NAME}`,
    RESET: `RESET_${NAME}`,
    UPSERT: `UPSERT_${NAME}`,
    REMOVE: `REMOVE_${NAME}`
});

export const clustersActionTypes = mapStoreActionTypesFactory('CLUSTERS');
export const shortClustersActionTypes = mapStoreActionTypesFactory('SHORT_CLUSTERS');
export const cachesActionTypes = mapStoreActionTypesFactory('CACHES');
export const shortCachesActionTypes = mapStoreActionTypesFactory('SHORT_CACHES');
export const modelsActionTypes = mapStoreActionTypesFactory('MODELS');
export const shortModelsActionTypes = mapStoreActionTypesFactory('SHORT_MODELS');
export const igfssActionTypes = mapStoreActionTypesFactory('IGFSS');
export const shortIGFSsActionTypes = mapStoreActionTypesFactory('SHORT_IGFSS');

export const itemsEditReducerFactory = (actionTypes) => {
    const setStoreReducer = setStoreReducerFactory(actionTypes);
    const mapStoreReducer = mapStoreReducerFactory(actionTypes);
    return (state = {ids: setStoreReducer(), changedItems: mapStoreReducer()}, action) => {
        switch (action.type) {
            case actionTypes.SET:
                return action.state;
            case actionTypes.LOAD:
                return {
                    ...state,
                    ids: setStoreReducer(state.ids, {...action, type: actionTypes.UPSERT})
                };
            case actionTypes.RESET:
            case actionTypes.UPSERT:
                return {
                    ids: setStoreReducer(state.ids, action),
                    changedItems: mapStoreReducer(state.changedItems, action)
                };
            case actionTypes.REMOVE:
                return {
                    ids: setStoreReducer(state.ids, {type: action.type, items: action.ids}),
                    changedItems: mapStoreReducer(state.changedItems, action)
                };
            default:
                return state;
        }
    };
};

export const editReducer2 = (state = editReducer2.getDefaults(), action) => {
    switch (action.type) {
        case 'SET_EDIT':
            return action.state;
        case 'EDIT_CLUSTER': {
            return {
                ...state,
                changes: {
                    ...['caches', 'models', 'igfss'].reduce((a, t) => ({
                        ...a,
                        [t]: {
                            ids: action.cluster ? action.cluster[t] || [] : [],
                            changedItems: []
                        }
                    }), state.changes),
                    cluster: action.cluster
                }
            };
        }
        case 'RESET_EDIT_CHANGES': {
            return {
                ...state,
                changes: {
                    ...['caches', 'models', 'igfss'].reduce((a, t) => ({
                        ...a,
                        [t]: {
                            ids: state.changes.cluster ? state.changes.cluster[t] || [] : [],
                            changedItems: []
                        }
                    }), state.changes),
                    cluster: {...state.changes.cluster}
                }
            };
        }
        case 'UPSERT_CLUSTER': {
            return {
                ...state,
                changes: {
                    ...state.changes,
                    cluster: action.cluster
                }
            };
        }
        case 'UPSERT_CLUSTER_ITEM': {
            const {itemType, item} = action;
            return {
                ...state,
                changes: {
                    ...state.changes,
                    [itemType]: {
                        ids: state.changes[itemType].ids.filter((_id) => _id !== item._id).concat(item._id),
                        changedItems: state.changes[itemType].changedItems.filter(({_id}) => _id !== item._id).concat(item)
                    }
                }
            };
        }
        case REMOVE_CLUSTER_ITEMS_CONFIRMED: {
            const {itemType, itemIDs} = action;
            return {
                ...state,
                changes: {
                    ...state.changes,
                    [itemType]: {
                        ids: state.changes[itemType].ids.filter((_id) => !itemIDs.includes(_id)),
                        changedItems: state.changes[itemType].changedItems.filter(({_id}) => !itemIDs.includes(_id))
                    }
                }
            };
        }
        default: return state;
    }
};
editReducer2.getDefaults = () => ({
    changes: ['caches', 'models', 'igfss'].reduce((a, t) => ({...a, [t]: {ids: [], changedItems: []}}), {cluster: null})
});

export const refsReducer = (refs) => (state, action) => {
    switch (action.type) {
        case 'ADVANCED_SAVE_COMPLETE_CONFIGURATION': {
            const newCluster = action.changedItems.cluster;
            const oldCluster = state.clusters.get(newCluster._id) || {};
            const val = Object.keys(refs).reduce((state, ref) => {
                if (!state || !state[refs[ref].store].size) return state;

                const addedSources = new Set(difference(newCluster[ref], oldCluster[ref] || []));
                const removedSources = new Set(difference(oldCluster[ref] || [], newCluster[ref]));
                const changedSources = new Map(action.changedItems[ref].map((m) => [m._id, m]));

                const targets = new Map();
                const maybeTarget = (id) => {
                    if (!targets.has(id)) targets.set(id, {[refs[ref].at]: {add: new Set(), remove: new Set()}});
                    return targets.get(id);
                };

                [...state[refs[ref].store].values()].forEach((target) => {
                    target[refs[ref].at]
                    .filter((sourceID) => removedSources.has(sourceID))
                    .forEach((sourceID) => maybeTarget(target._id)[refs[ref].at].remove.add(sourceID));
                });
                [...addedSources.values()].forEach((sourceID) => {
                    (changedSources.get(sourceID)[refs[ref].store] || []).forEach((targetID) => {
                        maybeTarget(targetID)[refs[ref].at].add.add(sourceID);
                    });
                });
                action.changedItems[ref].filter((s) => !addedSources.has(s._id)).forEach((source) => {
                    const newSource = source;
                    const oldSource = state[ref].get(source._id);
                    const addedTargets = difference(newSource[refs[ref].store], oldSource[refs[ref].store]);
                    const removedCaches = difference(oldSource[refs[ref].store], newSource[refs[ref].store]);
                    addedTargets.forEach((targetID) => {
                        maybeTarget(targetID)[refs[ref].at].add.add(source._id);
                    });
                    removedCaches.forEach((targetID) => {
                        maybeTarget(targetID)[refs[ref].at].remove.add(source._id);
                    });
                });
                const result = [...targets.entries()]
                    .filter(([targetID]) => state[refs[ref].store].has(targetID))
                    .map(([targetID, changes]) => {
                        const target = state[refs[ref].store].get(targetID);
                        return [
                            targetID,
                            {
                                ...target,
                                [refs[ref].at]: target[refs[ref].at]
                                    .filter((sourceID) => !changes[refs[ref].at].remove.has(sourceID))
                                    .concat([...changes[refs[ref].at].add.values()])
                            }
                        ];
                    });

                return result.length
                    ? {
                        ...state,
                        [refs[ref].store]: new Map([...state[refs[ref].store].entries()].concat(result))
                    }
                    : state;
            }, state);
            return val;
        }
        default:
            return state;
    }
};
