package ocelot.desktop.node

import ocelot.desktop.audio._
import ocelot.desktop.entity.OcelotCard
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
import ocelot.desktop.geometry.Vector2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.inventory.SyncedInventory
import ocelot.desktop.node.ComputerAwareNode._
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.BrainEvent
import ocelot.desktop.ui.event.handlers.DiskActivityHandler
import ocelot.desktop.ui.widget.ComputerErrorMessageLabel
import ocelot.desktop.util.Messages
import ocelot.desktop.{ColorScheme, OcelotDesktop}
import totoro.ocelot.brain.Settings
import totoro.ocelot.brain.entity.traits.{Entity, Environment, WorkspaceAware}
import totoro.ocelot.brain.event._

import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.util.Random

abstract class ComputerAwareNode(entity: Entity with Environment with WorkspaceAware)
  extends EntityNode(entity)
    with SyncedInventory
    with DiskActivityHandler
    with ShiftClickNode {

  // access should be synchronized because messages are added in the update thread
  private val messages = ArrayBuffer.empty[(Float, ComputerErrorMessageLabel)]

  private def addErrorMessage(message: ComputerErrorMessageLabel): Unit = messages.synchronized {
    messages += ((0f, message))
  }

  private case class LogParticle(
    var time: Float = -LogParticleGrow,
    angle: Float = Random.between(0f, 2 * math.Pi.toFloat * LogParticleMaxAngle)
  )

  // access should be synchronized because log particles are added in the update thread
  private val logParticles = mutable.ArrayDeque.empty[LogParticle]

  private def addLogParticle(): Unit = logParticles.synchronized {
    if (logParticles.length < MaxLogParticles) {
      logParticles += LogParticle()
    }
  }

  private lazy val soundCardSounds: (SoundStream, SoundSource) = Audio.newStream(SoundCategory.Records)
  private def soundCardStream: SoundStream = soundCardSounds._1
  private def soundCardSource: SoundSource = soundCardSounds._2

  eventHandlers += {
    case BrainEvent(event: MachineCrashEvent) =>
      val message = Messages.lift(event.message) match {
        case Some(message) =>
          logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message code ${event.message}: $message")
          message

        case None =>
          logger.info(s"[EVENT] Machine crash (address = ${event.address})! Message: ${event.message}")
          event.message
      }

      addErrorMessage(new ComputerErrorMessageLabel(this, message))

    case BrainEvent(event: BeepEvent) if !Audio.isDisabled =>
      BeepGenerator.newBeep(".", event.frequency, event.duration).play()

    case BrainEvent(event: BeepPatternEvent) if !Audio.isDisabled =>
      BeepGenerator.newBeep(event.pattern, 1000, 200).play()

    case BrainEvent(event: SoundCardAudioEvent) if !Audio.isDisabled =>
      val samples = SoundSamples(event.data, Settings.get.soundCardSampleRate, SoundSamples.Format.Mono8)
      soundCardStream.enqueue(samples)
      soundCardSource.volume = event.volume

    case BrainEvent(_: SelfDestructingCardBoomEvent) =>
      OcelotDesktop.workspace.runLater(() => {
        SoundSource.MinecraftExplosion.play()
        destroy()
      })

    case BrainEvent(_: OcelotCard.LogEvent) =>
      addLogParticle()
  }

  override def drawParticles(g: Graphics): Unit = {
    drawMessageParticles(g)
    drawLogParticles(g)
  }

  private def drawMessageParticles(g: Graphics): Unit = messages.synchronized {
    for ((time, message) <- messages.reverseIterator) {
      message.position = message.initialPosition + Vector2D(0, -MaxErrorMessageDistance * time)
      message.alpha = 1 - time
      message.draw(g)
    }
  }

  private def drawLogParticles(g: Graphics): Unit = logParticles.synchronized {
    for (particle <- logParticles) {
      val size = (1 + particle.time / LogParticleGrow).clamp() * LogParticleSize
      val offset = particle.time.clamp() * LogParticleMoveDistance
      val alpha = 1 - particle.time.clamp()

      val r1 = (bounds.w max bounds.h) / math.sqrt(2) + offset + LogParticlePadding
      val r2 = r1 + size

      for (i <- 0 until LogParticleCount) {
        val angle = particle.angle + (2 * math.Pi).toFloat * i / LogParticleCount
        val v = Vector2D.unit(angle)
        val p1 = v * r1 + bounds.center
        val p2 = v * r2 + bounds.center
        g.line(p1, p2, 1f, ColorScheme("LogParticle").mapA(_ => alpha))
      }
    }
  }

  override def update(): Unit = {
    logParticles.synchronized {
      logParticles.foreach(particle => particle.time += LogParticleMoveSpeed * UiHandler.dt)
      logParticles.filterInPlace(_.time <= 1f)
    }

    messages.synchronized {
      messages.mapInPlace { case (t, message) => (t + ErrorMessageMoveSpeed * UiHandler.dt, message) }
      messages.filterInPlace(_._1 <= 1f)
    }

    super.update()
  }
}

object ComputerAwareNode {
  private val MaxErrorMessageDistance: Float = 50f
  private val ErrorMessageMoveSpeed: Float = 0.5f

  private val MaxLogParticles: Int = 15
  private val LogParticleMaxAngle: Float = 0.25f
  private val LogParticleCount: Int = 12
  private val LogParticleGrow: Float = 0.25f
  private val LogParticlePadding: Float = 2f
  private val LogParticleSize: Float = 10f
  private val LogParticleMoveSpeed: Float = 1f
  private val LogParticleMoveDistance: Float = 20f
}
