import _ from 'underscore';
import $ from 'jquery';
import ko from 'knockout';
import koMapping from 'knockout-mapping';
import arches from 'arches';
import GraphModel from 'models/graph';
import CardViewModel from 'viewmodels/card';
import ProvisionalTileViewModel from 'viewmodels/provisional-tile';
import AlertViewModel from 'viewmodels/alert';
import uuid from 'uuid';
import Cookies from 'js-cookie';
import workflowComponentAbstractTemplate from 'templates/views/components/workflows/workflow-component-abstract.htm';   


function NonTileBasedComponent() {
    var self = this;
        

    this.initialize = function() {
        self.loading(false);
    };

    this.save = function() {
        self.complete(false);
        self.saving(true);

        self.setToWorkflowHistory('value', self.value()).then(() => {
            self.savedData(self.value());
            self.complete(true);
            self.saving(false);
        });
    };

    this.reset = function() {
        self.value(self.savedData() ? self.savedData() : null);
    };

    this.initialize();
}


function TileBasedComponent() {
    var self = this;
        

    this.tile = ko.observable();
    this.tiles = ko.observable();

    this.card = ko.observable();
    this.topCards = ko.observable();

    this.loadData = function(loadedData) {
        let data;
        if (!Array.isArray(loadedData)) {
            data = [loadedData];
        }
        else {
            data = loadedData;
        }
        
        /* a flat object of the previously saved data for all tiles */ 
        var tileDataLookup = data.reduce(function(acc, componentData) {
            var parsedTileData = componentData.data || JSON.parse(componentData.tileData);

            Object.keys(parsedTileData).forEach(function(key) {
                acc[key] = parsedTileData[key];
            });

            return acc;
        }, {});

        self.tiles().forEach(function(tile) {
            /* force the value of current tile data observables */
            Object.keys(tile.data).forEach(function(key) {
                if (ko.isObservable(tile.data[key])) {
                    tile.data[key](tileDataLookup[key]);
                }
            });
            tile._tileData(koMapping.toJSON(tile.data));
            
            data.forEach(function(datum){                    
                if (datum.tileData) {
                    if (JSON.stringify(Object.keys(koMapping.toJS(tile.data)).sort()) 
                        === JSON.stringify(Object.keys(JSON.parse(datum.tileData)).sort())) {
                        tile.nodegroup_id = datum.nodegroupId;
                        tile.tileid = datum.tileId;
                        tile.resourceinstance_id = datum.resourceInstanceId;        
                    }
                }
            });
        });
    };

    this.initialize = function() {
        if (self.componentData.tilesManaged === "one") {
            self.tile.subscribe(function(tile) {
                if (!self.tiles()) {
                    self.tiles([tile]);
                } 
            });

            self.tiles.subscribe(function(tiles) {
                if (tiles && !self.saving()) {
                    if (self.savedData()) {  /* if the refresh after tile save */
                        self.loadData(self.savedData());
                    }
                    else if (self.previouslyPersistedComponentData) { /* if previously saved data */
                        self.loadData(self.previouslyPersistedComponentData);
                    }
                    
                }
            });
        }

        $.getJSON(( arches.urls.api_card + this.getCardResourceIdOrGraphId() ), function(data) {
            var handlers = {
                'after-update': [],
                'tile-reset': []
            };
            var displayname = ko.observable(data.displayname);
            var createLookup = function(list, idKey) {
                return _.reduce(list, function(lookup, item) {
                    lookup[item[idKey]] = item;
                    return lookup;
                }, {});
            };

            self.reviewer = data.userisreviewer;
            self.provisionalTileViewModel = new ProvisionalTileViewModel({
                tile: self.tile,
                reviewer: data.userisreviewer
            });

            var graphModel = new GraphModel({
                data: {
                    nodes: data.nodes,
                    nodegroups: data.nodegroups,
                    edges: []
                },
                datatypes: data.datatypes
            });

            self.graphModel = graphModel;

            self.topCards = _.filter(data.cards, function(card) {
                var nodegroup = _.find(data.nodegroups, function(group) {
                    return group.nodegroupid === card.nodegroup_id;
                });
                return !nodegroup || !nodegroup.parentnodegroup_id;
            }).map(function(card) {
                self.componentData.parameters.nodegroupid = self.componentData.parameters.nodegroupid || card.nodegroup_id;
                return new CardViewModel({
                    card: card,
                    graphModel: graphModel,
                    tile: null,
                    resourceId: self.resourceId,
                    displayname: displayname,
                    handlers: handlers,
                    cards: data.cards,
                    tiles: data.tiles,
                    provisionalTileViewModel: self.provisionalTileViewModel,
                    cardwidgets: data.cardwidgets,
                    userisreviewer: data.userisreviewer,
                    loading: self.loading
                });
            });

            self.card.subscribe(function(card){
                if (ko.unwrap(card.widgets) && self.componentData.parameters.hiddenNodes) {
                    card.widgets().forEach(function(widget){
                        if (self.componentData.parameters.hiddenNodes.indexOf(widget.node_id()) > -1) {
                            widget.visible(false);
                        }
                    });
                }
            });

            self.topCards.forEach(function(topCard) {
                topCard.topCards = self.topCards;
            });

            self.widgetLookup = createLookup(
                data.widgets,
                'widgetid'
            );
            self.cardComponentLookup = createLookup(
                data['card_components'],
                'componentid'
            );
            self.nodeLookup = createLookup(
                graphModel.get('nodes')(),
                'nodeid'
            );
            self.on = function(eventName, handler) {
                if (handlers[eventName]) {
                    handlers[eventName].push(handler);
                }
            };

            self.flattenTree(self.topCards, []).forEach(function(item) {
                if (item.constructor.name === 'CardViewModel' && item.nodegroupid === ko.unwrap(self.componentData.parameters.nodegroupid)) {
                    if (ko.unwrap(self.componentData.parameters.parenttileid) && item.parent && ko.unwrap(self.componentData.parameters.parenttileid) !== item.parent.tileid) {
                        return;
                    }
                    if (self.customCardLabel) item.model.name(ko.unwrap(self.customCardLabel));
                    self.card(item);
                    if (ko.unwrap(self.componentData.parameters.tileid)) {
                        ko.unwrap(item.tiles).forEach(function(tile) {
                            if (tile.tileid === ko.unwrap(self.componentData.parameters.tileid)) {
                                self.tile(tile);
                            }
                        });
                    } else if (ko.unwrap(self.componentData.parameters.createTile) !== false) {
                        self.tile(item.getNewTile());
                    }
                }
            });

            self.componentData.parameters.card = self.card();
            self.componentData.parameters.tile = self.tile();
            self.componentData.parameters.loading = self.loading;
            self.componentData.parameters.provisionalTileViewModel = self.provisionalTileViewModel;
            self.componentData.parameters.reviewer = data.userisreviewer;
            self.componentData.parameters.dirty = self.dirty;
            self.componentData.parameters.save = self.save;
            self.componentData.parameters.tiles = self.tiles;

            self.loading(false);
        });
    };

    this.onSaveSuccess = function(savedData) {  // LEGACY -- DO NOT USE
        if (!(savedData instanceof Array)) { savedData = [savedData]; }
        
        self.savedData(savedData.map(function(savedDatum) {
            return {
                tileData: savedDatum._tileData(),
                tileId: savedDatum.tileid,
                nodegroupId: savedDatum.nodegroup_id,
                resourceInstanceId: savedDatum.resourceinstance_id,
            };
        }));
        
        self.saving(false);
        self.complete(true);
    };

    this.reset = function() {
        self.tiles().forEach(function(tile) {
            tile.reset();
        });
    };

    this.flattenTree = function(parents, flatList) {
        _.each(ko.unwrap(parents), function(parent) {
            flatList.push(parent);
            var childrenKey = parent.tiles ? 'tiles' : 'cards';
            self.flattenTree(
                ko.unwrap(parent[childrenKey]),
                flatList
            );
        });
        return flatList;
    };

    this.getTiles = function(nodegroupId, tileId) {
        var tiles = [];
        this.flattenTree(self.topCards, []).forEach(function(item) {
            if (item.constructor.name === 'CardViewModel' && item.nodegroupid === nodegroupId) {
                tiles = tiles.concat(ko.unwrap(item.tiles));
            }
        });
        if (tileId) {
            tiles = tiles.filter(function(tile) {
                return tile.tileid === tileId;
            });
        }

        return tiles;
    };

    this.initialize();
}

function AbstractCardAdapter() {  // CURRENTLY IN DEVLEOPMENT, USE AT YOUR OWN RISK!
    var self = this;
    this.cardinality = ko.observable();

    this.initialize = function() {
        self.loading(true);
        
        $.getJSON(( arches.urls.api_nodegroup(self.componentData.parameters['nodegroupid']) ), function(nodegroupData) {
            self.cardinality(nodegroupData.cardinality);

            const resourceInstanceDataPromise = new Promise(function(resolve, _reject) {
                const resourceInstanceId = self.componentData.parameters['resourceid'];

                if (resourceInstanceId) {
                    $.getJSON(
                        ( arches.urls.resource + `/${resourceInstanceId}/tiles?nodeid=${self.componentData.parameters['nodegroupid']}`),
                        function(data) {
                            resolve(data);
                        }
                    );
                }
                else {
                    resolve(null);
                }
            });

            resourceInstanceDataPromise.then( function(data) {
                if (self.cardinality() === '1') {
                    if (data && data['tiles'].length) {
                        self.componentData.parameters['tileid'] = data['tiles'][0]['tileid'];
                        self.complete(true);
                    }
                    TileBasedComponent.apply(self);
                }
                else if (self.cardinality() === 'n') {
                    MultipleTileBasedComponent.apply(self);
                    if (data && data['tiles'].length) {
                        self.complete(true);
                    }
                    
                    self.card.subscribe(function(card) {
                        if (!card.selected()) {
                            card.selected(card.tiles().length);
                        }
                    });

                    self.onSaveSuccess = function(savedData) {  // LEGACY -- DO NOT USE
                        if (!(savedData instanceof Array)) { savedData = [savedData]; }

                        let previouslySavedData = self.savedData();
                        if (!previouslySavedData) {
                            previouslySavedData = [];
                        }

                        self.savedData(
                            previouslySavedData.concat(
                                savedData.map(function(savedDatum) {
                                    return {
                                        tileData: savedDatum._tileData(),
                                        tileId: savedDatum.tileid,
                                        nodegroupId: savedDatum.nodegroup_id,
                                        resourceInstanceId: savedDatum.resourceinstance_id,
                                    };
                                })
                            )
                        );

                        self.value(self.savedData());

                        self.componentData.parameters.dirty(false);
                        self.card().getNewTile(true);  // `true` is forceNewTile
                        self.card().selected(true);
                        self.dirty(false);
                        self.saving(false);
                        self.complete(true);

                        /**
                         * TODO: this is a hack to get around previous data autofilling forms when creating a new tile in cardinality n cards
                         * It should be removed when we're able to figure out how to prevent that logic
                         * */ 
                        window.location.reload();  
                    };
                }
            });

        });
    };

    this.initialize();
}


function MultipleTileBasedComponent(title) {
    var self = this;
        

    TileBasedComponent.apply(this);

    this.tileLoadedInEditor = ko.observable();

    this.itemName = ko.observable(ko.unwrap(title) ? ko.unwrap(title) : 'Items');

    this.hasDirtyTiles = ko.computed(function() {
        var tiles = self.tiles();
        var hasDirtyTiles = false;

        if (!tiles) { 
            hasDirtyTiles = true; 
        }
        else if (self.savedData() ) {
            if (self.savedData().length !== tiles.length) {
                hasDirtyTiles = true;
            }

            self.savedData().forEach(function(savedDatum) {
                var matchingTile = tiles.find(function(tile) {
                    return tile.tileid === savedDatum.tileid;
                });

                if (!matchingTile) {
                    hasDirtyTiles = true;
                }
                else if (!_.isEqual(savedDatum.data, koMapping.toJS(matchingTile.data))) {
                    hasDirtyTiles = true;
                }
            });
        }
        else if (!self.previouslyPersistedComponentData && tiles.length) {
            hasDirtyTiles = true;
        }
        else if (self.previouslyPersistedComponentData) {
            if (self.previouslyPersistedComponentData.length !== tiles.length) {
                hasDirtyTiles = true;
            }

            self.previouslyPersistedComponentData.forEach(function(persistedComponentData) {
                var matchingTile = tiles.find(function(tile) {
                    return tile.tileid === persistedComponentData.tileid;
                });

                if (!matchingTile) {
                    hasDirtyTiles = true;
                }
                else if (!_.isEqual(persistedComponentData.data, koMapping.toJS(matchingTile.data))) {
                    hasDirtyTiles = true;
                }
            });
        }
        self.multiTileUpdated(hasDirtyTiles);
        return hasDirtyTiles;
    });
    
    this.initialize = function() {
        self.loading(true);

        var savingSubscription = self.saving.subscribe(function(saving) {
            if (!saving) {
                savingSubscription.dispose(); /* self-disposing subscription only runs once */
                self.initialize();
            }
        });

        var multiTileInitSubscription = self.tiles.subscribe(function(tiles) {
            /* 
                at this point in load `self.tiles()` contains a reference to the empty
                tile we're using to map values to other tiles. This removes the empty
                mapping tile from being tracked
            */
            if (tiles.length === 1 && !tiles[0].tileid) {
                var savedTiles = [];

                if (self.savedData()) {
                    var savedData = self.savedData();

                    savedData.forEach(function(savedDatum) {
                        var newTile = self.card().getNewTile(true);

                        newTile.tileid = savedDatum.tileid;
                        newTile.data = koMapping.fromJS(savedDatum.data);
                        
                        savedTiles.unshift(newTile);
                    });
                }

                multiTileInitSubscription.dispose();  /* self-disposing subscription only runs once */ 
                self.tiles(savedTiles);
            }
        });

        /* load previously persisted data */ 
        var multiTilePersistedDataSubscription = self.tiles.subscribe(function(tiles) {
            if (self.previouslyPersistedComponentData && (!tiles || !tiles.length)) {
                var persistedTiles = [];

                self.previouslyPersistedComponentData.forEach(function(persistedComponentData) {
                    var newTile = self.card().getNewTile(true);

                    newTile.tileid = persistedComponentData.tileid;
                    newTile.data = koMapping.fromJS(persistedComponentData.data);

                    persistedTiles.unshift(newTile);
                });

                multiTilePersistedDataSubscription.dispose();  /* self-disposing subscription only runs once */ 
                self.tiles(persistedTiles);
            }
        });

    };

    this.addOrUpdateTile = function() {
        var tiles = self.tiles();
        self.tiles(null);

        /* breaks observable chain */ 
        var tileData = koMapping.fromJSON(
            koMapping.toJSON(self.tile().data)
        );

        var tileLoadedInEditor = self.tileLoadedInEditor();

        if (tileLoadedInEditor) {
            var index = _.findIndex(tiles, tileLoadedInEditor);
            
            if (index > -1) {
                tileLoadedInEditor.data = tileData;
                tiles[index] = tileLoadedInEditor;
            }

            
            self.tileLoadedInEditor(null);
        }
        else {
            var newTile = self.card().getNewTile(true);
            newTile.data = tileData;

            tiles.unshift(newTile);
        }

        self.tiles(tiles);
        self.tile().reset();
    };

    this.loadTileIntoEditor = function(data) {
        self.tileLoadedInEditor(data);

        var tile = self.tile();

        /* force the value of current tile data observables */ 
        Object.keys(tile.data).forEach(function(key) {
            if (ko.isObservable(tile.data[key])) {
                if (ko.isObservable(data.data[key])) {
                    tile.data[key](data.data[key]());
                }
                else{
                    tile.data[key](ko.mapping.toJS(data.data[key]));
                }
            }
        });
    };

    this.tilesToRemove = ko.observableArray();

    this.removeTile = function(data) {
        var filteredTiles = self.tiles().filter(function(tile) { return tile !== data; });
        if (data.tileid) {
            self.tilesToRemove.push(data);
        }
        self.tiles(filteredTiles);
    };

    this.saveMultiTiles = function() {
        self.complete(false);
        self.saving(true);
        self.previouslyPersistedComponentData = [];
        
        var unorderedSavedData = ko.observableArray();

        self.tiles().forEach(function(tile) {
            tile.save(
                function(){/* onFail */}, 
                function(savedTileData) {
                    unorderedSavedData.push(savedTileData);
                }
            );
        });

        self.tilesToRemove().forEach(function(tile) {
            tile.deleteTile(
                function(response) {
                    self.alert(new AlertViewModel(
                        'ep-alert-red', 
                        response.responseJSON.title,
                        response.responseJSON.message,
                        null, 
                        function(){ return; }
                    ));
                },
                function() {
                    self.tilesToRemove.remove(tile);
                    if ( self.tilesToRemove().length === 0 ) {
                        self.complete(true);
                        self.loading(true);
                        self.saving(false);
                    }
                }
            );
        });

        var saveSubscription = unorderedSavedData.subscribe(function(savedData) {
            if (savedData.length === self.tiles().length) {
                self.complete(true);
                self.loading(true);
                self.saving(false);

                var orderedSavedData = self.tiles().map(function(tile) {
                    return savedData.find(function(datum) {
                        return datum.tileid === tile.tileid;
                    });
                });

                self.savedData(orderedSavedData.reverse());

                saveSubscription.dispose();  /* self-disposing subscription only runs once */ 
            }
        });
    };

    this.clearEditor = function() {
        self.tile().reset();
    };

    this.reset = function() {
        self.tileLoadedInEditor(null);
        self.tile().reset();
        self.initialize();
        self.loading(false);
    };

    this.initialize();
}


function WorkflowComponentAbstract(params) {
    var self = this;

    this.workflowId = params.workflowId;
    this.workflowName = params.workflowName;
    this.componentData = params.componentData;
    this.workflowHistory = params.workflowHistory;
    this.alert = params.alert;
    
    this.locked = params.locked;
    this.lockExternalStep = params.lockExternalStep;
    this.lockableExternalSteps = params.lockableExternalSteps;

    this.id = ko.observable();

    this.resourceId = ko.observable();
            
    this.loading = ko.observable(false);
    this.saving = ko.observable(false);

    this.savedComponentPaths = {};
    this.value = ko.observable(null);

    this.complete = ko.observable(false);
    this.error = ko.observable();
    this.dirty = ko.observable(); /* user can manually set dirty state */

    this.AlertViewModel = AlertViewModel;
    this.saveOnQuit = ko.observable();

    this.isStepActive = params.isStepActive;
    this.isStepActive.subscribe(function(stepActive) {
        if (stepActive) {
            self.loadComponent();
        }
    });

    this.savedData = ko.observable(null);
    this.savedData.subscribe(function(savedData) {
        self.setToWorkflowHistory('value', savedData);
    });

    this.multiTileUpdated = ko.observable();
    this.hasUnsavedData = ko.computed(function() {
        var hasUnsavedData = false;

        if (self.componentData.tilesManaged === "many") {
            if (self.multiTileUpdated()) {
                hasUnsavedData = true;
            }
        } else {
            if (!_.isEqual(self.savedData(), self.value())) {
                hasUnsavedData = true;
            }
            else if (self.dirty()) {
                hasUnsavedData = true;
            }
        }

        return hasUnsavedData;
    });

    this.initialize = function() {
        /* cached ID logic */ 
        if (params.workflowComponentAbstractId) {
            self.id(params.workflowComponentAbstractId);
        }
        else {
            self.id(uuid.generate());
        }

        const savedValue = self.getSavedValue();
        if (savedValue) {
            self.savedData(savedValue);
            self.value(savedValue);
            self.complete(true);
        }
    };

    this.loadComponent = function() {
        self.loading(true);

        /* 
            Checks format of parameter values for external-component-path-patterned arrays.
            If parameter value matches pattern, get external component data and update value in self.componentData
        */ 
        if (self.componentData.parameters) {
            Object.keys(self.componentData.parameters).forEach(function(componentDataKey) {
                var componentDataValue = self.componentData.parameters[componentDataKey];

                if (params.isValidComponentPath(componentDataValue)) {
                    self.componentData.parameters[componentDataKey] = params.getDataFromComponentPath(componentDataValue);
                    self.savedComponentPaths[componentDataKey] = componentDataValue;
                }
                else if (self.savedComponentPaths[componentDataKey]) {
                    self.componentData.parameters[componentDataKey] = params.getDataFromComponentPath(self.savedComponentPaths[componentDataKey]);
                }
            });
        }

        if (self.savedData()) {
            self.value(self.savedData());
            self.complete(true);
        }

        if (self.componentData && self.componentData['parameters']) {
            self.resourceId(ko.unwrap(self.componentData['parameters']['resourceid']));
            self.componentData['parameters']['renderContext'] = 'workflow';
        }

        if (self.componentData.componentType === 'card') {
            let previouslySavedValue = self.getSavedValue();
            let previouslySavedResourceInstanceId;

            if (previouslySavedValue) {
                if (!(previouslySavedValue instanceof Array)) { previouslySavedValue = [previouslySavedValue]; }

                if (previouslySavedValue[0]['resourceInstanceId']) {
                    previouslySavedResourceInstanceId = previouslySavedValue[0]['resourceInstanceId'];
                    params['componentData']['parameters']['resourceid'] =  previouslySavedResourceInstanceId;
                }
            }

            AbstractCardAdapter.apply(self);
        }

        else if (!self.componentData.tilesManaged || self.componentData.tilesManaged === "none") {
            NonTileBasedComponent.apply(self);
        }
        else if (self.componentData.tilesManaged === "one") {
            TileBasedComponent.apply(self);
        }
        else if (self.componentData.tilesManaged === "many") {
            MultipleTileBasedComponent.apply(self, [params.title] );
        }
    };

    this.setToWorkflowHistory = async function(key, value) {
        const workflowid = self.workflowId;
        const workflowname = self.workflowName;
        
        const workflowHistory = {
            workflowid,
            workflowname,
            completed: false,
            componentdata: {
                // Django view will patch in this key, keeping existing keys
                [self.id()]: {
                    [key]: value,
                },
            },
        };

        await fetch(arches.urls.workflow_history + workflowid, {
            method: 'POST',
            credentials: 'include',
            headers: {
                "X-CSRFToken": Cookies.get('csrftoken')
            },
            body: JSON.stringify(workflowHistory),
        });

    };

    this.getSavedValue = function() {
        const savedValue = this.workflowHistory.componentdata?.[self.id()];
        if (savedValue) {
            return savedValue['value'];
        }
    };

    this.getCardResourceIdOrGraphId = function() {
        return (ko.unwrap(self.componentData.parameters.resourceid) || ko.unwrap(self.componentData.parameters.graphid));
    };

    this.save = function(){};  /* overwritten by inherited components */

    this._saveComponent = function(componentBasedStepResolve, componentBasedStepReject) {
        self.complete(false);
        var completeSubscription = self.complete.subscribe(function(complete) {
            if (complete) {

                if (componentBasedStepResolve) {
                    componentBasedStepResolve({
                        [self.componentData.uniqueInstanceName]: self.savedData(),
                    });
                }
                completeSubscription.dispose();  /* disposes after save */
                errorSubscription.dispose();  /* disposes after save */
            }
        });

        var errorSubscription = self.error.subscribe(function(error) {
            if (error) {

                if (componentBasedStepReject) {
                    componentBasedStepReject(error);
                    self.error(null);
                }
                completeSubscription.dispose();  /* disposes after save */
                errorSubscription.dispose();  /* disposes after save */
            }
        });

        // only saves updated tiles
        if (ko.unwrap(self.dirty) || ko.unwrap(self.hasDirtyTiles) || ko.unwrap(self.hasUnsavedData)) {
            if (self.componentData.tilesManaged === "many"){
                self.saveMultiTiles();
            } else {
                self.save();
            }
        }
        else {
            self.complete(true);
        }
    };

    this._resetComponent = function(componentBasedStepResolve) {
        if (ko.unwrap(self.tile)) {
            ko.unwrap(self.tile).reset();
        }

        componentBasedStepResolve(self.reset());
    };

    this.initialize();
}

ko.components.register('workflow-component-abstract', {
    template: workflowComponentAbstractTemplate,
});

export default WorkflowComponentAbstract;
