import ManagedData from "../manageddata";
import { Link } from "./link";
import { NodeType } from "./nodetype";
import { Node } from "./node";
import { GLOBAL_PARAMS } from "../helper/serializableitem";
import { GraphElement } from "./graphelement";

const GRAPH_PARAMS = [...GLOBAL_PARAMS];
const GRAPH_DATA_PARAMS = ["nodes", "links", "types"];

type GraphData = { nodes: Node[]; links: Link[]; types: NodeType[] };

export class Graph extends ManagedData {
    public data: GraphData;

    private nextNodeId = 0;
    private nextLinkId = 0;
    private nextTypeId = 0;

    // Callbacks
    public onChangeCallbacks: { (data: any): void }[];

    constructor(data: GraphData) {
        super(data);
        this.onChangeCallbacks = [];

        this.prepareIds(data);
    }

    /**
     * Intuitive getter for links.
     * @returns All links associated with the graph.
     */
    public get links(): Link[] {
        return this.data.links;
    }

    /**
     * Intuitive getter for nodes.
     * @returns All nodes associated with the graph.number
     */
    public get nodes(): Node[] {
        return this.data.nodes;
    }

    /**
     * Intuitive getter for node types.
     * @returns All node types associated with the graph.
     */
    public get types(): NodeType[] {
        return this.data.types;
    }

    /**
     * Determines the highest, used ids for GraphElements in data for later use.
     * @param data Data to analyse.
     */
    private prepareIds(data: GraphData) {
        if (data.links.length > 0) {
            this.nextLinkId = this.getHighestId(data.links) + 1;
        }
        if (data.nodes.length > 0) {
            this.nextNodeId = this.getHighestId(data.nodes) + 1;
        }
        if (data.types.length > 0) {
            this.nextTypeId = this.getHighestId(data.types) + 1;
        }
    }

    /**
     * Finds the highest id from a list of graph elements.
     * @param elements List of elements containing element with highest id.
     * @returns Highest id in list.
     */
    private getHighestId(elements: GraphElement[]): number {
        let highest = 0;
        elements.forEach((element) => {
            if (highest < element.id) {
                highest = element.id;
            }
        });
        return highest;
    }

    /**
     * Calls all registered callbacks for the onChange event.
     * @private
     */
    private triggerOnChange() {
        this.onChangeCallbacks.forEach((fn) => fn(this.data));
    }

    /**
     * Triggers change event on data-redo.
     */
    protected onRedo() {
        this.triggerOnChange();
    }

    /**
     * Triggers change event on data-undo.
     */
    protected onUndo() {
        this.triggerOnChange();
    }

    protected storableData(data: GraphData): any {
        let clean: GraphData;

        clean.links = data.links.map((link) => link.getCleanInstance());
        clean.nodes = data.nodes.map((node) => node.getCleanInstance());
        clean.types = data.types.map((type) => type.getCleanInstance());

        return clean;
    }

    serialize(): any {
        return this.serializeData(this.data);
    }

    /**
     * Takes a data object and serializes it.
     * @param data GraphData object to serialize.
     * @returns Serialized data.
     */
    private serializeData(data: GraphData): any {
        return {
            ...this.serializeProperties(GRAPH_PARAMS),
            ...this.serializeProperties(GRAPH_DATA_PARAMS, data),
        };
    }

    /**
     * Adds a pre-created node to the graph.
     * @param node New node object.
     * @returns True, if successful.
     */
    public addNode(node: Node) {
        if (this.data.nodes.includes(node)) {
            return true; // Already exists in graph.
        }

        // Update id
        node.id = this.nextNodeId;
        this.nextNodeId += 1;

        this.data.nodes.push(node);

        this.triggerOnChange();
        // TODO: Use toString implementation of node
        this.storeCurrentData("Added node [" + node + "]");

        return true;
    }

    /**
     * Deletes a node from the graph.
     * @param node Node object to remove.
     * @returns True, if successful.
     */
    public deleteNode(node: Node): boolean {
        if (!this.data.nodes.includes(node)) {
            return true; // Doesn't even exist in graph to begin with.
        }

        this.data.nodes.filter((n: Node) => n !== node);

        try {
            // No save points should be created when deleting the links
            this.disableStoring();

            // Delete all the links that contain this node
            node.links().forEach((l) => {
                l.delete();
            });
        } finally {
            this.enableStoring();
        }

        this.triggerOnChange();
        // TODO: Use toString implementation of node
        this.storeCurrentData(
            "Deleted node [" + node + "] and all connected links"
        );

        return true;
    }

    /**
     * Adds a pre-created link to the graph.
     * @param link New link object.
     * @returns True, if successful.
     */
    public addLink(link: Link): boolean {
        if (this.data.links.includes(link)) {
            return true; // Already exists in graph.
        }

        // Updateid
        link.id = this.nextLinkId;
        this.nextLinkId += 1;

        this.data.links.push(link);

        this.triggerOnChange();
        // TODO: Use toString implementation of link
        this.storeCurrentData("Added link [" + link + "]");

        return true;
    }

    /**
     * Deletes a link from the graph.
     * @param link Link object to remove.
     * @returns True, if successful.
     */
    public deleteLink(link: Link): boolean {
        if (!this.data.links.includes(link)) {
            return true; // Doesn't even exist in graph to begin with.
        }

        this.data.links.filter((l: Link) => l !== link);

        this.triggerOnChange();
        // TODO: Use toString implementation of link
        this.storeCurrentData("Deleted link [" + link + "]");

        return true;
    }
}