import React, {useContext, useMemo, useCallback, useState, useEffect, useRef, useLayoutEffect} from 'react';
import * as Dagre from 'dagre'
import * as Step from './Step'
import  {batch, connect, useSelector,shallowEqual,useDispatch} from 'react-redux'
import * as Common from './common'
import * as Antd from 'antd'
import * as Immer from 'immer'
import * as Operation from './Operation'
import * as Shared from 'placeholder-shared'
import * as Drag from './Drag'
import * as Tree from './Tree'
import { curveBasis as d3CurveBasis, line as d3Line } from "d3-shape";
import useResizeAware from 'react-resize-aware';
import * as CreateTask from './CreateTask'

var classnames = require('classnames')
import './Task.scss'
import { ClassNames } from '@emotion/core';

type TaskWrapperType = {
    label:string;
    children:React.ReactNode
}
export const TaskWrapper = (props) => {
    const {label, children} = props
    return <Common.DivWithClassName className='columnFlexBox'>
        <span className='TaskWrapperLabel'>{props.label}</span>
        {props.children}
    </Common.DivWithClassName>
}
export const StepEdge = (props: {
    sourcePort: Shared.TreeData.Port;
    targetPort: Shared.TreeData.Port;
}) => {
    const {sourcePort, targetPort} = props;
    const getCenterOfPort = (port: Shared.TreeData.Port) => {
        const portID = `port-${port.stepCounter}-${port.portIndex}`
        const { width, height } = document.getElementById(portID).getBoundingClientRect()
        const { left, top } = Tree.getMainWindowOffset(portID)
        return [left + (width / 2), (top +
            (height / 2))]
    }
    try {
        const [startX, startY] = getCenterOfPort(sourcePort)
        const [endX, endY] = getCenterOfPort(targetPort)
        return <Edge start={{ x: startX, y: startY }} offset={{ x: endX - startX, y: endY - startY }} />
    }
    catch (e) {
        return null;
    }
}

export const Edge = (props:{start:Common.IPoint, offset:Common.IPoint}) => {
    const {start,offset} = props;
    const controlOffset = offset.y < 100 ? offset.y / 2 : 50;
    return <path className='Edge' d={`M${start.x},${start.y} C${start.x},${start.y + controlOffset } ${start.x + offset.x},${start.y + offset.y-controlOffset},
    ${start.x + offset.x } ${start.y + offset.y}`}>
    </path>
}
export enum DrawMode {
    dropTargets, normal
}
type TaskProps = {
    taskID:number;
    dagreOptions:any;
    //onResize:(width:number, height:number)=> any;
    //dragStep?:Drag.DragPayload;
}

const useNameLookup = () => {
    const version = useRef(0)
    const equalFunc = (left:any, right:any)=> {
        if (Object.keys(left).length != Object.keys(right).length) {
            version.current = version.current + 1
            return false;
        }
        for (let stepCounter in left) {
            if (right[stepCounter] != left[stepCounter]) {
                version.current = version.current + 1
                return false
            }
        }
        return true;
    }
    const treeData = useSelector(state => (state as any).treeData[1])
    const steps = useSelector(state => (state as any).steps)
    const nameLookup = useSelector((state) => {
        return Shared.Runner.getNameLookup(treeData, steps)
    }, equalFunc)
    return {nameLookup, version:version.current}
}
const useModelNames = () => {
    const steps = useSelector(state => (state as any).steps)
    const version = useRef(0)
    const isEqual = useCallback((left,right) => {
        const leftKeys = Object.keys(left)
        const rightKeys = Object.keys(right)
        let result = true;
        if (leftKeys.length != rightKeys.length) {
            version.current = version.current + 1
            result = false;
        }
        for (let stepCounter in left) {
            if (right[stepCounter] != left[stepCounter]) {
                version.current = version.current + 1
                result = false
            }
        }
        //console.log("got result", result, left, right)
        return result;
    },[])
    const modelNames = useSelector((state) => {
        let modelNames = {}
        Object.keys(steps).map((stepCounter: any) => {
            try{
            modelNames[stepCounter] = Shared.Runner.getName(steps[stepCounter])
            }
            catch(ex) {
                debugger;
            }
        })
        return modelNames;
    },isEqual)
    return {modelNames,version:version.current}
}

export const useGetEdges = () => {
    const dispatch = useDispatch()
    let nameLookup = useNameLookup()
    let modelNames = useModelNames()
    const tableReferences = useSelector(state => (state as any).tableRefs)
    const treeData = useSelector(state => (state as any).treeData[1])
    //const runResults = useSelector(state=> (state as any).runResults)
    const edgesInfo = useMemo (() => {
        const onError = (sourceStep, stepCounter, tableRef) => {
            /*
            if (!runResults[stepCounter].editResult) {
                dispatch(Shared.State.getEditError(sourceStep, stepCounter, {
                    success: false, error: {
                        message: `SQL references a model that doesn't exist: ${tableRef}`
                    }
                }))
            }
            */
        }
        const onSuccess = (stepCounter) => {
            /*
            if (Shared.Common.getValueSafe(runResults, [stepCounter, 'editResult'], null)) {
                dispatch(Shared.State.getClearEditError(stepCounter))
            }
            */
        }
        const edgesInfo = Shared.Runner.getDescendents(treeData, nameLookup, tableReferences, onError, onSuccess)
        dispatch(Shared.State.getNewEdges(edgesInfo, nameLookup, modelNames))
        return edgesInfo;
    },[modelNames.version, dispatch,treeData, nameLookup.version,tableReferences])




    return edgesInfo;
}

export const getFullGraph = (g,tree, getWidthHeight, edgesInfo) => {
    if (tree) {
        tree.nodes.map((item) => {
            let [widthOfNode, heightOfNode] = getWidthHeight(item.stepCounter)

            //let step = runState.steps[item.stepCounter]
            //console.log("added item", sizeData, item.stepCounter, widthOfNode, heightOfNode)
            g.setNode(item.stepCounter, {
                label: '',
                width: widthOfNode,
                height: heightOfNode
            })
            return true;
        })
        for (let sourceStep in edgesInfo) {
            for (let targetIndex in edgesInfo[sourceStep]) {
                const targetStep = edgesInfo[sourceStep][targetIndex]
                g.setEdge(sourceStep,
                    targetStep)
                //console.log("added edge", sourceStep, targetStep)
            }
        }
    }
    return g;
}
export const getLineageGraph = (g, gFinal,tree, getWidthHeight, edgesInfo, lineage) => {
    const addNode = (stepCounter, label) => {
        let [widthOfNode, heightOfNode] = getWidthHeight(stepCounter)
        g.setNode(stepCounter, {
            label,
            width:widthOfNode, 
            height:heightOfNode
        })
    }
    if (tree) {
        tree.nodes.map((item) => {
            let [widthOfNode, heightOfNode] = getWidthHeight(item.stepCounter)

            //let step = runState.steps[item.stepCounter]
            //console.log("added item", sizeData, item.stepCounter, widthOfNode, heightOfNode)
            if (item.stepCounter in lineage){
                g.setNode(item.stepCounter, {
                    label: '',
                    width: widthOfNode,
                    height: heightOfNode
                })
            }
            return true;
        })
        let outsideSteps = []
        for (let sourceStep in edgesInfo) {
            for (let targetIndex in edgesInfo[sourceStep]) {
                const targetStep = edgesInfo[sourceStep][targetIndex]
                if (sourceStep in lineage && targetStep in lineage) {
                    g.setEdge(sourceStep,
                        targetStep)
                }
                else if (sourceStep in lineage && !(targetStep in lineage)) {
                    outsideSteps = [...outsideSteps, ...getSuccessors(gFinal, targetStep), parseInt(targetStep)]
                    addNode(targetStep, "out")
                    g.setEdge(sourceStep,
                        targetStep, {label:'out'})
                }
                else if (!(sourceStep in lineage) && (targetStep in lineage)) {
                    outsideSteps = [...outsideSteps, ...getPredecessors(gFinal, sourceStep), parseInt(sourceStep)]
                    addNode(sourceStep, "out")
                    g.setEdge(sourceStep,
                        targetStep, {label:'out'})
                }
            }
        }
        outsideSteps.forEach((stepCounter)=> {
            addNode(stepCounter,"out")
        })
        for (let sourceStep in edgesInfo) {
            for (let targetIndex in edgesInfo[sourceStep]) {
                const targetStep = edgesInfo[sourceStep][targetIndex]
                if (outsideSteps.includes(parseInt(sourceStep)) && outsideSteps.includes(parseInt(targetStep))) {
                    g.setEdge(sourceStep,
                        targetStep, {label:'out'})
                }
            }
        }
    }
    return g;
}

export const getPredecessors = (gFinal, stepCounter) => {
    let predecessors = []
    const processNode = (stepCounter) => {
        const thisP = gFinal.predecessors(stepCounter)
        if (thisP) {
            thisP.forEach((singleP) => {
                predecessors.push(parseInt(singleP))
                processNode(singleP)
            })
        }
    }
    processNode(stepCounter)
    return predecessors
}
export const getLineageNodesFromLineageGroups  = (lineageGroups) => {
    let lineageNodes ={} 
    Object.keys(lineageGroups).map((l) => {
        if (lineageGroups[l].show){
            lineageNodes = {...lineageNodes, ...lineageGroups[l].steps}
        }

    })
    return lineageNodes
}

export const getSuccessors = (gFinal, stepCounter) => {
    let successors = []
    const processNode = (stepCounter) => {
        const thisP = gFinal.successors(stepCounter)
        if (thisP) {
            thisP.forEach((singleP) => {
                successors.push(parseInt(singleP))
                processNode(singleP)
            })
        }
    }
    processNode(stepCounter)
    return successors
}

export const GraphForTask = (props:TaskProps) => {
    const {taskID} = props
    const [resizeListener, taskSize] = useResizeAware();
    const portOffset = useSelector((state) => ((state as any).UIState.portOffset))
    const dispatch = useDispatch()

    const [sizeData, onResize] = Step.useSizeData();
    const getWidthHeight = useCallback((key: any) => {
        let widthOfNode = Shared.Common.getValueSafe(sizeData, [ key, 'width'], null)
        let heightOfNode = Shared.Common.getValueSafe(sizeData, [key, 'height'], null)
        return [widthOfNode, heightOfNode]
    }, [sizeData])

    const lineageGroups = useSelector(state => (state as any).lineageGroups)
    const onResizeChild = useCallback((key: any, width: number, height: number) => {
        onResize(key, width, height)
    }, [onResize])
    let tree = useSelector(state => (state as any).treeData[taskID])
    let currentStep = useSelector(state => (state as any).currentStepGraph)
    const stepsSelect =useSelector(state=> (state as any).stepsSelect)

    const edgesInfo = useSelector(state => (state as any).edges.edgesInfo)
    let stepPositions: { [stepCounter: number]: { x: number, y: number } } = {}

    const gFinal = useMemo(()=> {
        let g = new Dagre.graphlib.Graph({});
        //the dagre library modifies this dagreOptions that gets set. make a copy since this is a prop
        g.setGraph({...props.dagreOptions});
        g.setDefaultEdgeLabel(function () { return {}; });

        let gFinal = getFullGraph(g, tree, getWidthHeight, edgesInfo)
        Dagre.layout(gFinal)
        dispatch(Shared.State.getNewGraph(gFinal))
        return gFinal;
    },[edgesInfo, getWidthHeight, props.dagreOptions,dispatch,tree.nodes]
    )
    let successors = useMemo(()=> {
        const successors = (() => {
            let p = {}
            const processNode = (stepCounter) => {
                p[stepCounter]=true
                const thisP = gFinal.successors(stepCounter)
                if (thisP) {
                    thisP.forEach((singleP) => {
                        p[singleP] = true;
                        processNode(singleP)
                    })
                }
            }
            const steps = [currentStep, ...(Object.keys(stepsSelect).map(s=> parseInt(s)))]
            for (let index in steps) {
                processNode(steps[index])
            }
            return p
        })()
        return successors
    }, [  currentStep, gFinal,stepsSelect])

    let predecessors  = useMemo(() => {
        const predecessors = (() => {
            let p = {}
            const processNode = (stepCounter) => {
                p[stepCounter]=true
                const thisP = gFinal.predecessors(stepCounter)
                if (thisP) {
                    thisP.forEach((singleP) => {
                        p[singleP] = true;
                        processNode(singleP)
                    })
                }
            }
            const steps = [currentStep, ...(Object.keys(stepsSelect).map(s=> parseInt(s)))]
            for (let index in steps) {
                processNode(steps[index])
            }
            return p
        })()
        return predecessors
    }, [  currentStep, gFinal,stepsSelect])


    const gToUse = useMemo(() => {
        const lineageGroupsToShow = !lineageGroups ? []:Object.keys(lineageGroups).map(l => {
            if (lineageGroups[l].show){
                return l
            }
            else{
                return null;
            }
        })
        .filter(s=>!!s!)
        if (lineageGroupsToShow.length){
            let g = new Dagre.graphlib.Graph({});
            //the dagre library modifies this dagreOptions that gets set. make a copy since this is a prop
            g.setGraph({ ...props.dagreOptions });
            g.setDefaultEdgeLabel(function () { return {}; });
            const lineageNodes = getLineageNodesFromLineageGroups(lineageGroups)
            getLineageGraph(g, gFinal,tree,getWidthHeight,edgesInfo,lineageNodes)
            Dagre.layout(g)
            return g;
    }
        return gFinal;
    },[gFinal,lineageGroups,edgesInfo,getWidthHeight,tree,props.dagreOptions])
    let nodes = useMemo(() => {
        if (!tree)
            return []
        return gToUse.nodes().map((nodeID) => {
            let node = gToUse.node(nodeID)
            if (!(node)){
                debugger;
            }
            let [left, top] = Common.getTopLeftFromMiddlePointsAndSize(node.x, node.y, node.width, node.height)

            const item = <Step.Step
                stepCounter={parseInt(nodeID)} />
            return < StepNodeContainer onResize={(width, height) => {
                onResizeChild(parseInt(nodeID), width, height)
            }}
                key={parseInt(nodeID)} className={
                classnames({
                    Predecessor: (parseInt(nodeID) in predecessors),
                    Successor: (parseInt(nodeID) in successors),
                    NodeOut:node.label =='out'
                })
            }
                top={top} left={left} stepCounter={parseInt(nodeID)} > {item}
            </StepNodeContainer >
        }
        )
    }, [tree, gToUse, predecessors, onResizeChild])

    let edges = useMemo(() => { 
        let edges = []
        if (tree) {
            edges = gToUse.edges().map((edgeID) => {
                let edge = gToUse.edge(edgeID)
                const line = d3Line()
                    .x(function (d: any) { return d.x; })
                    .y(function (d: any) { return d.y; })
                    .curve(d3CurveBasis)
                    // .tension(0.75)
                    (edge.points)
                    ;
                return <path key={JSON.stringify(edgeID)} className={
                    Common.classNames({
                        LinkLine: true,
                        PredecessorLink: parseInt(edgeID.w) in predecessors  || edgeID.w == currentStep,
                        SuccessorLink: parseInt(edgeID.v) in successors || edgeID.w == currentStep,
                        EdgeOut:edge.label == 'out'
                    })}
                    d={line} />
            })

            /*
            edges = tree.edges.map((edgeID) => {
                const edge = edgeData[edgeID]
                return (<StepEdge key={edgeID} sourcePort={edge.sourcePort} targetPort={edge.targetPort} />)
            })
            */
        }
        return edges;
    }, [tree, gToUse,currentStep,predecessors])
    //console.log("got edges", edges)

    let dragEdge = useMemo(() => {
        return null;
        /*
        if (dragStep && dragStep.type == Drag.DragPayloadType.port) {
            return (<Edge key='dragEdge' start={{ x: portOffset.initialX, y: portOffset.initialY }} offset={{ x: portOffset.x, y: portOffset.y }} />)
        }
        return null;
        */
    }, [ ])
    let offsets = { x: 0, y: 0 }
    return (
        <div className='treeContainer'
        >
            <svg style={{ 'transform': `translate(${offsets.x}px, ${offsets.y}px` }} className='LinkCanvas'>
                {edges}
                {dragEdge}
            </svg>
            <div style={{ 'transform': `translate(${offsets.x}px, ${offsets.y}px` }} className='NodeCanvas'>
                <Common.DivWithClassName
                    className={classnames({
                        'RealNodesContainer': true,
                    })}>
                    {nodes}
                </Common.DivWithClassName>
            </div>
        </div>)
}

const mapProps = (currentState:any, ownProps) => {
    return {
        dragStep:currentState.UIState.dragStep,
        dragOver:currentState.UIState.dragOver
    }
}

export var StateGraphForTask = connect(mapProps, null)(GraphForTask)


type StepNodeContainerProps = {
    top:number;
    left:number;
    children:React.ReactNode;
    stepCounter:number;
    className?:string;
    dragStep?:Drag.DragPayload;
    onResize? : (width:number, height:number)=>any;
}
const StepNodeContainer = (props: StepNodeContainerProps) => {
    const { onResize, top, left, children , stepCounter,className, dragStep} = props

    const [resizeListener,sizes]= useResizeAware()
    useEffect( () => {
        onResize(sizes.width, sizes.height)
    },[sizes.width, sizes.height])
    return <div data-stepcounter={stepCounter} style={{
        /*width: gFinal.graph().width, height: gFinal.graph().height,*/
        top, left
    }}
        className={classnames({
            'StepNodeContainer': true,
            [className]: true,
        })} key={stepCounter} >
        {resizeListener}
        {children}
    </div>
}

export interface DragState {

}

export interface DropPayload{
    stepCounter:number;
    taskID: number;
}
