package ocelot.desktop.audio

import ocelot.desktop.Settings
import ocelot.desktop.util.{Logging, Transaction}
import org.lwjgl.LWJGLException
import org.lwjgl.openal.{AL, AL10, ALC10}

import java.nio.{ByteBuffer, ByteOrder}
import scala.collection.mutable
import scala.util.control.Exception.catching

object Audio extends Logging {
  val sampleRate: Int = 44100

  private val sources = new mutable.HashMap[SoundSource, Int]
  private var _disabled = true

  /** Should be called _before_ initializing any sound-related resources */
  def init(): Unit = {
    try {
      AL.create()
      logger.info(s"OpenAL device: ${ALC10.alcGetString(AL.getDevice, ALC10.ALC_DEVICE_SPECIFIER)}")
      _disabled = false
    } catch {
      case e: LWJGLException =>
        logger.error("Unable to initialize OpenAL. Disabling sound")
        e.printStackTrace()
    }
  }

  def isDisabled: Boolean = _disabled

  def newStream(
    soundCategory: SoundCategory.Value,
    pitch: Float = 1f,
    volume: Float = 1f,
  ): (SoundStream, SoundSource) = {
    var source: SoundSource = null

    val stream = new SoundStream {
      override def enqueue(samples: SoundSamples): Unit = Audio.synchronized {
        OpenAlException.ignoring {
          Transaction.runAbortable { tx =>
            if (!Audio.isDisabled) {
              val sourceId = if (sources.contains(source)) {
                sources(source)
              } else {
                Transaction.run { tx =>
                  val sourceId = AL10W.alGenSources()
                  tx.onFailure {
                    AL10W.alDeleteSources(sourceId)
                  }

                  AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)

                  setPosition(sourceId, source)
                  setGain(sourceId, source)

                  sources.put(source, sourceId)

                  sourceId
                }
              }

              cleanupSourceBuffers(sourceId)

              val bufferId = samples.genBuffer().getOrElse { tx.abort() }
              tx.onFailure { AL10W.alDeleteBuffers(bufferId) }
              AL10W.alSourceQueueBuffers(sourceId, bufferId)

              if (AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) != AL10.AL_PLAYING) {
                AL10W.alSourcePlay(sourceId)
              }
            }
          }
        }
      }
    }

    source = SoundSource.fromStream(stream, soundCategory, looping = false, pitch, volume).build()
    (stream, source)
  }

  def getSourceStatus(source: SoundSource): SoundSource.Status.Value = synchronized {
    if (!sources.contains(source))
      return SoundSource.Status.Stopped

    val sourceId = sources(source)

    catching(classOf[OpenAlException]) opt AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
      case Some(AL10.AL_PLAYING) => SoundSource.Status.Playing
      case Some(AL10.AL_PAUSED) => SoundSource.Status.Paused
      case _ => SoundSource.Status.Stopped
    }
  }

  def playSource(source: SoundSource): Unit = synchronized {
    OpenAlException.ignoring {
      if (Audio.isDisabled) {
        return
      }

      if (getSourceStatus(source) == SoundSource.Status.Playing) {
        return
      }

      if (sources.contains(source)) {
        AL10W.alSourcePlay(sources(source))
        return
      }

      Transaction.runAbortable { tx =>
        val sourceId = AL10W.alGenSources()
        tx.onFailure { AL10W.alDeleteSources(sourceId) }

        source.kind match {
          case SoundSource.Kind.Buffer(buffer) =>
            buffer.bufferId match {
              case Some(bufferId) =>
                AL10W.alSourcei(sourceId, AL10.AL_BUFFER, bufferId)

              case None =>
                logger.error(s"Called play on a SoundBuffer $buffer with bufferId = None")
                tx.abort()
            }

          case SoundSource.Kind.Samples(samples) =>
            Transaction.run { innerTx =>
              val bufferId = samples.genBuffer().getOrElse { tx.abort() }
              innerTx.onFailure { AL10W.alDeleteBuffers(bufferId) }
              AL10W.alSourceQueueBuffers(sourceId, bufferId)
            }

          case SoundSource.Kind.Stream(_) =>
        }

        AL10W.alSourcef(sourceId, AL10.AL_PITCH, source.pitch)
        setPosition(sourceId, source)
        AL10W.alSourcei(sourceId, AL10.AL_LOOPING, if (source.looping) AL10.AL_TRUE else AL10.AL_FALSE)
        setGain(sourceId, source)
        AL10W.alSourcePlay(sourceId)

        sources.put(source, sourceId)
      }
    }
  }

  def pauseSource(source: SoundSource): Unit = synchronized {
    OpenAlException.ignoring {
      if (Audio.isDisabled)
        return

      if (getSourceStatus(source) == SoundSource.Status.Paused)
        return

      if (sources.contains(source)) {
        AL10W.alSourcePause(sources(source))
      }
    }
  }

  def stopSource(source: SoundSource): Unit = synchronized {
    OpenAlException.ignoring {
      if (Audio.isDisabled) {
        return
      }

      if (getSourceStatus(source) == SoundSource.Status.Stopped)
        return

      if (sources.contains(source)) {
        AL10W.alSourceStop(sources(source))
      }
    }
  }

  private def setPosition(sourceId: Int, source: SoundSource): Unit = {
    AL10W.alSource3f(sourceId, AL10.AL_POSITION, source.position.x, source.position.y, source.position.z)
  }

  private def setGain(sourceId: Int, source: SoundSource): Unit = {
    AL10W.alSourcef(
      sourceId,
      AL10.AL_GAIN,
      source.volume * SoundCategory.getSettingsValue(source.soundCategory) * Settings.get.volumeMaster,
    )
  }

  def update(): Unit = synchronized {
    if (isDisabled) return

    sources.filterInPlace { case (source, sourceId) =>
      OpenAlException.defaulting(false) {
        cleanupSourceBuffers(sourceId)

        setPosition(sourceId, source)
        setGain(sourceId, source)

        AL10W.alGetSourcei(sourceId, AL10.AL_SOURCE_STATE) match {
          case AL10.AL_STOPPED =>
            deleteSource(sourceId)
            false
          case _ => true
        }
      }
    }
  }

  def destroy(): Unit = synchronized {
    if (isDisabled) return

    for ((_, sourceId) <- sources) {
      OpenAlException.ignoring {
        deleteSource(sourceId)
      }
    }

    sources.clear()
    AL.destroy()
    _disabled = true
  }

  @throws[OpenAlException]
  private def deleteSource(sourceId: Int): Unit = {
    AL10W.alSourceStop(sourceId)
    cleanupSourceBuffers(sourceId)
    AL10W.alDeleteSources(sourceId)
  }

  @throws[OpenAlException]
  private def cleanupSourceBuffers(sourceId: Int): Unit = {
    val count = AL10W.alGetSourcei(sourceId, AL10.AL_BUFFERS_PROCESSED)
    if (count <= 0) return

    val buff = ByteBuffer.allocateDirect(count * 4)
    buff.order(ByteOrder.nativeOrder())
    val buf = buff.asIntBuffer()

    AL10W.alSourceUnqueueBuffers(sourceId, buf)

    for (i <- 0 until count) {
      AL10W.alDeleteBuffers(buf.get(i))
    }
  }

  def removeAllSources(): Unit = synchronized {
    for (sourceId <- sources.values) {
      deleteSource(sourceId)
    }

    sources.clear()
  }
}
