import {Point} from '../../..'
import {PointPair} from '../../../math/geometry/pointPair'
import {PolylinePoint} from '../../../math/geometry/polylinePoint'
import {compareNumbers} from '../../../utils/compare'
import {PointPairMap} from '../../../utils/pointPairMap'
import {IMetroMapOrderingAlgorithm} from './IMetroMapOrderingAlgorithm'
import {getOrientationOf3Vectors} from './MetroGraphData'
import {Metroline} from './MetroLine'
import {PointPairOrder} from './PointPairOrder'
import {Station} from './Station'

//  greedy bundle map ordering based on path comparison
export class GeneralMetroMapOrdering implements IMetroMapOrderingAlgorithm {
  //  bundle lines

  Metrolines: Array<Metroline>

  bundles: PointPairMap<PointPairOrder>

  //  Initialize bundle graph and build the ordering

  constructor(Metrolines: Array<Metroline>) {
    this.Metrolines = Metrolines
    this.BuildOrder()
  }

  *GetOrder(u: Station, v: Station): IterableIterator<Metroline> {
    const pointPair = new PointPair(u.Position, v.Position)
    const orderedMetrolineListForUv = this.bundles.get(pointPair).Metrolines
    if (u.Position == pointPair.First) {
      for (let i = 0; i < orderedMetrolineListForUv.length; i++) {
        yield orderedMetrolineListForUv[i]
      }
    } else {
      for (let i: number = orderedMetrolineListForUv.length - 1; i >= 0; i--) {
        yield orderedMetrolineListForUv[i]
      }
    }
  }

  //  Get the index of line on the edge (u->v) and node u

  GetLineIndexInOrder(u: Station, v: Station, ml: Metroline): number {
    const edge = new PointPair(u.Position, v.Position)
    const reversed = u.Position != edge.First
    const d = this.bundles.get(edge).LineIndexInOrder
    const ret = this.bundles.get(edge).LineIndexInOrder.get(ml)

    return !reversed ? ret : d.size - 1 - d.get(ml)
  } //  Do the main job

  ///  <summary>
  ///  Do the main job
  ///  </summary>
  BuildOrder() {
    this.bundles = new PointPairMap<PointPairOrder>()
    // initialization
    for (const ml of this.Metrolines) {
      for (let p = ml.Polyline.startPoint; p.next != null; p = p.next) {
        const e = new PointPair(p.point, p.next.point)
        let li: PointPairOrder = this.bundles.get(e)
        if (!li) {
          this.bundles.set(e, (li = new PointPairOrder()))
        }

        li.Add(ml)
      }
    }

    for (const edge of this.bundles) {
      this.BuildOrderPP(edge[0], edge[1])
    }
  }

  ///  <summary>
  ///  Build order for edge (u->v)
  ///  </summary>
  BuildOrderPP(pair: PointPair, order: PointPairOrder) {
    if (order.orderFixed) {
      return
    }

    order.Metrolines.sort((line0, line1) => this.CompareLines.bind(line0, line1, pair.First, pair.Second))
    // save order
    order.orderFixed = true
    order.LineIndexInOrder = new Map<Metroline, number>()
    for (let i = 0; i < order.Metrolines.length; i++) {
      order.LineIndexInOrder.set(order.Metrolines[i], i)
    }
  }

  ///  <summary>
  ///  Compare two lines on station u with respect to edge (u->v)
  ///  </summary>
  CompareLines(ml0: Metroline, ml1: Metroline, u: Point, v: Point): number {
    const t: {
      polyPoint: PolylinePoint
      next: (a: PolylinePoint) => PolylinePoint
      prev: (a: PolylinePoint) => PolylinePoint
    } = {polyPoint: null, next: null, prev: null}
    this.FindStationOnLine(u, v, ml0, t)
    const polylinePoint0 = t.polyPoint
    const next0 = t.next
    const prev0 = t.prev

    this.FindStationOnLine(u, v, ml1, t)
    const polylinePoint1 = t.polyPoint
    const next1 = t.next
    const prev1 = t.prev

    // go backward
    let p0 = polylinePoint0
    let p1 = polylinePoint1
    const p11: PolylinePoint = null
    let p00: PolylinePoint
    while (prev0(p0) != null && prev1(p1) != null && p00.point == p11.point) {
      const edge = new PointPair(p00.point, p0.point)
      if (this.bundles.get(edge).orderFixed) {
        return this.CompareOnFixedOrder(edge, ml0, ml1, p00.point != edge.First)
      }

      p0 = p00
      p1 = p11
    }

    if (p00 != null && p11 != null) {
      // we have a backward fork
      const forkBase = p0.point
      return GeneralMetroMapOrdering.IsLeft(next0(p0).point.sub(forkBase), p00.point.sub(forkBase), p11.point.sub(forkBase))
    }

    // go forward
    p0 = polylinePoint0
    p1 = polylinePoint1
    while (next0(p0) != null && next1(p1) != null && p00.point.equal(p11.point)) {
      const edge = new PointPair(p00.point, p0.point)
      if (this.bundles.get(edge).orderFixed) {
        return this.CompareOnFixedOrder(edge, ml0, ml1, !p0.point.equal(edge.First))
      }

      p0 = p00
      p1 = p11
    }

    if (p00 != null && p11 != null) {
      // compare forward fork
      const forkBase = p0.point
      return -GeneralMetroMapOrdering.IsLeft(prev0(p0).point.sub(forkBase), p00.point.sub(forkBase), p11.point.sub(forkBase))
    }

    // these are multiple edges
    return compareNumbers(ml0.Index, ml1.Index)
  }

  CompareOnFixedOrder(edge: PointPair, ml0: Metroline, ml1: Metroline, reverse: boolean): number {
    const mlToIndex = this.bundles.get(edge).LineIndexInOrder
    const r = reverse ? -1 : 1
    return r * compareNumbers(mlToIndex.get(ml0), mlToIndex.get(ml1))
  }

  ///  <summary>
  ///  Reimplement it in more efficient way!!! (cache indexes)
  ///  </summary>
  FindStationOnLine(
    u: Point,
    v: Point,
    ml: Metroline,
    t: {
      polyPoint: PolylinePoint
      next: (a: PolylinePoint) => PolylinePoint
      prev: (a: PolylinePoint) => PolylinePoint
    },
  ) {
    for (let p = ml.Polyline.startPoint; p.next != null; p = p.next) {
      if (p.point == u && p.next.point == v) {
        t.next = GeneralMetroMapOrdering.Next
        t.prev = GeneralMetroMapOrdering.Prev
        t.polyPoint = p
        return
      }

      if (p.point == v && p.next.point == u) {
        t.prev = GeneralMetroMapOrdering.Next
        t.next = GeneralMetroMapOrdering.Prev
        t.polyPoint = p.next
        return
      }
    }

    throw new Error()
  }

  static Next(p: PolylinePoint): PolylinePoint {
    return p.next
  }

  static Prev(p: PolylinePoint): PolylinePoint {
    return p.prev
  }

  ///  <summary>
  ///  Compare polar angles of v1 and v2 with respect to v0
  ///  (v1 lyes to the left of v2 ?)
  ///  </summary>
  ///  <returns>
  ///   -1  if v1 lyes to the left of v2
  ///    1  if v1 lyes to the right of v2
  ///    0  if v1 and v2 are collinear (and codirectinal)
  ///  </returns>
  static IsLeft(v0: Point, v1: Point, v2: Point): number {
    return getOrientationOf3Vectors(v0, v1, v2)
  }
}
