package ocelot.desktop.graphics

import ocelot.desktop.geometry.Rect2D
import ocelot.desktop.util.{Logging, Resource}
import org.lwjgl.opengl.GL11
import totoro.ocelot.brain.util.FontUtils

import java.awt.image.{BufferedImage, DataBufferByte, IndexColorModel}
import java.io.InputStream
import java.nio.ByteBuffer
import scala.collection.immutable.IntMap
import scala.collection.mutable
import scala.io.{Codec, Source}

class Font(val name: String, val fontSize: Int) extends Resource with Logging {
  val AtlasWidth = 4096
  val AtlasHeight = 4096
  private var glyphCount = 0
  private var outOfRangeGlyphCount = 0

  private val atlas: BufferedImage = {
    val icmArr = Array(0.toByte, 0xff.toByte)
    val icm = new IndexColorModel(1, 2, icmArr, icmArr, icmArr, icmArr)
    new BufferedImage(AtlasWidth, AtlasHeight, BufferedImage.TYPE_BYTE_BINARY, icm)
  }

  var map: IntMap[Rect2D] = _
  var texture: Texture = _

  init()

  def charWidth(codePoint: Int): Int = (map.getOrElse(codePoint, map('?')).w * AtlasWidth).toInt

  private def init(): Unit = {
    logger.info(f"Loading font $name")

    val rawMap = new mutable.HashMap[Int, Rect2D]

    var ox = 0
    var oy = 0

    val resource = getClass.getResource(f"/ocelot/desktop/$name.hex")
    val source = Source.fromInputStream(resource.getContent.asInstanceOf[InputStream])(Codec.UTF8)

    for (line <- source.getLines()) {
      val colon = line.indexOf(':')
      val charCode = Integer.parseInt(line.substring(0, colon), 16)

      if (charCode >= 0 && charCode < FontUtils.codepoint_limit) {
        val expectedWidth = FontUtils.wcwidth(charCode)

        // skip control characters
        if (expectedWidth >= 1) {
          val width = (line.length - colon - 1) * 4 / fontSize

          if (ox + width > AtlasWidth) {
            ox = 0
            oy += fontSize
          }

          rawMap(charCode) = Rect2D(
            ox.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
            oy.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float],
            width.asInstanceOf[Float] / AtlasWidth.asInstanceOf[Float],
            fontSize.asInstanceOf[Float] / AtlasHeight.asInstanceOf[Float],
          )

          var x = 0
          var y = 0

          for (char <- line.substring(colon + 1)) {
            for (i <- 3 to 0 by -1) {
              val bit = (Character.digit(char, 16) >> i) & 1

              atlas.setRGB(ox + x, oy + y,
                bit match {
                  case 0 => 0x00000000
                  case 1 => 0xffffffff
                })

              x += 1

              if (x == width) {
                x = 0
                y += 1
              }
            }
          }

          ox += width
          glyphCount += 1
        }
      } else {
        outOfRangeGlyphCount += 1
        logger.warn(String.format("Unicode font contained unexpected glyph: U+%04X, ignoring", charCode))
      }
    }

    val size = atlas.getRaster.getDataBuffer.getSize
    logger.info(s"Packed $glyphCount glyphs into ${AtlasWidth}x$AtlasHeight 1-bit texture ($size bytes)")
    logger.info(s"Skipped $outOfRangeGlyphCount non-BMP glyphs")

    map = IntMap.from(rawMap)
    texture = makeTexture()
  }

  private def makeTexture(): Texture = {
    val bytes = atlas.getRaster.getDataBuffer.asInstanceOf[DataBufferByte]
    val buf = ByteBuffer.allocateDirect(AtlasWidth * AtlasHeight * 1)

    for (b <- bytes.getData) { // :/
      buf.put(((b >> 7 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 6 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 5 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 4 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 3 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 2 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 1 & 1) * 255).asInstanceOf[Byte])
      buf.put(((b >> 0 & 1) * 255).asInstanceOf[Byte])
    }

    buf.flip()

    val tex = new Texture()
    tex.bind()
    GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1)
    tex.set(AtlasWidth, AtlasHeight, GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, buf)

    tex
  }

  override def freeResource(): Unit = {
    super.freeResource()
    texture.freeResource()
  }
}

object Font extends Resource {
  val NormalFont = new Font("unscii-16", 16)
  val SmallFont = new Font("unscii-8", 8)

  override def freeResource(): Unit = {
    super.freeResource()
    NormalFont.freeResource()
    SmallFont.freeResource()
  }
}
