import {ICurve, Edge, GeomEdge, GeomGraph, Graph, Node, GeomNode} from '..'
import {Port} from '../layout/core/port'
import {EdgeGeometry} from '../layout/core/edgeGeometry'
import {Curve, Point, PointLocation} from '../math/geometry'
import {HitTestBehavior} from '../math/geometry/RTree/HitTestBehavior'
import {CreateRectangleNodeOnData, RectangleNode} from '../math/geometry/RTree/RectangleNode'
import {CrossRectangleNodesSameType} from '../math/geometry/RTree/RectangleNodeUtils'
import {GetConnectedComponents} from '../math/graphAlgorithms/ConnectedComponentCalculator'
import {BasicGraphOnEdges, mkGraphOnEdgesN} from '../structs/basicGraphOnEdges'
import {IntPair} from '../utils/IntPair'
import {BundlingSettings} from './BundlingSettings'
import {InteractiveEdgeRouter} from './InteractiveEdgeRouter'
import {PreGraph} from './PreGraph'
import {Shape} from './shape'
import {BundleRouter} from './spline/bundling/BundleRouter'
import {SdShortestPath} from './spline/bundling/SdShortestPath'

export class MultiEdgeRouter {
  multiEdgeGeometries: Array<EdgeGeometry[]>

  interactiveEdgeRouter: InteractiveEdgeRouter

  bundlingSettings: BundlingSettings

  transparentShapeSetter: (e: EdgeGeometry) => Array<Shape>

  nodeTree: RectangleNode<ICurve, Point>

  constructor(
    multiEdgeGeoms: Array<GeomEdge[]>,
    interactiveEdgeRouter: InteractiveEdgeRouter,
    nodeBoundaryCurves: Array<ICurve>,
    bundlingSettings: BundlingSettings,
    transparentShapeSetter: (e: EdgeGeometry) => Array<Shape>,
  ) {
    this.multiEdgeGeometries = multiEdgeGeoms.map((l) => l.map((e) => e.edgeGeometry))
    this.interactiveEdgeRouter = interactiveEdgeRouter
    this.bundlingSettings = bundlingSettings
    this.transparentShapeSetter = transparentShapeSetter
    this.nodeTree = CreateRectangleNodeOnData(nodeBoundaryCurves, (c) => c.boundingBox)
  }

  run() {
    for (const graph of this.GetGeometryGraphs()) {
      const br = new BundleRouter(
        Array.from(graph.edges()),
        new SdShortestPath(this.transparentShapeSetter, null, null),
        this.interactiveEdgeRouter.VisibilityGraph,
        this.bundlingSettings,
        this.interactiveEdgeRouter.LoosePadding,
        this.interactiveEdgeRouter.TightHierarchy,
        this.interactiveEdgeRouter.LooseHierarchy,
        null,
        null,
        null,
      )
      br.run()
    }
  }

  *GetGeometryGraphs(): IterableIterator<GeomGraph> {
    for (const preGraph of this.GetIndependantPreGraphs()) yield this.CreateGeometryGraph(preGraph)
  }

  CreateGeometryGraph(preGraph: PreGraph): GeomGraph {
    const graph = new Graph()
    const gg = new GeomGraph(graph)
    const nodeDictionary = new Map<ICurve, Node>()
    let i = 0
    for (const curve of preGraph.nodeBoundaries) {
      const node = new Node(i.toString())
      i++
      nodeDictionary.set(curve, node)
      graph.nodeCollection.addNode(node)
      new GeomNode(node).boundaryCurve = curve
    }
    for (const eg of preGraph.edgeGeometries) {
      this.AddEdgeGeometryToGraph(eg, nodeDictionary, gg)
    }

    return gg
  }

  AddEdgeGeometryToGraph(eg: EdgeGeometry, nodeDictionary: Map<ICurve, Node>, gg: GeomGraph) {
    const sourceNode = this.GetOrCreateNode(eg.sourcePort, nodeDictionary, gg)
    const targetNode = this.GetOrCreateNode(eg.targetPort, nodeDictionary, gg)
    const e = new Edge(sourceNode, targetNode)
    new GeomEdge(e).edgeGeometry = eg
  }

  private GetOrCreateNode(port: Port, nodeDictionary: Map<ICurve, Node>, gg: GeomGraph): Node {
    const curve = this.GetPortCurve(port)
    let node: Node = nodeDictionary.get(curve)
    if (!node) {
      node = new Node(nodeDictionary.size.toString())
      const geomNode = new GeomNode(node)
      geomNode.boundaryCurve = curve
      gg.graph.nodeCollection.addNode(node)
      nodeDictionary.set(curve, node)
    }

    return node
  }

  private GetPortCurve(port: Port): ICurve {
    const curve = this.nodeTree.FirstHitNodeWithPredicate(port.Location, (point, c) =>
      Curve.PointRelativeToCurveLocation(point, c) != PointLocation.Outside ? HitTestBehavior.Stop : HitTestBehavior.Continue,
    ).UserData
    return curve
  }

  //  creates a set of pregraphs suitable for bundle routing

  GetIndependantPreGraphs(): Array<PreGraph> {
    const preGraphs = this.CreateInitialPregraphs()

    do {
      const count = preGraphs.length
      const t = {preGraphs: preGraphs}
      this.UniteConnectedPreGraphs(t)
      if (count <= preGraphs.length) break
    } while (true)
    return preGraphs
  }

  UniteConnectedPreGraphs(t: {preGraphs: Array<PreGraph>}) {
    const intersectionGraph = MultiEdgeRouter.GetIntersectionGraphOfPreGraphs(t.preGraphs)
    if (intersectionGraph == null) return
    const connectedComponents = GetConnectedComponents(intersectionGraph)
    const newPreGraphList = new Array<PreGraph>()
    for (const component of connectedComponents) {
      let preGraph: PreGraph = null
      for (const i of component) {
        if (preGraph == null) {
          preGraph = t.preGraphs[i]
          newPreGraphList.push(preGraph)
        } else {
          preGraph.AddGraph(t.preGraphs[i])
        }
      }
    }
    t.preGraphs = newPreGraphList
    for (const pg of t.preGraphs) this.AddIntersectingNodes(pg)
  }

  private AddIntersectingNodes(pg: PreGraph) {
    const rect = pg.boundingBox
    for (const curve of this.nodeTree.GetNodeItemsIntersectingRectangle(rect)) {
      pg.AddNodeBoundary(curve)
    }
  }

  static GetIntersectionGraphOfPreGraphs(preGraphs: Array<PreGraph>): BasicGraphOnEdges<IntPair> {
    const intersectingPairs = MultiEdgeRouter.EnumeratePairsOfIntersectedPreGraphs(preGraphs)
    if (intersectingPairs.length) {
      return mkGraphOnEdgesN(intersectingPairs, preGraphs.length)
    }

    return null
  }

  static EnumeratePairsOfIntersectedPreGraphs(preGraphs: Array<PreGraph>): Array<IntPair> {
    const arr = Array.from(Array(preGraphs.length).keys())
    const rn = CreateRectangleNodeOnData(arr, (i) => preGraphs[i].boundingBox)
    const list = new Array<IntPair>()
    CrossRectangleNodesSameType(rn, rn, (i, j) => list.push(new IntPair(i, j)))
    return list
  }

  CreateInitialPregraphs(): Array<PreGraph> {
    return this.multiEdgeGeometries.map((a: EdgeGeometry[]) => this.CreatePregraphFromSetOfEdgeGeometries(a))
  }

  private CreatePregraphFromSetOfEdgeGeometries(egs: EdgeGeometry[]): PreGraph {
    const nodeBoundaries = new Set<ICurve>()
    const eg = egs[0]
    const c = this.GetPortCurve(eg.sourcePort)
    const rect = c.boundingBox
    nodeBoundaries.add(c)
    nodeBoundaries.add(eg.targetPort.Curve)
    rect.addRec(eg.targetPort.Curve.boundingBox)
    const overlapped = this.nodeTree.GetNodeItemsIntersectingRectangle(rect)
    for (const nodeBoundary of overlapped) nodeBoundaries.add(nodeBoundary)
    return PreGraph.constructorStatic(egs, nodeBoundaries)
  }
}
