import {hierarchy as hierarchyFunctions, 
    common, getTemplateApi } from "budibase-core";
import {filter, cloneDeep, sortBy, map, last,
    find, isEmpty, groupBy, reduce} from "lodash/fp";
import {chain, getNode, validate,
    constructHierarchy, templateApi} from "../common/core";
import {writable} from "svelte/store";

export const getStore = () => {

    const initial = {
        apps:[],
        appname:"",
        hierarchy: {},
        actions: [],
        triggers: [],
        currentNodeIsNew: false,
        errors: [],
        activeNav: "database",
        hasAppPackage: false,
        accessLevels: [],
        currentNode: null};

    const store = writable(initial);

    store.initialise = initialise(store, initial);
    store.newChildRecord = newRecord(store, false);
    store.newRootRecord = newRecord(store, true);
    store.selectExistingNode = selectExistingNode(store);
    store.newChildIndex = newIndex(store, false);
    store.newRootIndex = newIndex(store, true);
    store.saveCurrentNode = saveCurrentNode(store);
    store.importAppDefinition = importAppDefinition(store);
    store.deleteCurrentNode = deleteCurrentNode(store);
    store.saveField = saveField(store);
    store.deleteField = deleteField(store);
    store.saveAction = saveAction(store);
    store.deleteAction = deleteAction(store);
    store.saveTrigger = saveTrigger(store);
    store.deleteTrigger = deleteTrigger(store);
    store.saveLevel = saveLevel(store);
    store.deleteLevel = deleteLevel(store);
    store.setActiveNav = setActiveNav(store);
    return store;
} 

export default getStore;

const initialise = (store, initial) => async () => {

    const appname = window.location.hash 
                ? last(window.location.hash.substr(1).split("/"))
                : "";

    if(!appname) {
        initial.apps = await fetch(`/_builder/api/apps`)
                             .then(r => r.json());
        initial.hasAppPackage = false;
        store.set(initial);
        return initial;
    }

    const pkg = await fetch(`/_builder/api/${appname}/appPackage`)
                     .then(r => r.json());

    initial.appname = appname;
    initial.hasAppPackage = true;
    initial.hierarchy = pkg.appDefinition.hierarchy;
    initial.accessLevels = pkg.accessLevels;
    initial.actions = reduce((arr, action) => {
        arr.push(action);
        return arr;
    })(pkg.appDefinition.actions, []);
    initial.triggers = pkg.appDefinition.triggers;

    if(!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
        initial.hierarchy = constructHierarchy(initial.hierarchy);
        const shadowHierarchy = createShadowHierarchy(initial.hierarchy);
        if(initial.currentNode !== null)
            initial.currentNode = getNode(
                shadowHierarchy, initial.currentNode.nodeId
            );
    }
    store.set(initial);
    return initial;
}

const newRecord = (store, useRoot) => () => {
    store.update(s => {
        s.currentNodeIsNew = true;
        const shadowHierarchy = createShadowHierarchy(s.hierarchy);
        parent = useRoot ? shadowHierarchy
                 : getNode(
                    shadowHierarchy, 
                    s.currentNode.nodeId);
        s.errors = [];
        s.currentNode = templateApi(shadowHierarchy)
                         .getNewRecordTemplate(parent, "", true);
        return s;
    });
}


const selectExistingNode = (store) => (nodeId) => {
    store.update(s => {
        const shadowHierarchy = createShadowHierarchy(s.hierarchy);
        s.currentNode = getNode(
            shadowHierarchy, nodeId
        );
        s.currentNodeIsNew = false;
        s.errors = [];
        return s;
    })
}

const newIndex = (store, useRoot) => () => {
    store.update(s => {
        s.currentNodeIsNew = true;
        s.errors = [];
        const shadowHierarchy = createShadowHierarchy(s.hierarchy);
        parent = useRoot ? shadowHierarchy
                 : getNode(
                    shadowHierarchy, 
                    s.currentNode.nodeId);

        s.currentNode = templateApi(shadowHierarchy)
                         .getNewIndexTemplate(parent);
        return s;
    });
}

const saveCurrentNode = (store) => () => {
    store.update(s => {

        const errors = validate.node(s.currentNode);
        s.errors = errors;
        if(errors.length > 0) {
            return s;
        }

        const parentNode = getNode(
            s.hierarchy, s.currentNode.parent().nodeId);

        const existingNode = getNode(
            s.hierarchy, s.currentNode.nodeId);

        let index = parentNode.children.length;
        if(!!existingNode) {
            // remove existing
            index = existingNode.parent().children.indexOf(existingNode);
            existingNode.parent().children = chain(existingNode.parent().children, [
                filter(c => c.nodeId !== existingNode.nodeId)
            ]);
        }

        // should add node into existing hierarchy
        const cloned = cloneDeep(s.currentNode);
        templateApi(s.hierarchy).constructNode(
            parentNode, 
            cloned
        );

        const newIndexOfchild = child => {
            if(child === cloned) return index;
            const currentIndex = parentNode.children.indexOf(child);
            return currentIndex >= index ? currentIndex + 1 : currentIndex;
        }

        parentNode.children = chain(parentNode.children, [
            sortBy(newIndexOfchild)
        ]);

        s.currentNodeIsNew = false;
        
        savePackage(store, s);

        return s;
    });
}

const importAppDefinition = store => appDefinition => {
    store.update(s => {
        s.hierarchy = appDefinition.hierarchy;
        s.currentNode = appDefinition.hierarchy.children.length > 0
                         ? appDefinition.hierarchy.children[0] 
                         : null;
        s.actions = appDefinition.actions;
        s.triggers = appDefinition.triggers;
        s.currentNodeIsNew = false; 
        return s;
    });
} 

const deleteCurrentNode = store => () => {
    store.update(s => {
        const nodeToDelete = getNode(s.hierarchy, s.currentNode.nodeId);
        s.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent())
                         ? find(n => n != s.currentNode)
                               (s.hierarchy.children)
                         : nodeToDelete.parent();
        if(hierarchyFunctions.isRecord(nodeToDelete)) {
            nodeToDelete.parent().children = filter(c => c.nodeId !== nodeToDelete.nodeId)
                                                   (nodeToDelete.parent().children);
        } else {
            nodeToDelete.parent().indexes = filter(c => c.nodeId !== nodeToDelete.nodeId)
                                                   (nodeToDelete.parent().indexes);
        }
        s.errors = [];
        savePackage(store, s);
        return s;
    });
}

const saveField = databaseStore => (field) => {
    databaseStore.update(db => {
        db.currentNode.fields = filter(f => f.name !== field.name)
                                      (db.currentNode.fields);
            
        templateApi(db.hierarchy).addField(db.currentNode, field);
        return db;
    });
}


const deleteField = databaseStore => field => {
    databaseStore.update(db => {
        db.currentNode.fields = filter(f => f.name !== field.name)
                                      (db.currentNode.fields);

        return db;
    });
}


const saveAction = store => (newAction, isNew, oldAction=null) => {
    store.update(s => {

        const existingAction = isNew 
                               ? null
                               : find(a => a.name === oldAction.name)(s.actions);
            
        if(existingAction) {
            s.actions = chain(s.actions, [
                map(a => a === existingAction ? newAction : a)
            ]);
        } else {
            s.actions.push(newAction);
        }
        savePackage(store, s);
        return s;
    });
}

const deleteAction  = store => action => {
    store.update(s => {
        s.actions = filter(a => a.name !== action.name)(s.actions);
        savePackage(store, s);
        return s;
    });
}

const saveTrigger = store => (newTrigger, isNew, oldTrigger=null) => {
    store.update(s => {

        const existingTrigger = isNew 
                               ? null
                               : find(a => a.name === oldTrigger.name)(s.triggers);
            
        if(existingTrigger) {
            s.triggers = chain(s.triggers, [
                map(a => a === existingTrigger ? newTrigger : a)
            ]);
        } else {
            s.triggers.push(newTrigger);
        }
        savePackage(store, s);
        return s;
    });
}

const deleteTrigger  = store => trigger => {
    store.update(s => {
        s.triggers = filter(t => t.name !== trigger.name)(s.triggers);
        return s;
    });
}

const saveLevel = store => (newLevel, isNew, oldLevel=null) => {
    store.update(s => {

        const existingLevel = isNew 
                               ? null
                               : find(a => a.name === oldLevel.name)(s.accessLevels);
            
        if(existingLevel) {
            s.accessLevels = chain(s.accessLevels, [
                map(a => a === existingLevel ? newLevel : a)
            ]);
        } else {
            s.accessLevels.push(newLevel);
        }
        savePackage(store, s);
        return s;
    });
}

const deleteLevel = store => level => {
    store.update(s => {
        s.accessLevels = filter(t => t.name !== level.name)(s.accessLevels);
        savePackage(store, s);
        return s;
    });
}

const setActiveNav = databaseStore => navName => {
    databaseStore.update(db => {
        db.activeNav = navName;
        return db;
    });
}

const createShadowHierarchy = hierarchy => 
    constructHierarchy(JSON.parse(JSON.stringify(hierarchy)));

const savePackage = (store, db) => {

    const appDefinition = {
        hierarchy:db.hierarchy,
        triggers:db.triggers,
        actions: groupBy("name")(db.actions)
    };

    const data = {
        appDefinition,
        accessLevels:db.accessLevels
    }

    fetch(`/_builder/api/${db.appname}/appPackage`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(data), 
    });
}
