package de.qwertyuiop.copret

import Terminal.*
import scala.util.Try
import javax.imageio.ImageIO

object KittyGraphicsProtocol:
  val MaxID = 4294967295L // max 32-bit unsigned

  def checkSupport() =
    queryTerm(s"${apc}Gi=${KittyGraphicsProtocol.MaxID},s=1,v=1,a=q,t=d,f=24;AAAA${st}${csi}c")
      .contains(s"${apc}Gi=${KittyGraphicsProtocol.MaxID}")

  def imageSize(img: os.Path) =
    Try(ImageIO.read(img.toIO)).toOption.map: image =>
      SizePx(image.getWidth, image.getHeight)

  enum Sizing:
    case Absolute(pixels: Int)
    case Relative(ratio: Double)
    case Infer

  case class SizePx(width: Int, height: Int)
  case class SizeCells(width: Int, height: Int)

  case class ImageSize(px: SizePx, cells: SizeCells)

  def fitCellsToScreen(sizeCells: SizeCells, termSize: TermCells): SizeCells =
    val widthRatio  = sizeCells.width.toDouble / termSize.cols
    val heightRatio = sizeCells.height.toDouble / termSize.rows
    if widthRatio > 1 || heightRatio > 1 then
      val ratio = widthRatio max heightRatio
      SizeCells((sizeCells.width / ratio).toInt, (sizeCells.height / ratio).toInt)
    else sizeCells

  def calculateSize(
      size: SizePx,
      requestedWidth: Sizing,
      requestedHeight: Sizing,
      keepAspect: Boolean,
      termSize: TermSize,
  ): ImageSize =
    import Sizing.*

    def scale(dimensionPx: Int, requestedPx: Sizing): Int =
      requestedPx match
        case Absolute(pixels) => pixels
        case Relative(ratio)  => (ratio * dimensionPx).toInt
        case Infer            => dimensionPx

    val cell = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)

    val scaledWidth  = scale(size.width, requestedWidth)
    val scaledHeight = scale(size.height, requestedHeight)

    val pixels =
      if !keepAspect then SizePx(scaledWidth, scaledHeight)
      else
        val widthRatio  = scaledWidth.toDouble / size.width
        val heightRatio = scaledHeight.toDouble / size.height
        if widthRatio < heightRatio then SizePx(scaledWidth, (size.height * widthRatio).toInt)
        else SizePx((size.width * heightRatio).toInt, scaledHeight)

    val sizeCells = SizeCells(pixels.width / cell.width, pixels.height / cell.height)

    ImageSize(pixels, sizeCells)
  end calculateSize

  def showImage(img: os.Path): String = showImage(img, Sizing.Infer, Sizing.Infer, true)
  def showImage(
      img: os.Path,
      requestedWidth: Sizing,
      requestedHeight: Sizing,
      keepAspect: Boolean,
      fitToScreen: Boolean = true,
  ): String =
    import java.util.Base64
    (
      for
        _         <- Option.when(checkSupport())(true)
        termSize  <- Terminal.getSize()
        cursorPos <- Terminal.getCursorPos()
        sizeOrig  <- imageSize(img)

        ImageSize(pixels, cells) = calculateSize(sizeOrig, requestedWidth, requestedHeight, keepAspect, termSize)
      yield
        val size =
          ImageSize(
            pixels,
            if fitToScreen then fitCellsToScreen(cells, termSize.cells)
            else cells,
          )

        val image = Base64.getEncoder.encodeToString(os.read.bytes(img))
        val cell  = SizePx(termSize.pixels.width / termSize.cells.cols, termSize.pixels.height / termSize.cells.rows)

        val commonParams = s"s=${sizeOrig.width},v=${sizeOrig.height},c=${size.cells.width},r=${size.cells.height}"
        logger.info(s"Image size (px): ${sizeOrig.width} x ${sizeOrig.height}")
        logger.info(s"Cellsize (px): ${cell.width} x ${cell.height}")
        logger.info(s"Preferred size (px) ${size.px}")
        logger.info(s"Preferred size (Cells) ${size.cells}")

        if image.length > 4096 then
          val chunks = image.grouped(4096).toVector

          s"${apc}Gf=100,t=d,m=1,a=T,${commonParams};${chunks.head}${st}" +
            chunks.tail.init.map(c => s"${apc}Gm=1;${c}${st}").mkString +
            s"${apc}Gm=0;${chunks.last}${st}"
        else s"${apc}Gf=100,t=d,a=T,${commonParams};${image}${st}"
    ).getOrElse("Could not show image")
  end showImage

  trait Param:
    def code: String

  enum Action(val encoded: Char) extends Param:
    case Transmit                             extends Action('t') // - transmit data
    case TransmitShow                         extends Action('T') // - transmit data and display image
    case Query                                extends Action('q') // - query terminal
    case Show                                 extends Action('p') // - put (display) previous transmitted image
    case Delete(what: DelType, free: Boolean) extends Action('d') // - delete image
    case Animation                            extends Action('a') // - control animation
    case AnimationFrame                       extends Action('f') // - transmit data for animation frames
    case AnimationCompose                     extends Action('c') // - compose animation frames

    def code: String = this match
      case Delete(action, free) =>
        "d=" + (if free then action.code.capitalize else action.code)
      case other                => s"a=${other.encoded}"

  enum Responses(val code: String) extends Param:
    case All     extends Responses("q=0")
    case NoOk    extends Responses("q=1")
    case NoError extends Responses("q=2")

  enum Format(val code: String) extends Param:
    case RGB(width: Int, height: Int)  extends Format(s"f=24,s=${width},v=${height}")
    case RGBA(width: Int, height: Int) extends Format(s"f=32,s=${width},v=${height}")
    case PNG                           extends Format("f=100")

  enum Medium(val code: String) extends Param:
    case Direct       extends Medium("t=d")
    case File         extends Medium("t=f")
    case TempFile     extends Medium("t=t")
    case SharedMemory extends Medium("t=s")

  enum AnimationState(val code: String) extends Param:
    case Stop      extends AnimationState("s=1")
    case WaitFrame extends AnimationState("s=2")
    case Run       extends AnimationState("s=3")

  private def delType(what: Char, freeData: Boolean) =
    s"a=d,d=${if freeData then what.toUpper else what}"
  enum DelType extends Param:
    case AllPlacements
    case ById(id: Int)
    case ByNumber(num: Int)
    case AtCursor
    case Frames
    case AtPosition(x: Int, y: Int, z: Int = -1)
    case ByIdRange(from: Int, to: Int)
    case ByColumn(x: Int)
    case ByRow(y: Int)
    case ByZIndex(z: Int)

    def code: String = this match
      case AllPlacements       => "a"
      case ById(id)            => s"i,i=$id"
      case ByNumber(num)       => s"n,I=$num"
      case AtCursor            => "c"
      case Frames              => "f"
      case AtPosition(x, y, z) =>
        val (what, zarg) = if z >= 0 then ("q", s",z=$z") else ("p", "")
        what + s",x=$x,y=$y" + zarg
      case ByIdRange(x, y)     => s"r,x=$x,y=$y"
      case ByColumn(x)         => s"x,x=$x"
      case ByRow(y)            => s"y,y=$y"
      case ByZIndex(z)         => s"z,z=$z"

  def optionInts(values: (Int, Char)*) =
    values.flatMap((value, key) => Option.when(value >= 0)(s"${key}=${value}")).mkString(",")

  case class SimpleParam(val code: String) extends Param

  def Filesize(bytes: Int)                     = SimpleParam(s"S=$bytes")
  def Offset(bytes: Int)                       = SimpleParam(s"O=$bytes")
  def ImageID(id: Int)                         = SimpleParam(s"i=$id")
  def ImageNumber(num: Int)                    = SimpleParam(s"I=$num")
  def PlacementID(id: Int)                     = SimpleParam(s"p=$id")
  def ZIndex(zindex: Int)                      = SimpleParam(s"z=$zindex")
  def Parent(id: Int)                          = SimpleParam(s"P=$id")
  def ParentPlacement(id: Int)                 = SimpleParam(s"Q=$id")
  def RelativeOffset(x: Int = -1, y: Int = -1) = SimpleParam(
    optionInts((x, 'H'), (y, 'V')),
  )

  val CompressZlib       = SimpleParam("o=z")
  val ContinueChunk      = SimpleParam("m=1")
  val DontMoveCursor     = SimpleParam("C=1")
  val UnicodePlaceholder = SimpleParam("U=1")

  def Crop(
      originX: Int = -1,
      originY: Int = -1,
      width: Int = -1,
      height: Int = -1,
  ) =
    SimpleParam(
      optionInts((originX, 'x'), (originY, 'y'), (width, 'w'), (height, 'h')),
    )

  def AnimFrameUpdate(
      originX: Int = -1,
      originY: Int = -1,
      baseID: Int = -1,
      editID: Int = -1,
  ) =
    SimpleParam:
      optionInts((originX, 'x'), (originY, 'y'), (baseID, 'c'), (editID, 'r'))

  def AnimGap(millis: Int) = SimpleParam(s"z=$millis")
  val AnimUseOverwrite     = SimpleParam("X=1")

  def CellOffset(x: Int = -1, y: Int = -1) = SimpleParam:
    optionInts((x, 'X'), (y, 'Y'))

  def DisplayCells(cols: Int = -1, rows: Int = -1) = SimpleParam:
    optionInts((cols, 'c'), (rows, 'r'))
end KittyGraphicsProtocol
