// SPDX-License-Identifier: Apache-2.0

package firrtl.graph

import scala.collection.mutable

/** Euler Tour companion object */
object EulerTour {

  /** Create an Euler Tour of a `DiGraph[T]` */
  def apply[T](diGraph: DiGraph[T], start: T): EulerTour[Seq[T]] = {
    val r = mutable.Map[Seq[T], Int]()
    val e = mutable.ArrayBuffer[Seq[T]]()
    val h = mutable.ArrayBuffer[Int]()

    def tour(u: T, parent: Vector[T], height: Int): Unit = {
      val id = parent :+ u
      r.getOrElseUpdate(id, e.size)
      e += id
      h += height
      diGraph.getEdges(id.last).foreach { v =>
        tour(v, id, height + 1)
        e += id
        h += height
      }
    }

    tour(start, Vector.empty, 0)
    new EulerTour(r.toMap, e.toSeq, h.toSeq)
  }
}

/** A class that represents an Euler Tour of a directed graph from a
  * given root. This requires `O(n)` preprocessing time to generate
  * the initial Euler Tour.
  *
  * @constructor Create a new EulerTour from the specified data
  * @param r A map of a node to its first index
  * @param e A representation of the EulerTour as a `Seq[T]`
  * @param h The depths of the Euler Tour represented as a `Seq[Int]`
  */
class EulerTour[T](r: Map[T, Int], e: Seq[T], h: Seq[Int]) {
  private def lg(x: Double): Double = math.log(x) / math.log(2)

  /** Range Minimum Query of an Euler Tour using a naive algorithm.
    *
    * @param x The first query bound
    * @param y The second query bound
    * @return The minimum between the first and second query
    * @note The order of '''x''' and '''y''' does not matter
    * @note '''Performance''':
    *   - preprocessing: `O(1)`
    *   - query: `O(n)`
    */
  def rmqNaive(x: T, y: T): T = {
    val Seq(i, j) = Seq(r(x), r(y)).sorted
    e.zip(h).slice(i, j + 1).minBy(_._2)._1
  }

  // n: the length of the Euler Tour
  // m: the size of blocks the Euler Tour is split into
  private val n = h.size
  private val m = math.max(1, math.ceil(lg(n) / 2).toInt)

  /** Split up the tour into blocks of size m, padding the last block to
    * be a multiple of m. Compute the minimum of each block, a, and
    * the index of that minimum in each block, b.
    */
  private lazy val blocks = (h ++ (1 to (m - n % m))).grouped(m).toArray
  private lazy val a = blocks.map(_.min)
  private lazy val b = blocks.map(b => b.indexOf(b.min))

  /** Construct a Sparse Table (ST) representation for the minimum index
    * of a sequence of integers. Data in the returned array is indexed
    * as: [base, power of 2 range]
    */
  private def constructSparseTable(x: Seq[Int]): Array[Array[Int]] = {
    val tmp = Array.ofDim[Int](x.size + 1, math.ceil(lg(x.size)).toInt)
    for {
      i <- 0 to x.size - 1
      j <- 0 to math.ceil(lg(x.size)).toInt - 1
    } {
      tmp(i)(j) = -1
    }

    def tableRecursive(base: Int, size: Int): Int = {
      if (size == 0) {
        tmp(base)(size) = base
        base
      } else {
        val (a, b, c) = (base, base + (1 << (size - 1)), size - 1)

        val l = if (tmp(a)(c) != -1) { tmp(a)(c) }
        else { tableRecursive(a, c) }

        val r = if (tmp(b)(c) != -1) { tmp(b)(c) }
        else { tableRecursive(b, c) }

        val min = if (x(l) < x(r)) l else r
        tmp(base)(size) = min
        assert(min >= base)
        min
      }
    }

    for {
      i <- (0 to x.size - 1)
      j <- (0 to math.ceil(lg(x.size)).toInt - 1)
      if i + (1 << j) - 1 < x.size
    } {
      tableRecursive(i, j)
    }
    tmp
  }
  private lazy val st = constructSparseTable(a)

  /** Precompute all possible RMQs for an array of size `n where each
    * entry in the range is different from the last by only +-1
    */
  private def constructTableLookups(n: Int): Array[Array[Array[Int]]] = {
    def sortSeqSeq[A <: Int](x: Seq[A], y: Seq[A]): Boolean = {
      if (x(0) != y(0)) x(0) < y(0) else sortSeqSeq(x.tail, y.tail)
    }

    val size = m - 1
    val out = Seq
      .fill(size)(Seq(-1, 1))
      .flatten
      .combinations(m - 1)
      .flatMap(_.permutations)
      .toList
      .sortWith(sortSeqSeq)
      .map(_.foldLeft(Seq(0))((h, pm) => (h.head + pm) +: h).reverse)
      .map { a =>
        val tmp = Array.ofDim[Int](m, m)
        for {
          i <- 0 to size
          j <- i to size
        } yield {
          val window = a.slice(i, j + 1)
          tmp(i)(j) = window.indexOf(window.min) + i
        }
        tmp
      }
      .toArray
    out
  }
  private lazy val tables = constructTableLookups(m)

  /** Compute the precomputed table index of a given block */
  private def mapBlockToTable(block: Seq[Int]): Int = {
    var index = 0
    var power = block.size - 2
    for (Seq(l, r) <- block.sliding(2)) {
      if (l < r) { index += 1 << power }
      power -= 1
    }
    index
  }

  /** Precompute a mapping of all blocks to their precomputed RMQ table
    * indices
    */
  private def mapBlocksToTables(blocks: Seq[Seq[Int]]): Array[Int] = {
    val out = blocks.map(mapBlockToTable(_)).toArray
    out
  }
  private lazy val tableIdx = mapBlocksToTables(blocks)

  /** Range Minimum Query using the Berkman--Vishkin algorithm with the
    * simplifications of Bender--Farach-Colton.
    *
    * @param x The first query bound
    * @param y The second query bound
    * @return The minimum between the first and second query
    * @note The order of '''x''' and '''y''' does not matter
    * @note '''Performance''':
    *   - preprocessing: `O(n)`
    *   - query: `O(1)`
    */
  def rmqBV(x: T, y: T): T = {
    val Seq(i, j) = Seq(r(x), r(y)).sorted

    // Compute block and word indices
    val (block_i, block_j) = (i / m, j / m)
    val (word_i, word_j) = (i % m, j % m)

    /** Up to four possible minimum indices are then computed based on the
      * following conditions:
      *   1. `i` and `j` are in the same block:
      *     - one precomputed RMQ from `i` to `j`
      *   2. `i` and `j` are in adjacent blocks:
      *     - one precomputed RMQ from `i` to the end of its block
      *     - one precomputed RMQ from `j` to the beginning of its block
      *   3. `i` and `j` have blocks between them:
      *     - one precomputed RMQ from `i` to the end of its block
      *     - one precomputed RMQ from `j` to the beginning of its block
      *     - two sparse table lookups to fully cover all blocks
      *       between `i` and `j`
      */
    val minIndices = (block_i, block_j) match {
      case (bi, bj) if (block_i == block_j) =>
        val min_i = block_i * m + tables(tableIdx(block_i))(word_i)(word_j)
        Seq(min_i)
      case (bi, bj) if (block_i == block_j - 1) =>
        val min_i = block_i * m + tables(tableIdx(block_i))(word_i)(m - 1)
        val min_j = block_j * m + tables(tableIdx(block_j))(0)(word_j)
        Seq(min_i, min_j)
      case _ =>
        val min_i = block_i * m + tables(tableIdx(block_i))(word_i)(m - 1)
        val min_j = block_j * m + tables(tableIdx(block_j))(0)(word_j)
        val (min_between_l, min_between_r) = {
          val range = math.floor(lg(block_j - block_i - 1)).toInt
          val base_0 = block_i + 1
          val base_1 = block_j - (1 << range)

          val (idx_0, idx_1) = (st(base_0)(range), st(base_1)(range))
          val (min_0, min_1) = (b(idx_0) + idx_0 * m, b(idx_1) + idx_1 * m)
          (min_0, min_1)
        }
        Seq(min_i, min_between_l, min_between_r, min_j)
    }

    // Return the minimum of all possible minimum indices
    e(minIndices.minBy(h(_)))
  }

  /** Range Minimum Query of the Euler Tour.
    *
    * Use this for typical queries.
    *
    * @param x The first query bound
    * @param y The second query bound
    * @return The minimum between the first and second query
    * @note This currently maps to `rmqBV`, but may choose to map to
    * either `rmqBV` or `rmqNaive`
    * @note The order of '''x''' and '''y''' does not matter
    */
  def rmq(x: T, y: T): T = rmqBV(x, y)
}
