import { Solver } from '../solver/Solver'
import { Body } from '../objects/Body'
import type { Equation } from '../equations/Equation'
import type { World } from '../world/World'
import { GSSolver } from './GSSolver'

type SplitSolverNode = { body: Body | null; children: SplitSolverNode[]; eqs: Equation[]; visited: boolean }

/**
 * Splits the equations into islands and solves them independently. Can improve performance.
 */
export class SplitSolver extends Solver {
  /**
   * The number of solver iterations determines quality of the constraints in the world. The more iterations, the more correct simulation. More iterations need more computations though. If you have a large gravity force in your world, you will need more iterations.
   */
  iterations: number

  /**
   * When tolerance is reached, the system is assumed to be converged.
   */
  tolerance: number
  /** subsolver */
  subsolver: GSSolver
  nodes: SplitSolverNode[]
  nodePool: SplitSolverNode[]

  constructor(subsolver: GSSolver) {
    super()

    this.iterations = 10
    this.tolerance = 1e-7
    this.subsolver = subsolver
    this.nodes = []
    this.nodePool = []

    // Create needed nodes, reuse if possible
    while (this.nodePool.length < 128) {
      this.nodePool.push(this.createNode())
    }
  }

  /**
   * createNode
   */
  createNode(): SplitSolverNode {
    return { body: null, children: [], eqs: [], visited: false }
  }

  /**
   * Solve the subsystems
   * @return number of iterations performed
   */
  solve(dt: number, world: World): number {
    const nodes = SplitSolver_solve_nodes
    const nodePool = this.nodePool
    const bodies = world.bodies
    const equations = this.equations
    const Neq = equations.length
    const Nbodies = bodies.length
    const subsolver = this.subsolver

    // Create needed nodes, reuse if possible
    while (nodePool.length < Nbodies) {
      nodePool.push(this.createNode())
    }
    nodes.length = Nbodies
    for (let i = 0; i < Nbodies; i++) {
      nodes[i] = nodePool[i]
    }

    // Reset node values
    for (let i = 0; i !== Nbodies; i++) {
      const node = nodes[i]
      node.body = bodies[i]
      node.children.length = 0
      node.eqs.length = 0
      node.visited = false
    }
    for (let k = 0; k !== Neq; k++) {
      const eq = equations[k]
      const i = bodies.indexOf(eq.bi)
      const j = bodies.indexOf(eq.bj)
      const ni = nodes[i]
      const nj = nodes[j]
      ni.children.push(nj)
      ni.eqs.push(eq)
      nj.children.push(ni)
      nj.eqs.push(eq)
    }

    let child: SplitSolverNode | false
    let n = 0
    let eqs = SplitSolver_solve_eqs

    subsolver.tolerance = this.tolerance
    subsolver.iterations = this.iterations

    const dummyWorld = SplitSolver_solve_dummyWorld
    while ((child = getUnvisitedNode(nodes))) {
      eqs.length = 0
      dummyWorld.bodies.length = 0
      bfs(child, visitFunc, dummyWorld.bodies, eqs)

      const Neqs = eqs.length

      eqs = eqs.sort(sortById)

      for (let i = 0; i !== Neqs; i++) {
        subsolver.addEquation(eqs[i])
      }

      const iter = subsolver.solve(dt, dummyWorld as World)
      subsolver.removeAllEquations()
      n++
    }

    return n
  }
}

// Returns the number of subsystems
const SplitSolver_solve_nodes: SplitSolverNode[] = [] // All allocated node objects
const SplitSolver_solve_nodePool: SplitSolverNode[] = [] // All allocated node objects
const SplitSolver_solve_eqs: Equation[] = [] // Temp array
const SplitSolver_solve_bds: Body[] = [] // Temp array
const SplitSolver_solve_dummyWorld: { bodies: Body[] } = { bodies: [] } // Temp object

const STATIC = Body.STATIC

function getUnvisitedNode(nodes: SplitSolverNode[]): SplitSolverNode | false {
  const Nnodes = nodes.length
  for (let i = 0; i !== Nnodes; i++) {
    const node = nodes[i]
    if (!node.visited && !(node.body!.type & STATIC)) {
      return node
    }
  }
  return false
}

const queue: SplitSolverNode[] = []

function bfs(
  root: SplitSolverNode,
  visitFunc: (node: SplitSolverNode, bds: (Body | null)[], eqs: Equation[]) => void,
  bds: (Body | null)[],
  eqs: Equation[]
): void {
  queue.push(root)
  root.visited = true
  visitFunc(root, bds, eqs)
  while (queue.length) {
    const node = queue.pop()!
    // Loop over unvisited child nodes
    let child: SplitSolverNode | false
    while ((child = getUnvisitedNode(node.children))) {
      child.visited = true
      visitFunc(child, bds, eqs)
      queue.push(child)
    }
  }
}

function visitFunc(node: SplitSolverNode, bds: (Body | null)[], eqs: Equation[]): void {
  bds.push(node.body)
  const Neqs = node.eqs.length
  for (let i = 0; i !== Neqs; i++) {
    const eq = node.eqs[i]
    if (!eqs.includes(eq)) {
      eqs.push(eq)
    }
  }
}

function sortById(a: { id: number }, b: { id: number }): number {
  return b.id - a.id
}
