//  Adjust current bundle-routing

import {Point} from '../../..'
import {Polyline} from '../../../math/geometry'
import {Assert} from '../../../utils/assert'
import {PointSet} from '../../../utils/PointSet'
import {random} from '../../../utils/random'
import {BundlingSettings} from '../../BundlingSettings'
import {CostCalculator} from './CostCalculator'
import {IntersectionCache} from './IntersectionCache'
import {MetroGraphData} from './MetroGraphData'
import {Station} from './Station'

export class SimulatedAnnealing {
  //  bundle data

  metroGraphData: MetroGraphData

  //  Algorithm settings

  bundlingSettings: BundlingSettings

  //   calculates rouing cost
  costCalculator: CostCalculator

  //   used for fast calculation of intersections
  cache: IntersectionCache

  //  fix routing by simulated annealing algorithm

  static FixRouting(metroGraphData: MetroGraphData, bundlingSettings: BundlingSettings): boolean {
    return this.FixRoutingMBP(metroGraphData, bundlingSettings, null)
  }

  static FixRoutingMBP(metroGraphData: MetroGraphData, bundlingSettings: BundlingSettings, changedPoints: PointSet): boolean {
    return new SimulatedAnnealing(metroGraphData, bundlingSettings).FixRoutingP(changedPoints)
  }

  constructor(metroGraphData: MetroGraphData, bundlingSettings: BundlingSettings) {
    this.metroGraphData = metroGraphData
    this.bundlingSettings = bundlingSettings
    this.costCalculator = new CostCalculator(this.metroGraphData, this.bundlingSettings)
    this.cache = new IntersectionCache(this.metroGraphData, this.bundlingSettings, this.costCalculator, this.metroGraphData.Cdt)
  }

  static MaxIterations = 100

  static MaxStep = 50

  static MinStep = 1

  static MinRelativeChange = 0.0005

  stationsForOptimizations: Set<Station>

  //  Use constraint edge routing to reduce ink

  FixRoutingP(changedPoints: PointSet): boolean {
    this.stationsForOptimizations = this.GetStationsForOptimizations(changedPoints)
    this.cache.InitializeCostCache()
    let step: number = SimulatedAnnealing.MaxStep
    let energy: number = Number.POSITIVE_INFINITY
    let x: Array<Point> = this.metroGraphData.VirtualNodes().map((v) => v.Position)
    let iteration = 0
    while (iteration++ < SimulatedAnnealing.MaxIterations) {
      const coordinatesChanged = this.TryMoveNodes()
      //TimeMeasurer.DebugOutput("  #iter = " + iteration + " moved: " + cnt + "/" + metroGraphData.VirtualNodes().Count() + " step: " + step);

      if (iteration <= 1 && !coordinatesChanged) return false
      if (!coordinatesChanged) break

      const oldEnergy = energy
      energy = CostCalculator.Cost(this.metroGraphData, this.bundlingSettings)
      //TimeMeasurer.DebugOutput("energy: " + energy);

      step = this.UpdateMaxStep(step, oldEnergy, energy)
      const oldX: Array<Point> = x
      x = this.metroGraphData.VirtualNodes().map((v) => v.Position)
      if (step < SimulatedAnnealing.MinStep || this.Converged(step, oldX, x)) {
        break
      }
    }

    // TimeMeasurer.DebugOutput("SA completed after " + iteration + " iterations");
    return true
  }

  GetStationsForOptimizations(changedPoints: PointSet): Set<Station> {
    if (changedPoints == null) {
      return new Set<Station>(this.metroGraphData.VirtualNodes())
    } else {
      const result: Set<Station> = new Set<Station>()
      for (const p of changedPoints) {
        const s = this.metroGraphData.PointToStations.get(p)
        if (s && !s.IsRealNode) result.add(s)
      }
      return result
    }
  }

  //  stop SA if relative changes are small

  Converged(step: number, oldx: Array<Point>, newx: Array<Point>): boolean {
    // return false;
    let den = 0
    let num = 0
    for (let i = 0; i < oldx.length; i++) {
      num += oldx[i].sub(newx[i]).lengthSquared
      den += oldx[i].lengthSquared
    }

    const res: number = Math.sqrt(num / den)
    return res < SimulatedAnnealing.MinRelativeChange
  }

  stepsWithProgress = 0

  UpdateMaxStep(step: number, oldEnergy: number, newEnergy: number): number {
    // cooling factor
    const T = 0.8
    if (newEnergy + 1 < oldEnergy) {
      this.stepsWithProgress++
      if (this.stepsWithProgress >= 5) {
        this.stepsWithProgress = 0
        step = Math.min(SimulatedAnnealing.MaxStep, step / T)
      }
    } else {
      this.stepsWithProgress = 0
      step *= T
    }

    return step
  }

  TryMoveNodes(): boolean {
    let coordinatesChanged = false
    const movedStations: Set<Station> = new Set<Station>()
    // for (var node of metroGraphData.VirtualNodes()) {
    for (const node of this.stationsForOptimizations) {
      if (this.TryMoveNode(node)) {
        Assert.assert(this.stationsForOptimizations.has(node))
        coordinatesChanged = true
        movedStations.add(node)
        for (const adj of node.Neighbors) {
          if (!adj.IsRealNode) {
            movedStations.add(adj)
          }
        }
      }
    }
    this.stationsForOptimizations = movedStations
    return coordinatesChanged
  }

  //  Move node to decrease the cost of the drawing
  //  Returns true iff position has changed

  TryMoveNode(node: Station): boolean {
    let direction: Point = this.BuildDirection(node)
    if (direction.length == 0) {
      return false
    }

    let stepLength: number = this.BuildStepLength(node, direction)
    if (stepLength < SimulatedAnnealing.MinStep) {
      // try random direction
      direction = RandomPoint()
      stepLength = this.BuildStepLength(node, direction)
      if (stepLength < SimulatedAnnealing.MinStep) {
        return false
      }
    }

    const step: Point = direction.mul(stepLength)
    const newPosition: Point = node.Position.add(step)
    // can this happen?
    if (this.metroGraphData.PointToStations.has(newPosition)) {
      return false
    }

    this.metroGraphData.MoveNode(node, newPosition)
    this.cache.UpdateCostCache(node)
    return true
  }

  //  Calculate the direction to improve the ink function

  BuildDirection(node: Station): Point {
    const forceInk = this.BuildForceForInk(node)
    const forcePL = this.BuildForceForPathLengths(node)
    const forceR = this.BuildForceForRadius(node)
    const forceBundle = this.BuildForceForBundle(node)
    const force = forceInk.add(forcePL.add(forceR.add(forceBundle)))
    if (force.length < 0.1) {
      return new Point(0, 0)
    }

    return force.normalize()
  }

  BuildStepLength(node: Station, direction: Point): number {
    let stepLength: number = SimulatedAnnealing.MinStep
    let costGain: number = this.CostGain(node, node.Position.add(direction.mul(stepLength)))
    if (costGain < 0.01) {
      return 0
    }

    while (2 * stepLength <= SimulatedAnnealing.MaxStep) {
      const newCostGain: number = this.CostGain(node, node.Position.add(direction.mul(stepLength * 2)))
      if (newCostGain <= costGain) {
        break
      }

      stepLength *= 2
      costGain = newCostGain
    }

    return stepLength
  }

  //  Computes cost delta when moving the node
  //  the cost will be negative if a new position overlaps obstacles

  CostGain(node: Station, newPosition: Point): number {
    const MInf = -12345678
    const rGain: number = this.costCalculator.RadiusGain(node, newPosition)
    if (rGain < MInf) {
      return MInf
    }

    const bundleGain: number = this.costCalculator.BundleGain(node, newPosition)
    if (bundleGain < MInf) {
      return MInf
    }

    const inkGain: number = this.costCalculator.InkGain(node, newPosition)
    const plGain: number = this.costCalculator.PathLengthsGain(node, newPosition)
    return rGain + inkGain + plGain + bundleGain
  }

  //  force to decrease ink

  BuildForceForInk(node: Station): Point {
    //return new Point(0,0);
    let direction = new Point(0, 0)
    for (const adj of node.Neighbors) {
      const p = adj.Position.sub(node.Position)
      direction = direction.add(p.normalize())
    }

    //derivative
    const force = direction.mul(this.bundlingSettings.InkImportance)
    return force
  }

  //  direction to decrease path lengths

  BuildForceForPathLengths(node: Station): Point {
    // return new Point(0,0);
    let direction = new Point(0, 0)
    for (const mni of this.metroGraphData.MetroNodeInfosOfNode(node)) {
      const metroline = mni.Metroline
      const u: Point = mni.PolyPoint.next.point
      const v: Point = mni.PolyPoint.prev.point
      const p1 = u.sub(node.Position)
      const p2 = v.sub(node.Position)
      direction = direction.add(p1.div(p1.length * metroline.IdealLength))
      direction = direction.add(p2.div(p2.length * metroline.IdealLength))
    }
    // derivative
    const force: Point = direction.mul(this.bundlingSettings.PathLengthImportance)
    return force
  }

  //  direction to increase radii

  BuildForceForRadius(node: Station): Point {
    let direction: Point = new Point(0, 0)
    const idealR: number = node.cachedIdealRadius
    const t: {touchedObstacles: Array<[Polyline, Point]>} = {touchedObstacles: []}
    const res: boolean = this.metroGraphData.looseIntersections.HubAvoidsObstaclesSPNBA(node, node.Position, idealR, t)
    Assert.assert(res) // problem here
    for (const d of t.touchedObstacles) {
      const dist: number = d[1].sub(node.Position).length
      Assert.assert(dist <= idealR)
      const lforce: number = 2 * (1 - dist / idealR)
      const dir: Point = node.Position.sub(d[1]).normalize()
      direction = direction.add(dir.mul(lforce))
    }
    // derivative
    const force: Point = direction.mul(this.bundlingSettings.HubRepulsionImportance)
    return force
  }

  //  direction to push a bundle away from obstacle

  BuildForceForBundle(node: Station): Point {
    let direction = new Point(0, 0)
    for (const adj of node.Neighbors) {
      const idealWidth = this.metroGraphData.GetWidthSSN(node, adj, this.bundlingSettings.EdgeSeparation)

      const t: {closestDist: Array<[Point, Point]>} = {closestDist: []}
      const res = this.metroGraphData.cdtIntersections.BundleAvoidsObstacles(node, adj, node.Position, adj.Position, idealWidth / 2, t)
      if (!res) {
        // #if TEST_MSAGL&& TEST_MSAGL
        //                     HubDebugger.ShowHubs(metroGraphData, bundlingSettings, new LineSegment(node.Position, adj.Position));
        // #endif
      }
      //Debug.Assert(res);  //todo : still unsolved

      for (const d of t.closestDist) {
        const dist = d[0].sub(d[1]).length
        // Debug.Assert(LessOrEqual(dist, idealWidth / 2));
        const lforce = 2.0 * (1.0 - dist / (idealWidth / 2))
        const dir = d[0].sub(d[1]).normalize().neg()
        direction = direction.add(dir.mul(lforce))
      }
    }

    //derivative
    const force = direction.mul(this.bundlingSettings.BundleRepulsionImportance)

    return force
  }
}
function RandomPoint(): Point {
  return new Point(1 + 2 * random(), 1 + 2 * random())
}
