package ocelot.desktop.ui.widget.card

import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
import ocelot.desktop.ui.widget.card.SoundCardWindow._
import ocelot.desktop.ui.widget.window.{PanelWindow, Window}
import ocelot.desktop.ui.widget.{Oscilloscope, PaddingBox, Widget}
import ocelot.desktop.util.{DrawUtils, Orientation}
import totoro.ocelot.brain.Settings
import totoro.ocelot.brain.entity.sound_card._
import totoro.ocelot.brain.event.{EventBus, SoundCardAudioEvent}

import scala.collection.mutable

class SoundCardWindow(card: SoundCard) extends PanelWindow {
  private val process: AudioProcess = card.board.process.copy()
  private val eventQueue = new mutable.ArrayDeque[SoundCardAudioEvent]()
  private val instructions = new mutable.ArrayDeque[Instruction]()
  private var startTime = System.nanoTime()
  private var passedTime = 0

  private val masterOscilloscope = new Oscilloscope

  private val channelOscilloscopes = Array.fill(numChannels) {
    new Oscilloscope(isTiny = true)
  }

  private val channelWidgets: Array[Channel] = channelOscilloscopes.zip(process.channels).map { case (osc, proc) =>
    new Channel(proc, osc)
  }

  private val patchbays: Array[Patchbay] = (0 until numChannels / 2)
    .map(i => new Patchbay(2 * i, 2 * i + 1, isFirst = i == 0, isLast = i == numChannels / 2 - 1))
    .toArray

  private val existingConnectors = new mutable.ArrayBuffer[Connector]()

  private var eventSub: Option[EventBus.Subscription] = None

  override protected def title: String = s"Sound Card ${card.node.address}"

  override protected def titleMaxLength: Int = 80

  setInner(
    new Widget {
      override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)

      children :+= new PaddingBox(masterOscilloscope, Padding2D(left = 4, right = 4))

      for (i <- 0 until numChannels / 2) {
        children :+= new Widget {
          children :+= channelWidgets(2 * i)
          children :+= patchbays(i)
          children :+= channelWidgets(2 * i + 1)
        }
      }
    },
    padding = Padding2D(left = 4, right = 4, bottom = 8),
  )

  private def numChannels: Int = process.channels.length

  override def onOpening(): Window.State.Value = {
    if (eventSub.isEmpty) {
      eventSub = Some(EventBus.subscribe {
        case event: SoundCardAudioEvent if event.address == card.node.address =>
          eventQueue.synchronized {
            eventQueue.append(event)
          }
      })
    }

    super.onOpening()
  }

  override def onClosed(): Window.State.Value = {
    eventSub.foreach(_.cancel())
    eventSub = None

    super.onClosed()
  }

  override def draw(g: Graphics): Unit = {
    beginDraw(g)
    DrawUtils.windowWithShadow(g, position.x, position.y, size.width, size.height, 1f, 0.5f)
    drawChildren(g)
    drawConnectors(g)
    endDraw(g)
  }

  private def drawConnectors(g: Graphics): Unit = {
    val initColor = ColorScheme("Label")
    for (i <- 0 until numChannels) {
      setColCh(i, initColor)
      setColAm(i, initColor)
      setColFm(i, initColor)
    }

    existingConnectors.clear()

    var colorIdx = 0
    var color = ColorScheme("SoundCardWire0")
    val px = patchbays(0).posLeftCh.x

    for (i <- 0 until numChannels) {
      var hadAny = false

      for (j <- 0 until numChannels) {
        if (i != j) {
          val dstCh = process.channels(j)
          if (dstCh.amplitudeMod.exists(_.modulatorIndex == i)) {
            setColAm(j, color)
            drawConnector(g, px, posCh(i), posAm(j), colorIdx)
            hadAny = true
          }

          if (dstCh.frequencyMod.exists(_.modulatorIndex == i)) {
            setColFm(j, color)
            drawConnector(g, px, posCh(i), posFm(j), colorIdx)
            hadAny = true
          }
        }
      }

      if (hadAny) {
        setColCh(i, color)
        colorIdx += 1
        color = ColorScheme(s"SoundCardWire${colorIdx % 8}")
      }
    }
  }

  private def drawConnector(g: Graphics, px: Float, a: Vector2D, b: Vector2D, colorIdx: Int): Unit = {
    var from = a.y
    var to = b.y
    if (from > to) {
      from = b.y
      to = a.y
    }

    var column = 0
    val existing = existingConnectors.find(con => con.colorIdx == colorIdx)
    if (existing.isDefined) {
      column = existing.get.column
    } else {
      var done = false
      while (column >= 0 && column < 6 && !done) {
        val conflict = existingConnectors.exists(con => {
          con.column == column && con.from <= to && con.to >= from
        })
        if (conflict) {
          column += 1
        } else {
          done = true
        }
      }
    }

    val mx = px + column * 4 + 9
    val col = ColorScheme(s"SoundCardWire${colorIdx % 8}")

    g.line(a, Vector2D(mx, a.y), 2, col)
    g.line(Vector2D(mx, from - 1), Vector2D(mx, to + 1), 2, col)
    g.line(b, Vector2D(mx, b.y), 2, col)

    if (b.x > px) {
      g.sprite(IconSource.Icons.WireArrowRight, b.x - 4, b.y - 4, col)
    } else {
      g.sprite(IconSource.Icons.WireArrowLeft, b.x, b.y - 4, col)
    }

    existingConnectors += Connector(column, colorIdx, from, to)
  }

  private def posCh(i: Int): Vector2D = {
    if (i % 2 == 0) patchbays(i / 2).posLeftCh else patchbays(i / 2).posRightCh
  }

  private def posAm(i: Int): Vector2D = {
    if (i % 2 == 0) patchbays(i / 2).posLeftAm else patchbays(i / 2).posRightAm
  }

  private def posFm(i: Int): Vector2D = {
    if (i % 2 == 0) patchbays(i / 2).posLeftFm else patchbays(i / 2).posRightFm
  }

  private def setColCh(i: Int, col: Color): Unit = {
    if (i % 2 == 0) patchbays(i / 2).colLeftCh = col else patchbays(i / 2).colRightCh = col
  }

  private def setColAm(i: Int, col: Color): Unit = {
    if (i % 2 == 0) patchbays(i / 2).colLeftAm = col else patchbays(i / 2).colRightAm = col
  }

  private def setColFm(i: Int, col: Color): Unit = {
    if (i % 2 == 0) patchbays(i / 2).colLeftFm = col else patchbays(i / 2).colRightFm = col
  }

  override def update(): Unit = {
    super.update()

    val now = System.nanoTime()

    eventQueue.synchronized {
      while (eventQueue.nonEmpty) {
        val event = eventQueue.removeHead()
        if (instructions.isEmpty && process.delay == 0) {
          startTime = now
          passedTime = 0
        }

        instructions.appendAll(event.instructions)

        val masterData = new Array[Float](event.cleanData(0).length)

        for (ch <- 0 until numChannels) {
          val data = event.cleanData(ch)
          channelOscilloscopes(ch).enqueue(data)
          for (i <- data.indices) {
            masterData(i) += data(i) * event.volume
          }
        }

        masterOscilloscope.enqueue(masterData)
      }
    }

    var remaining = ((now - startTime) / 1000000).toInt - passedTime

    while (remaining > 0 && (process.delay > 0 || instructions.nonEmpty)) {
      val subtract = process.delay.min(remaining)
      remaining -= subtract
      passedTime += subtract
      process.delay -= subtract
      val samples = subtract * Settings.get.soundCardSampleRate / 1000
      for (_ <- 0 until samples) {
        for (i <- 0 until numChannels) {
          process.channels(i).generate(process)
        }
      }
      while (process.delay == 0 && instructions.nonEmpty) {
        val instr = instructions.removeHead()
        if (instr.isValid) {
          instr match {
            case i: Instruction.Open =>
              channelWidgets(i.channelIndex).updateEnvelope()
            case i: Instruction.Close =>
              channelWidgets(i.channelIndex).updateEnvelope()
            case _ =>
          }
          instr.execute(process)
        }
      }
    }
  }
}

object SoundCardWindow {
  private val notes = Array("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B")

  private def freqToNote(freq: Float): Option[String] = {
    if (freq <= 0) return None
    val idx = (12 * math.log(freq / 440) / math.log(2) + 48).round.toInt
    val octave = idx / 12
    if (octave < 0) return None
    val note = notes(math.floorMod(idx + 9, 12))
    Some(note + octave.toString)
  }

  private def magToDb(mag: Float): Float = {
    20 * math.log10(mag).toFloat
  }

  private val waves = Array(
    (IconSource.Icons.WaveSine, SignalGenerator.Sine.getClass),
    (IconSource.Icons.WaveTriangle, SignalGenerator.Triangle.getClass),
    (IconSource.Icons.WaveSawtooth, SignalGenerator.Sawtooth.getClass),
    (IconSource.Icons.WaveSquare, SignalGenerator.Square.getClass),
    (IconSource.Icons.WaveNoise, classOf[SignalGenerator.Noise]),
    (IconSource.Icons.WaveLFSR, classOf[SignalGenerator.LFSR]),
  )

  private def drawEnvelope(g: Graphics, env: ADSREnvelope, bounds: Rect2D, elapsedMs: Float): Unit = {
    val envDuration = env.attack + env.decay + env.release

    val decayStart = env.attack
    val releaseStart = decayStart + env.decay

    val elapsedTime = env.phase match {
      case ADSREnvelope.Phase.Closed => 0
      case ADSREnvelope.Phase.Attack => elapsedMs
      case ADSREnvelope.Phase.Decay => elapsedMs
      case ADSREnvelope.Phase.Sustain => releaseStart
      case ADSREnvelope.Phase.Release => releaseStart + elapsedMs
    }

    var col = ColorScheme("SoundCardWaveActive").withAlpha(0.6f)
    for (sx <- 0 until bounds.w.toInt) {
      val t = (sx / bounds.w) * envDuration
      if (t >= elapsedTime) col = col.withAlpha(0.2f)
      val v = if (t < decayStart) {
        t / env.attack
      } else if (t < releaseStart) {
        1 - (t - decayStart) / env.decay * (1 - env.sustain)
      } else {
        (1 - (t - releaseStart) / env.release) * env.sustain
      }
      val h = v * bounds.h
      g.rect(bounds.x + sx, bounds.y + bounds.h - h, 1, h, col)
    }

    val unit = bounds.w / envDuration
    val ay = bounds.y + bounds.h
    val by = bounds.y
    val cy = bounds.y + (1 - env.sustain) * bounds.h
    col = ColorScheme("SoundCardWaveActive")

    var ax = bounds.x
    var bx = bounds.x + env.attack * unit
    g.line(ax, ay, bx, by, 1, col)
    ax = bx
    bx += env.decay * unit
    g.line(ax, by, bx, cy, 1, col)
    ax = bx
    bx += env.release * unit
    g.line(ax, cy, bx, ay, 1, col)
  }

  private class Channel(channel: AudioChannel, oscilloscope: Oscilloscope) extends Widget {
    private var lastEvent = System.nanoTime()

    def updateEnvelope(): Unit = {
      lastEvent = System.nanoTime()
    }

    children :+= new PaddingBox(
      new Widget {
        override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)

        children :+= oscilloscope

        children :+= new Widget {
          override def minimumSize: Size2D = Size2D(300, 50)

          override def draw(g: Graphics): Unit = {
            for (((icon, clazz), i) <- waves.zipWithIndex) {
              val active = clazz.isInstance(channel.generator)
              val color = if (active) ColorScheme("SoundCardWaveActive") else ColorScheme("SoundCardWaveOff")
              g.sprite(icon, position.x + 24 * i + 2, position.y + 4, color)
            }

            var text = ""
            g.setSmallFont()
            g.foreground = ColorScheme("Label")

            val note = freqToNote(channel.frequency)
            text = if (note.isDefined) {
              f"Freq: ${channel.frequency}%.1f (${note.get})"
            } else {
              f"Freq: ${channel.frequency}%.1f"
            }
            g.text(position.x + 1, position.y + 22, text, shrink = 1)

            text = f"Vol:  ${channel.volume}%.2f (${magToDb(channel.volume)}%.1fdB)"
            g.text(position.x + 1, position.y + 32, text, shrink = 1)

            text = f"Pos:  ${channel.offset}%.4f"
            g.text(position.x + 1, position.y + 42, text, shrink = 1)

            if (channel.envelope.isEmpty) {
              text = if (channel.isOpen) "Open" else "Closed"
              g.text(position.x + width / 2, position.y + 22, text, shrink = 1)

              text = "No envelope"
              g.text(position.x + width / 2 + 1, position.y + 32, text, shrink = 1)
            }

            for (env <- channel.envelope) {
              drawEnvelope(g, env, Rect2D(position.x + width / 2 + 2, position.y + 2, width / 2 - 4, 16),
                (System.nanoTime() - lastEvent) / 1e6f)

              g.foreground = ColorScheme("Label")

              text = f"A: ${env.attack / 1000f}%.3f"
              g.text(position.x + width / 2 + 1, position.y + 22, text, shrink = 1)

              text = f"D: ${env.decay / 1000f}%.3f"
              g.text(position.x + width / 2 + 70, position.y + 22, text, shrink = 1)

              text = f"S: ${env.sustain}%.3f"
              g.text(position.x + width / 2 + 1, position.y + 32, text, shrink = 1)

              text = f"R: ${env.release / 1000f}%.3f"
              g.text(position.x + width / 2 + 70, position.y + 32, text, shrink = 1)
            }

            for (fm <- channel.frequencyMod) {
              text = f"FM depth: ${fm.depth}%.1f"
              g.text(position.x + width / 2 + 1, position.y + 42, text, shrink = 1)
            }

            g.setNormalFont()
          }
        }
      },
      Padding2D.equal(4),
    )
  }

  private class Patchbay(leftIdx: Int, rightIdx: Int, isFirst: Boolean, isLast: Boolean) extends Widget {
    var colLeftCh: Color = ColorScheme("Label")
    var colLeftAm: Color = ColorScheme("Label")
    var colLeftFm: Color = ColorScheme("Label")
    var colRightCh: Color = ColorScheme("Label")
    var colRightAm: Color = ColorScheme("Label")
    var colRightFm: Color = ColorScheme("Label")

    override def minimumSize: Size2D = Size2D(74, 100)

    override def maximumSize: Size2D = Size2D(74, Float.PositiveInfinity)

    def posLeftCh: Vector2D = Vector2D(position.x + 18, position.y + 22)

    def posLeftAm: Vector2D = Vector2D(position.x + 18, position.y + 38)

    def posLeftFm: Vector2D = Vector2D(position.x + 18, position.y + 54)

    def posRightCh: Vector2D = Vector2D(position.x + width - 20, position.y + 22)

    def posRightAm: Vector2D = Vector2D(position.x + width - 20, position.y + 38)

    def posRightFm: Vector2D = Vector2D(position.x + width - 20, position.y + 54)

    override def draw(g: Graphics): Unit = {
      val x = position.x
      var y = position.y
      val w = width
      var h = height

      val sy = if (isFirst) 10 else 0
      y += sy
      val sh = (if (isFirst) 10 else 0) + (if (isLast) 50 else 0)
      h -= sh

      g.sprite(IconSource.LightPanel.BorderL, x, y, 4, h)
      g.sprite(IconSource.LightPanel.BorderR, x + w - 4, y, 4, h)
      g.sprite(IconSource.LightPanel.Fill, x + 4, y, w - 8, h)

      if (isFirst) {
        g.sprite(IconSource.LightPanel.CornerTL, x, y - 4, 4, 4)
        g.sprite(IconSource.LightPanel.CornerTR, x + w - 4, y - 4, 4, 4)
        g.sprite(IconSource.LightPanel.BorderT, x + 4, y - 4, w - 8, 4)
      }

      if (isLast) {
        g.sprite(IconSource.LightPanel.CornerBL, x, y + h, 4, 4)
        g.sprite(IconSource.LightPanel.CornerBR, x + w - 4, y + h, 4, 4)
        g.sprite(IconSource.LightPanel.BorderB, x + 4, y + h, w - 8, 4)
        g.sprite(IconSource.LightPanel.Vent, x, y + h + 8, w, 38)
      }

      for (i <- 0 until 6) {
        g.rect(x + 26 + i * 4, y - 2, 2, h + 4, ColorScheme("SoundCardWireOff"))
      }

      y -= sy
      h += sh

      g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 16, 18, 14)
      g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 32, 18, 14)
      g.sprite(IconSource.LightPanel.BookmarkLeft, x, y + 48, 18, 14)

      g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 16, 20, 14)
      g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 32, 20, 14)
      g.sprite(IconSource.LightPanel.BookmarkRight, x + w - 20, y + 48, 20, 14)

      g.setSmallFont()
      g.background = Color.Transparent

      g.foreground = colLeftCh
      g.text(x, y + 18, s"C${leftIdx + 1}")
      g.foreground = colLeftAm
      g.text(x, y + 34, "AM")
      g.foreground = colLeftFm
      g.text(x, y + 50, "FM")

      g.foreground = colRightCh
      g.text(x + w - 18, y + 18, s"C${rightIdx + 1}")
      g.foreground = colRightAm
      g.text(x + w - 18, y + 34, "AM")
      g.foreground = colRightFm
      g.text(x + w - 18, y + 50, "FM")

      g.setNormalFont()
    }
  }

  private case class Connector(column: Int, colorIdx: Int, from: Float, to: Float)
}
