package ocelot.desktop.util

import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer

object Profiler {
  private val timeEntries = mutable.HashMap[String, TimeEntry]()

  def measure(name: String)(f: => Unit): Unit = {
    startTimeMeasurement(name)

    try {
      f
    } finally {
      endTimeMeasurement(name)
    }
  }

  private def startTimeMeasurement(name: String): Unit = {
    val entry = timeEntries.getOrElseUpdate(name, new TimeEntry)
    entry.startTime = System.nanoTime().toDouble / 1e9
  }

  private def endTimeMeasurement(name: String): Unit = {
    val entry = timeEntries.getOrElseUpdate(name, new TimeEntry)
    entry.addSample(System.nanoTime().toDouble / 1e9 - entry.startTime)
  }

  def report(): Array[String] = {
    var res = Array[String]()
    val width = timeEntries.map(_._1.length).max
    for ((name, entry) <- timeEntries.iterator.toArray.sortBy(_._1).reverse) {
      val (p5, p50, p95) = entry.percentiles()
      res +:= s"${name.padTo(width, ' ')} │ 5% ${formatTime(p5)} │ 50% ${formatTime(p50)} │ 95% ${formatTime(p95)} │ last ${formatTime(entry.lastSample)}"
    }
    res
  }

  private def formatTime(v: Double): String = {
    val unit = if (v < 1e-6)
      ("ns", 1e-9d)
    else if (v < 1e-3d)
      ("us", 1e-6d)
    else if (v < 1)
      ("ms", 1e-3d)
    else
      ("s", 1d)

    f"${v / unit._2}%6.2f ${unit._1}"
  }

  private class TimeEntry {
    var numSamples = 0
    var startTime = 0d
    var lastSample = 0d

    private val samples = ArrayBuffer[Double]()
    private var samplesSorted = ArrayBuffer[Double]()

    def addSample(sample: Double): Unit = {
      lastSample = sample
      numSamples += 1

      if (samples.length < 1024)
        samples.append(lastSample)
      else
        samples(numSamples % 1024) = lastSample
    }

    def percentiles(): (Double, Double, Double) = {
      if (samples.length < 3)
        return (lastSample, lastSample, lastSample)

      samplesSorted = samples.sorted.reverse
      val p5 = (samples.length.toDouble * 0.05).round.toInt
      val p50 = samples.length / 2
      val p95 = samples.length - p5 - 1
      (samplesSorted(p5), samplesSorted(p50), samplesSorted(p95))
    }
  }
}
