import * as Config from "../config";
import * as Helpers from "./helpers";
// import { loadGraphJson } from "../datasets";

import * as THREE from "three";
import { ForceGraph3D } from "react-force-graph";
// import screenfull from "screenfull";

// import {
//     CSS3DRenderer,
//     CSS3DSprite,
// } from "three/examples/jsm/renderers/CSS3DRenderer.js";
import { MODE, DRAG_THRESHOLD_3D } from "../config";
// import background from "./background.jpg";

//import { Line2, LineGeometry, LineMaterial } from "three-fatline";
import { Line2, LineGeometry, LineMaterial } from "three-fatline";
import React from "react";
import PropTypes, { InferType } from "prop-types";
import SpriteText from "three-spritetext";
import { Object3D } from "three";
import Graph, { Coordinate, LinkData, NodeData } from "./graph";
// import { graph } from "../editor/js/editor";

export interface GraphNode extends NodeData {
    x: number;
    y: number;
    z: number;
    vx: number;
    vy: number;
    vz: number;
    fx: number;
    fy: number;
    fz: number;
    color: string;
    __threeObj: THREE.Group;
}

export interface GraphLink extends LinkData {
    __lineObj?: Line2;
}

export class GraphRenderer extends React.Component<
    InferType<typeof GraphRenderer.propTypes>,
    InferType<typeof GraphRenderer.stateTypes>
> {
    props: InferType<typeof GraphRenderer.propTypes>;
    state: InferType<typeof GraphRenderer.stateTypes>;
    forceGraph: React.RefObject<any>; // using typeof ForceGraph3d produces an error here...
    edgeColors: Map<string, string>;
    nodeColors: Map<string, string>;
    highlightedNodes: Set<GraphNode>;
    highlightedLinks: Set<GraphLink>;
    hoverNode: GraphNode;

    static propTypes = {
        graph: PropTypes.instanceOf(Graph).isRequired,
        loadingFinishedCallback: PropTypes.func,
        onNodeClicked: PropTypes.func,
        isFullscreen: PropTypes.bool,
    };

    static stateTypes = {};

    constructor(props: InferType<typeof GraphRenderer.propTypes>) {
        super(props);

        this.highlightedNodes = new Set();
        this.highlightedLinks = new Set();
        this.hoverNode = null;
        this.forceGraph = React.createRef();
        this.edgeColors = new Map<string, string>();
        this.nodeColors = new Map<string, string>();

        this.mapLinkColors();
        this.mapNodeColors();
        // TODO: NodeVisibility, linkVisibility, graphLoading has to be moved to parent component
    }

    // componentDidMount() {
    //     loadGraphJson(this.props.spaceId).then((json) =>
    //         this.updateGraph(json)
    //     );
    // }

    // updateGraph(json: { nodes: NodeData[]; links: LinkData[] }) {
    //     const graph = new Graph(json.nodes, json.links);
    //     this.setState({ graph: graph });
    //
    //     this.edgeVisibility = new Map<string, boolean>(
    //         graph.getLinkClasses().map((cls) => [cls, true])
    //     );
    //     this.nodeVisibility = new Map<string, boolean>(
    //         graph.getNodeClasses().map((cls) => [cls, true])
    //     );
    // }

    drawNode(node: GraphNode): Object3D {
        const group = new THREE.Group();

        const text = new SpriteText(node.name);
        text.color = "white";
        text.backgroundColor = "black"; // TODO: Set this dynamically based on the node type
        text.textHeight = 5;
        // text.padding = 2;
        text.borderRadius = 5;
        text.borderWidth = 3;
        text.borderColor = "black";
        text.translateY(12);
        text.material.opacity = 0.85;
        text.renderOrder = 999;
        group.add(text);

        // Draw node circle image
        const textureLoader = new THREE.TextureLoader();
        textureLoader.setCrossOrigin("anonymous");
        const imageAlpha = textureLoader.load(
            Config.PLUGIN_PATH + "datasets/images/alpha.png"
        );

        const material = new THREE.SpriteMaterial({
            //map: imageTexture,
            color: this.nodeColors.get(node.type),
            alphaMap: imageAlpha,
            transparent: true,
            alphaTest: 0.2,
            depthWrite: false,
            depthTest: false,
        });

        const sprite = new THREE.Sprite(material);
        //sprite.renderOrder = 999; // This may not be optimal. But it allows us to render the sprite on top of everything else.
        sprite.scale.set(8, 8, 8);

        group.add(sprite);

        return group;
    }

    drawLink(link: LinkData) {
        const colors = new Float32Array(
            [].concat(
                ...[link.target, link.source]
                    .map((node) => this.nodeColors.get(node.type))
                    .map((color) => color.replace(/[^\d,]/g, "").split(",")) // Extract rgb() color components
                    .map((rgb) => rgb.map((v) => parseInt(v) / 255))
            )
        );

        const geometry = new LineGeometry();
        geometry.setPositions([0, 0, 0, 1, 1, 1]);
        geometry.setColors(colors);

        const material = new LineMaterial({
            color: 0xffffff,
            linewidth: Config.LINK_WIDTH, // in world units with size attenuation, pixels otherwise
            vertexColors: true,

            resolution: new THREE.Vector2(
                window.screen.width,
                window.screen.height
            ), // Set the resolution to the maximum width and height of the screen.
            dashed: false,
            alphaToCoverage: true,
        });

        const line = new Line2(geometry, material);
        line.computeLineDistances();
        line.scale.set(1, 1, 1);
        return line;
    }

    onNodeHover(node: GraphNode) {
        // no state change
        if (
            (!node && !this.highlightedNodes.size) ||
            (node && this.hoverNode === node)
        )
            return;

        const highlightedNodes = new Set<GraphNode>();
        const highlightedLinks = new Set<GraphLink>();

        if (node) {
            highlightedNodes.add(node);
            node.neighbors.forEach((neighbor) =>
                highlightedNodes.add(neighbor as GraphNode)
            );
            node.links.forEach((link) =>
                highlightedLinks.add(link as GraphLink)
            );
        }

        this.hoverNode = node || null;
        this.updateHighlight(highlightedNodes, highlightedLinks);
    }

    onLinkHover(link: GraphLink) {
        const highlightedNodes = new Set<GraphNode>();
        const highlightedLinks = new Set<GraphLink>();

        if (link && link.__lineObj) {
            highlightedLinks.add(link);
            highlightedNodes.add(link.source as GraphNode);
            highlightedNodes.add(link.target as GraphNode);
        }

        this.updateHighlight(highlightedNodes, highlightedLinks);
    }

    updateHighlight(
        highlightedNodes: Set<GraphNode>,
        highlightedLinks: Set<GraphLink>
    ) {
        // Update Links
        this.highlightedLinks.forEach(
            (link) => (link.__lineObj.material.linewidth = Config.LINK_WIDTH)
        );
        this.highlightedLinks = highlightedLinks;
        this.highlightedLinks.forEach(
            (link) =>
                (link.__lineObj.material.linewidth = Config.HOVER_LINK_WIDTH)
        );

        // Update Nodes
        this.highlightedNodes.forEach((node) => {
            node.__threeObj.children[1].scale.set(8, 8, 8);
        });
        this.highlightedNodes = highlightedNodes;
        this.highlightedNodes.forEach((node) => {
            node.__threeObj.children[1].scale.set(12, 12, 12);
        });
    }

    onNodeClick(node: GraphNode) {
        this.focusOnNode(node);
        if (MODE === "default" && this.props.onNodeClicked) {
            this.props.onNodeClicked(node);
        }
    }

    onNodeDragEnd(node: GraphNode, translate: Coordinate) {
        // NodeDrag is handled like NodeClick if distance is very short
        if (
            Math.sqrt(
                Math.pow(translate.x, 2) +
                    Math.pow(translate.y, 2) +
                    Math.pow(translate.z, 2)
            ) < DRAG_THRESHOLD_3D
        ) {
            this.onNodeClick(node);
        }
    }

    focusOnNode(node: GraphNode) {
        // Aim at node from outside it
        const distance = 400;
        const distRatio = 1 + distance / Math.hypot(node.x, node.y, node.z);

        this.forceGraph.current.cameraPosition(
            {
                x: node.x * distRatio,
                y: node.y * distRatio,
                z: node.z * distRatio,
            }, // new position
            node, // lookAt ({ x, y, z })
            1000 // ms transition duration
        );
    }

    getLinkColor(link: LinkData) {
        if ("type" in link) {
            return this.edgeColors.get(link.type);
        }
        return "rgb(255, 255, 255)";
    }

    getLinkWidth(link: LinkData) {
        return this.state.highlightLinks.has(link) ? 2 : 0.8;
    }

    /**
     * Maps the colors of the color palette to the different edge types
     */
    mapLinkColors() {
        // TODO: Move this to the graph data structure - access is also needed in the other menues?
        const linkClasses = this.props.graph.getLinkClasses();
        for (let i = 0; i < linkClasses.length; i++) {
            this.edgeColors.set(
                linkClasses[i],
                Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
            );
        }
    }

    /**
     * Maps the colors of the color palette to the different edge types
     */
    mapNodeColors() {
        // TODO: Move this to the graph data structure - access is also needed in the other menues?
        const nodeClasses = this.props.graph.getNodeClasses();
        for (let i = 0; i < nodeClasses.length; i++) {
            this.nodeColors.set(
                nodeClasses[i],
                Config.COLOR_PALETTE[i % Config.COLOR_PALETTE.length]
            );
        }
    }

    resize() {
        // TODO
        // if (screenfull.isFullscreen) {
        //     this.forceGraph.height(screen.height);
        //     this.forceGraph.width(screen.width);
        // } else {
        //     this.forceGraph.height(window.innerHeight - 200);
        //     this.forceGraph.width(Helpers.getWidth());
        // }
    }

    updateLinkPosition(line: Line2, start: Coordinate, end: Coordinate) {
        if (!(line instanceof Line2)) {
            return false;
        }
        // console.log("Updating links");
        const startR = 4;
        const endR = 4;
        const lineLen = Math.sqrt(
            Math.pow(end.x - start.x, 2) +
                Math.pow(end.y - start.y, 2) +
                Math.pow(end.z - start.z, 2)
        );

        const positions = [startR / lineLen, 1 - endR / lineLen]
            .map((t) =>
                ["x", "y", "z"].map(
                    (dim) =>
                        start[dim as keyof typeof start] +
                        (end[dim as keyof typeof end] -
                            start[dim as keyof typeof start]) *
                            t
                )
            )
            .flat();
        // line.geometry.setFromPoints()
        line.geometry.setPositions(positions);
        // line.geometry.getAttribute("position").needsUpdate = true;
        // line.computeLineDistances();

        return true;
    }

    render() {
        return (
            <ForceGraph3D
                ref={this.forceGraph}
                width={Helpers.getWidth()} // TODO: Replace Helpers downstream? This could be a prop
                height={Helpers.getHeight()}
                graphData={this.props.graph}
                rendererConfig={{ antialias: true }}
                // nodeLabel={"hidden"}
                nodeThreeObject={(node: GraphNode) => this.drawNode(node)}
                linkThreeObject={(link: LinkData) => this.drawLink(link)}
                onNodeClick={(node: GraphNode) => this.onNodeClick(node)}
                //d3AlphaDecay={0.1}
                warmupTicks={150}
                cooldownTime={1000} // TODO: Do we want the simulation to unfreeze on node drag?
                onNodeHover={(node: GraphNode) => this.onNodeHover(node)}
                onLinkHover={(link: GraphLink) => this.onLinkHover(link)}
                linkPositionUpdate={(
                    line: Line2,
                    coords: { start: Coordinate; end: Coordinate }
                ) => this.updateLinkPosition(line, coords.start, coords.end)}
                onNodeDragEnd={(node: GraphNode, translate: Coordinate) =>
                    this.onNodeDragEnd(node, translate)
                }
            />
        );
    }
}