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

import { StedGraph, StedNode } 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({ multigraph: true })
    stedNodeList.forEach(stedNode => {
      generatedGraph.setNode(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
  }

  findCycles(stedGraph: StedGraph) {
    let reorderedCycles: string[][] = []
    // transform its own json graph to graphlib graph
    let genGraph = this.generateGraph(stedGraph.stedNodes)
    // find loop cycles using tarjan algorithm
    let cycles = alg.findCycles(this.generateGraph(stedGraph.stedNodes))
    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]
  }

  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
      for (let cycleIdx = 0; cycleIdx < cycle.length; cycleIdx++) {
        let dstCycleIdx = cycleIdx + 1 >= cycle.index ? 0 : cycleIdx + 1
        let targetNodeEdges = copiedGraph.nodeEdges(cycle[cycleIdx], cycle[dstCycleIdx])
        if (targetNodeEdges) {
          let targetNodeEdge = targetNodeEdges.filter(
            edge => edge.v === cycle[cycleIdx] && edge.w === cycle[dstCycleIdx]
          )
          let tempGraph = copiedGraph.removeEdge(cycle[cycleIdx], cycle[dstCycleIdx], targetNodeEdge[0].name)
          if (alg.isAcyclic(tempGraph)) {
            generatedGraph = tempGraph
            this.cutoffNodes.push([cycleIdx, dstCycleIdx])
            break
          }
        }
      }
      // original
      // let targetNodeEdges = generatedGraph.nodeEdges(cycle[0], cycle[1])
      // if (targetNodeEdges) {
      //   let targetNodeEdge = targetNodeEdges.filter(edge => edge.v === cycle[0] && edge.w === cycle[1])
      //   generatedGraph = generatedGraph.removeEdge(cycle[0], cycle[1], targetNodeEdge[0].name)
      // }
    })

    // 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(
        [cycle[this.cutoffNodes[idx][0]], cycle[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
  }

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

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

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

    // 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
      // TODO: need to check if there are only two nodes with a cycle?
      let [nextNode, prevNode] = this.lineUpTwoNodes(
        [cycle[this.cutoffNodes[idx][0]], cycle[this.cutoffNodes[idx][1]]],
        stedGraph
      )

      // // CHECK LATER: adjust the sequence of the final node array
      // const prevNodeIndex = stedGraph.stedNodes.indexOf(prevNode)
      // const toIndex = stedGraph.stedNodes.length
      // stedGraph.stedNodes.splice(prevNodeIndex, 1)
      // stedGraph.stedNodes.splice(toIndex, 0, prevNode)

      // 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'
        }
      })
    })

    return stedGraph

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