import { alg, Graph } from 'graphlib'
const deepClone = require('lodash/cloneDeep')

import { StedGraph, StedNode, StedNodeAvailResult, StedGraphWithAvailability } from './sted-graph-type'

const debug = require('debug')('sted:sted-graph-generator')

export class StedGraphGenerator {
  private stedGraph: StedGraph
  static recycleNodeName: string = 'Recycle'
  static supplyPrefix = /[me]s-[a-z-]*/
  static exhaustPrefix = /[me]e-[a-z-]*/

  private supplyNodes: Array<string>
  private exhaustNodes: Array<string>
  private otherNodes: Array<string>
  private cutoffNodes: Array<any>

  constructor(graphData: StedGraph) {
    this.stedGraph = graphData
    this.supplyNodes = Array()
    this.exhaustNodes = Array()
    this.otherNodes = Array()
    this.cutoffNodes = Array()
  }

  print() {
    console.log(JSON.stringify(this.stedGraph))
  }

  generateGraph(stedNodeList: StedNode[]) {
    var generatedGraph = new Graph({ directed: true, multigraph: true })
    stedNodeList.forEach(stedNode => {
      generatedGraph.setNode(stedNode.name, stedNode.name)
      stedNode.outlets.forEach(out =>
        generatedGraph.setEdge(stedNode.name, out.dstNode, `${stedNode.name}-${out.name}-${out.dstNode}`, out.name)
      )
    })

    return generatedGraph
  }

  prepareStedNodes() {
    let genGraph = this.generateGraph(this.stedGraph.stedNodes)
    let tarjanResults = alg.tarjan(genGraph)

    // find supply & exhaust modules first
    let foundNode: StedNode
    tarjanResults.forEach(tarjanNodes => {
      tarjanNodes.forEach(tarjanNode => {
        // find supply modules & push them to supplyNodes array
        foundNode = this.stedGraph.stedNodes.find(stedNode => tarjanNode === stedNode.name)
        if (foundNode && foundNode.type.match(StedGraphGenerator.supplyPrefix)) {
          this.supplyNodes.push(foundNode.name)
        } else if (foundNode && foundNode.type.match(StedGraphGenerator.exhaustPrefix)) {
          this.exhaustNodes.push(foundNode.name)
        } else {
          this.otherNodes.push(foundNode.name)
        }
      })
    })

    // find and arrange other nodes except supply & exhaust
    let preOrderResult: string[] = []
    preOrderResult = alg.preorder(genGraph, this.otherNodes)
    preOrderResult = preOrderResult.filter(
      item => !this.supplyNodes.includes(item) && !this.exhaustNodes.includes(item)
    )
    this.otherNodes = preOrderResult

    let stedArrangedGraph: StedGraph = { stedNodes: [] }

    // reorder the nodes of sted graph by prefix & sufix modules
    this.supplyNodes
      .concat(this.otherNodes)
      .concat(this.exhaustNodes)
      .forEach(nodeName => {
        stedArrangedGraph.stedNodes.push(this.stedGraph.stedNodes.find(stedNode => nodeName === stedNode.name))
      })

    return stedArrangedGraph
  }

  findAllCycles(stedGraph: StedGraph) {
    let referenceGraph: Graph = this.generateGraph(stedGraph.stedNodes)
    let edges = {}
    referenceGraph.nodes().forEach(node => {
      edges[node] = referenceGraph.successors(node) || []
    })

    const findCycle = require('find-cycle/directed')
    const getConnectedNodes = node => referenceGraph.successors(node) || []

    var resultCycles = []
    let cycle = []
    do {
      cycle = findCycle(referenceGraph.nodes(), getConnectedNodes)
      if (cycle) {
        resultCycles.push(cycle)
        const cycleNodeEdges = referenceGraph.nodeEdges(cycle[0], cycle[1])
        if (cycleNodeEdges) {
          cycleNodeEdges.forEach(edge => {
            referenceGraph.removeEdge(cycle[0], cycle[1], edge.name)
          })
        }
      }
    } while (cycle)

    return resultCycles
  }

  findCycles(stedGraph: StedGraph) {
    let reorderedCycles: string[][] = []
    // transform its own json graph to graphlib graph
    let genGraph: Graph = this.generateGraph(stedGraph.stedNodes)

    // find cycles
    // let oneStepCycles: string[][] = alg.findCycles(this.generateGraph(stedGraph.stedNodes))
    // let cycles = this.findSubCycles(oneStepCycles, genGraph)
    let cycles = this.findAllCycles(stedGraph)

    cycles.forEach((cycle, idx) => {
      let preOrderResult: string[] = []
      preOrderResult = alg.preorder(genGraph, cycle)
      preOrderResult = preOrderResult.filter(item => cycle.includes(item))
      reorderedCycles[idx] = preOrderResult
    })

    return reorderedCycles
  }

  sortNodes(stedBaseGraph: StedGraph) {
    var genGraph = this.generateGraph(stedBaseGraph.stedNodes)
    return alg.topsort(genGraph)
  }

  lineUpTwoNodes(nodes: string[], refGraph: StedGraph) {
    let node0: StedNode = refGraph.stedNodes.find(node => node.name === nodes[0])
    let node1: StedNode = refGraph.stedNodes.find(node => node.name === nodes[1])

    return node0.outlets.find(outl => outl.dstNode === nodes[1]) ? [node0, node1] : [node1, node0]
  }

  findCutoffPoint(cycle, stedNodes: StedNode[]): { fromNode: string; toNode: string } {
    // 1. 하나의 폐루프 내에 초기화 포인트가 1개 있는 지 확인.
    // 2. 0개 이거나 2개 이상인 경우에는 프로세스를 중단한다.
    // 3. 폐루프 내 하나의 초기화 포인트가 있을 경우, Outlet과 Inlet을 확인하여 두 개의 인접노드를 찾는다.
    let fromNode: string
    let toNode: string
    cycle.forEach(node => {
      const foundNode = stedNodes.find(stedNode => stedNode.name === node)
      // 4. 폐루프의 절단 지점을 중심으로 한 노드의 출력과 반대쪽 노드의 일력을 찾는다.
      //   이 때, 노드의 출력 스트림에 초기 값이 있다면 해당 노드는 절단 지점의 입력이 되고,
      //   노드의 입력 스트림에 초기값이 있다면 해당 노드는 절단 지점의 출력이 된다.
      foundNode.outlets.forEach(outlet => {
        if (outlet.loopInitializationPoint && cycle.includes(outlet.dstNode.toString())) {
          fromNode = foundNode.name
        }
      })
      foundNode.inlets.forEach(inlet => {
        if (inlet.loopInitializationPoint && cycle.includes(inlet.srcNode.toString())) {
          toNode = foundNode.name
        }
      })
    })

    return {
      fromNode,
      toNode
    }
  }

  sortGraph(cycles, stedNodes: StedNode[]): any {
    // generate a Graph instance using STED nodes
    let generatedGraph = this.generateGraph(stedNodes)

    // remove cycled edges to make it acyclic
    cycles.forEach(cycle => {
      // clone generatedGraph
      let copiedGraph: Graph = Object.assign(Object.create(Object.getPrototypeOf(generatedGraph)), generatedGraph)
      // find one edge cut to  make this graph acyclic
      const { fromNode, toNode } = this.findCutoffPoint(cycle, stedNodes)
      // let dstCycleIdx = 0
      // let srcCycleIdx = cycle.length - 1

      let targetNodeEdges = copiedGraph.nodeEdges(fromNode, toNode)
      if (targetNodeEdges) {
        let targetNodeEdge = targetNodeEdges.filter(edge => edge.v === fromNode && edge.w === toNode)
        let tempGraph = copiedGraph.removeEdge(fromNode, toNode, targetNodeEdge[0].name)
        generatedGraph = tempGraph
        this.cutoffNodes.push([fromNode, toNode])
      }
    })

    // check if the Graph is a acyclic and apply sorting to it.
    let sortedList = alg.isAcyclic(generatedGraph) ? alg.topsort(generatedGraph) : []

    // sort the STED graph based on Graph node sequence by sorting algorithm
    sortedList.forEach((element, sortedIndex) => {
      let originalIndex = stedNodes.findIndex(node => node.name == element)
      if (originalIndex > 0) {
        stedNodes.splice(sortedIndex, 0, stedNodes.splice(originalIndex, 1)[0])
      }
    })
  }

  // Deprecated soon.
  sortGraphDeprecated(cycles, stedNodes: StedNode[]): any {
    // generate a Graph instance using STED nodes
    let generatedGraph = this.generateGraph(stedNodes)

    // remove cycled edges to make it acyclic
    cycles.forEach(cycle => {
      // clone generatedGraph
      let copiedGraph: Graph = Object.assign(Object.create(Object.getPrototypeOf(generatedGraph)), generatedGraph)
      // find one edge cut to  make this graph acyclic
      let dstCycleIdx = 0
      let srcCycleIdx = cycle.length - 1

      let targetNodeEdges = copiedGraph.nodeEdges(cycle[srcCycleIdx], cycle[dstCycleIdx])
      if (targetNodeEdges) {
        let targetNodeEdge = targetNodeEdges.filter(
          edge => edge.v === cycle[srcCycleIdx] && edge.w === cycle[dstCycleIdx]
        )
        let tempGraph = copiedGraph.removeEdge(cycle[srcCycleIdx], cycle[dstCycleIdx], targetNodeEdge[0].name)
        generatedGraph = tempGraph
        this.cutoffNodes.push([srcCycleIdx, dstCycleIdx])
      }
    })

    // check if the Graph is a acyclic and apply sorting to it.
    let sortedList = alg.isAcyclic(generatedGraph) ? alg.topsort(generatedGraph) : []

    // sort the STED graph based on Graph node sequence by sorting algorithm
    sortedList.forEach((element, sortedIndex) => {
      let originalIndex = stedNodes.findIndex(node => node.name == element)
      if (originalIndex > 0) {
        stedNodes.splice(sortedIndex, 0, stedNodes.splice(originalIndex, 1)[0])
      }
    })
  }

  _changeNodeOrder(stedGraph: StedGraph, prevNodeIndex: number, toIndex: number, targetNode: StedNode) {
    stedGraph.stedNodes.splice(prevNodeIndex, 1)
    stedGraph.stedNodes.splice(toIndex, 0, targetNode)
  }

  _arrangeGraphWithCuttingEdges(stedGraph: StedGraph, cycles): StedGraph {
    let arrangedGraph: StedGraph = deepClone(stedGraph)

    cycles.forEach((cycle, idx) => {
      // choose first two nodes as a cutting edge
      // TODO: need to check if there are only two nodes with a cycle?
      let [nextNode, prevNode] = this.lineUpTwoNodes(
        [this.cutoffNodes[idx][0], this.cutoffNodes[idx][1]],
        arrangedGraph
      )
      let foundIndex = arrangedGraph.stedNodes.findIndex(node => node.name == nextNode.name)
      let foundNode: StedNode = arrangedGraph.stedNodes[foundIndex]
      foundNode.inlets.forEach(inlet => {
        if (inlet.srcNode == prevNode.name) {
          inlet.srcNode = ''
          inlet.srcEdge = ''
        }
      })
    })

    return arrangedGraph
  }

  _reorderStedGraph(tempGraph: StedGraph, baseGraph: StedGraph): StedGraph {
    //let reorderedStedGraph: StedGraph = deepClone(tempGraph)
    tempGraph.stedNodes.forEach((node, idx) => {
      let foundIndexList = []
      if (idx > 0) {
        node.inlets.forEach(inlet => {
          let foundIndex = tempGraph.stedNodes.findIndex(node => inlet.srcNode == node.name)
          foundIndexList.push(foundIndex)
        })
        let maxInletIdx = Math.max(...foundIndexList)
        // 그래프의 경우에 따라서, sorting이 되었지만, 실제 계산에 필요한 순서가 맞지 않을 수 있다. (자세한 사항은 연구 노트에 명시 예정)
        // 이와 같은 경우를 극복하기 위해서,
        // 각 노드의 inlet의 srcNode의 index를 찾아서, 현재 노드의 index보다 작은 경우에는, 현재 노드를 inlet의 srcNode의 index보다 뒤로 보내는 작업을 수행한다.
        if (maxInletIdx > idx) {
          this._changeNodeOrder(baseGraph, idx, maxInletIdx, node)
        }
      }
    })

    return baseGraph
  }

  /*
   * 현재 그래프의 초기값 설정에 이상 유무를 확인합니다.
   *  1. 초기값 설정은 폐루프 당 하나만 존재해야 합니다.
   *  2. 만약 단일 노드에 여러 개의 스트림에 폐루프에 관여된다면, 해당 폐루프 설정은 피하도록 유도해야 합니다.
   *  (초기값을 여러 개 설정해야 하고, 폐루프 설정 및 여러 예외 사항에 대한 고려가 복잡합니다.
   *   초기값 설정에 대한 사용자 복잡도를 단순화하도록 1개의 초기값 설정으로 제한합니다.)
   */
  _checkGraphAvailablity(stedGraph: StedGraph, cycles): StedNodeAvailResult {
    const generatedGraph: Graph = this.generateGraph(stedGraph.stedNodes)

    let inletInitPointCount = 0
    let outletInitPointCount = 0

    // validate cycles with initialization points
    for (let cycle of cycles) {
      let initPointSrcNode = ''
      let initPointDstNode = ''

      // 노드에 여러 개의 입출력 스트림이 존재하고,
      // 이로 인해서 폐루프에는 하나의 노드가 여러 개의 스트림에 관여가 될 수 있습니다.
      // 다만, 현재 사용 중인 모듈의 경우 폐루프 검출 시, 다중 스트림에 대한 고려가 없기 때문에,
      // 아래와 같이 임의의 폐루프 어레이를 생성해서 이를 기준으로 다중 스트림에 대한 초기화 포인트 설정을 확인합니다.
      let modifiedCycle = []
      cycle.forEach((node, idx) => {
        let successors = generatedGraph.successors(node)
        let modifiedSuccessors = []
        successors &&
          successors.forEach(successor => {
            let successorsOfSuccessors = generatedGraph.successors(successor)
            successorsOfSuccessors &&
              successorsOfSuccessors.forEach(successorOfSuccessor => {
                if (cycle.includes(successorOfSuccessor)) {
                  modifiedSuccessors.push(successor)
                }
              })
          })
        modifiedCycle.push(modifiedSuccessors)
      })

      // 루프 내에 초기화 포인트가 1개 있는 지 확인
      for (let nodeNumbers of modifiedCycle) {
        nodeNumbers.forEach(nodeNumber => {
          const node = stedGraph.stedNodes.find(stedNode => stedNode.name === nodeNumber)
          node.inlets.forEach(inlet => {
            if (inlet.loopInitializationPoint && modifiedCycle.flat().includes(inlet.srcNode.toString())) {
              inletInitPointCount++
              initPointSrcNode = inlet.srcNode
            }
          })
          node.outlets.forEach(outlet => {
            if (outlet.loopInitializationPoint && modifiedCycle.flat().includes(outlet.dstNode.toString())) {
              outletInitPointCount++
              initPointDstNode = outlet.dstNode
            }
          })
        })
      }

      const checkResult: boolean = (inletInitPointCount === 1 && outletInitPointCount === 1) || false
      const checkCycle: any = cycle
      inletInitPointCount = 0
      outletInitPointCount = 0
      if (!checkResult) {
        return {
          result: false,
          detail: `폐루프 [${checkCycle}]의 초기값 설정이 0개 혹은 2개 이상 존재합니다. 폐루프에 초기값 설정은 반드시 1개 존재해야 합니다.`
        }
      }

      // 초기화 포인트가 1개로 확정된 상태에서, 획득되는 폐루프가 멀티 스트림에 대한 고려가 없기 때문에,
      // 즉, 노드에 여러 개의 스트림이 있고 폐루프가 여러개의 스트림에 연결되어 있을 경우
      // 다른 초기화 포인트를 선택하도록 메시지 출력
      let multiPossibleStreams = 0
      const initPointSrcNodeSuccessors: void | string[] = generatedGraph.successors(initPointSrcNode)
      initPointSrcNodeSuccessors &&
        initPointSrcNodeSuccessors.forEach(successor => {
          let successorNext = generatedGraph.successors(successor)
          successorNext &&
            successorNext.map(successorNextSuccessor => {
              if (cycle.includes(successorNextSuccessor)) {
                multiPossibleStreams++
              }
            })
        })
      const initPointDstNodePredecessors: void | string[] = generatedGraph.predecessors(initPointDstNode)
      initPointDstNodePredecessors &&
        initPointDstNodePredecessors.forEach(predecessor => {
          let predecessorPrev = generatedGraph.predecessors(predecessor)
          predecessorPrev &&
            predecessorPrev.map(predecessorPrevDecessor => {
              if (cycle.includes(predecessorPrevDecessor)) {
                multiPossibleStreams++
              }
            })
        })
      if (multiPossibleStreams > 2) {
        return {
          result: false,
          detail: `${initPointSrcNode}와 ${initPointDstNode} 사이에 한 개 이상의 스트림이 존재합니다. 폐루프에는 하나의 초기화 설정만 가능하기 때문에  다른 스트림에 초기값을 설정해야 합니다.`
        }
      }
    }

    return {
      result: true,
      detail: '프로세스 분석 결과가 정상적으로 저장되었습니다.'
    }
  }

  run(): StedGraphWithAvailability {
    let stedGraph = this.prepareStedNodes()

    let cycles = this.findCycles(stedGraph)
    debug('cycles: ', cycles)

    // clone input graph
    //var newGraph = deepClone(stedGraph)

    const availResult: StedNodeAvailResult = this._checkGraphAvailablity(stedGraph, cycles)
    if (!availResult.result) {
      return {
        stedGraph,
        availResult
      }
    }

    // sort the graph
    this.sortGraph(cycles, stedGraph.stedNodes)
    debug('sorted graph: ', stedGraph)

    // arrange the graph with cutting edges
    let tempGraph = this._arrangeGraphWithCuttingEdges(stedGraph, cycles)

    // change some sequence of the nodes for some exceptional cases
    stedGraph = this._reorderStedGraph(tempGraph, stedGraph)

    // insert recycle node between cycled nodes
    cycles.forEach((cycle, idx) => {
      // choose first two nodes as a cutting edge
      let [nextNode, prevNode] = this.lineUpTwoNodes([this.cutoffNodes[idx][0], this.cutoffNodes[idx][1]], stedGraph)

      // create new node name(prevName_nextName)
      let cycleNodeName = prevNode.name + '_' + nextNode.name
      debug('[DEBUG] cycleNodeName: ', cycleNodeName)

      // TODO: chekc if two nodes have two cycles simultaneously..
      //  if so, we need to set up an inlet and an outlet based on both nodes(prevNode & nextNode)
      // append new cycle node to the exsiting graph
      stedGraph.stedNodes.push({
        name: cycleNodeName,
        type: StedGraphGenerator.recycleNodeName,
        params: {},
        inlets: [
          {
            name: prevNode.outlets.find(outlet => outlet.dstNode.toString() === nextNode.name.toString())?.name,
            kind: 'InStr',
            type: prevNode.outlets.find(outlet => outlet.dstNode.toString() === nextNode.name.toString())?.type,
            srcNode: prevNode.name,
            srcEdge: prevNode.outlets.find(outlet => outlet.dstNode.toString() === nextNode.name.toString())?.kind
          }
        ],
        outlets: [
          {
            name: nextNode.inlets.find(inlet => inlet.srcNode.toString() === prevNode.name.toString())?.name,
            kind: 'OutStr',
            type: nextNode.inlets.find(inlet => inlet.srcNode.toString() === prevNode.name.toString())?.type,
            dstNode: nextNode.name,
            dstEdge: nextNode.inlets.find(inlet => inlet.srcNode.toString() === prevNode.name.toString())?.kind
          }
        ]
      })

      // update the adjacent nodes for a new generated node
      prevNode.outlets.map(outl => {
        if (outl.dstNode.toString() === nextNode.name.toString()) {
          outl.dstNode = cycleNodeName
          outl.dstEdge = 'InStr'
        }
      })
      nextNode.inlets.map(inl => {
        if (inl.srcNode.toString() === prevNode.name.toString()) {
          inl.srcNode = cycleNodeName
          inl.srcEdge = 'OutStr'
        }
      })
    })

    //const availResult: StedNodeAvailResult = this._checkGraphAvailablity(stedGraph, cycles)

    return {
      stedGraph,
      availResult
    }

    // reorder the STED graph based on the node sequence for some exceptional cases
    // let orderedStedGraph = this._reorderStedGraph(stedGraph)
    // return orderedStedGraph
  }
}
