package ocelot.desktop.ui.widget

import ocelot.desktop.ColorScheme
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.widget.Text.{getWidth, wrap, CharHeight}
import ocelot.desktop.util.DrawUtils
import totoro.ocelot.brain.util.FontUtils

import scala.collection.mutable.ArrayBuffer

class Text extends Widget {
  def text: String = ""

  private var cachedText = ""
  private var cachedWrapWidth = None: Option[Int]
  private var cachedLines = ArrayBuffer.empty[String]
  private var cachedWidth = 0

  def color: Color = ColorScheme("Label")

  def wrapWidth: Option[Int] = None

  private def recomputeIfNeeded(): Unit = {
    if (text != cachedText || cachedWrapWidth != wrapWidth) {
      recomputeProperties()
    }
  }

  final def textWidth: Int = {
    recomputeIfNeeded()
    cachedWidth
  }

  final def textHeight: Int = {
    recomputeIfNeeded()
    cachedLines.length
  }

  final def textLines: Iterable[String] = {
    recomputeIfNeeded()
    cachedLines
  }

  override def minimumSize: Size2D = Size2D(textWidth, CharHeight * (textHeight max 1))
  override def maximumSize: Size2D = minimumSize.copy(width = Float.PositiveInfinity)

  override def draw(g: Graphics): Unit = {
    g.background = Color.Transparent
    g.foreground = color
    DrawUtils.drawTextLines(g, position.x, position.y, textLines)
  }

  private def recomputeProperties(): Unit = {
    cachedText = text
    cachedWrapWidth = wrapWidth
    cachedLines = wrap(cachedText, cachedWrapWidth)
    cachedWidth = getWidth(cachedLines)
  }
}

object Text {
  val CharHeight = 16

  private def getWidth(codepoint: Int): Int = FontUtils.wcwidth(codepoint)

  private def getWidth(line: String): Int =
    line.codePoints().map(getWidth).sum()

  private def getWidth(lines: Iterable[String]): Int =
    lines.iterator.map(getWidth).maxOption.getOrElse(0)

  def wrap(text: String, wrapWidth: Option[Int]): ArrayBuffer[String] = {
    val lines = text.linesIterator
      .flatMap(line => {
        wrapWidth match {
          case Some(wrapWidth) if getWidth(line) > wrapWidth => wrapLine(line, wrapWidth)
          case _ => Some(line)
        }
      })
      .to(ArrayBuffer)

    val lastNonEmptyLineIdx = lines.lastIndexWhere(_.nonEmpty)
    lines.takeInPlace(lastNonEmptyLineIdx + 1)

    lines
  }

  private def wrapLine(line: String, wrapWidth: Int): Iterable[String] = {
    // adapted from cheburator's message splitter

    val lines = ArrayBuffer.empty[String]

    var lineStart = 0
    var pos = -1
    var lineWidth = 0
    var lastSpacePos = -1
    var lastSpaceSize = 1
    var spaceSeq = false

    def firstLine: Boolean = lines.isEmpty

    def pushLine(line: String): Unit = {
      lines += line

      lastSpacePos = -1
      lastSpaceSize = 1
      lineWidth = 0
      spaceSeq = false
    }

    while (pos + 1 < line.length) {
      pos += 1

      val (c, count) = if (pos + 1 < line.length && Character.isSurrogatePair(line(pos), line(pos + 1))) {
        (Character.toCodePoint(line(pos), line(pos + 1)), 2)
      } else (line(pos): Int, 1)

      val isSpace = Character.isSpaceChar(c)

      if (isSpace && pos == lineStart && !firstLine) {
        // skip leading space
        lineStart += count
      } else {
        if (!isSpace) {
          spaceSeq = false
        }

        if (isSpace && !spaceSeq) {
          lastSpacePos = pos
          lastSpaceSize = count
          spaceSeq = true
        }

        lineWidth += getWidth(c)

        if (lineWidth > wrapWidth) {
          if (lastSpacePos >= (pos - lineStart) / 2) {
            val newLine = line.substring(lineStart, lastSpacePos)

            // `+ lastSpaceSize` starts the line after the last whitespace character
            // we set pos to the same position -- but before it gets there, it'll get incremented by `count`,
            // so we do the subtraction
            lineStart = lastSpacePos + lastSpaceSize
            pos = lastSpacePos + lastSpaceSize - count

            pushLine(newLine)
          } else {
            val newLine = line.substring(lineStart, pos)
            lineStart = pos
            pos -= 1

            pushLine(newLine)
          }
        }
      }

      // subtract one, which we'll add back at the start of the next iteration
      pos += count - 1
    }

    if (lineStart != pos + 1) {
      pushLine(line.substring(lineStart))
    }

    lines
  }
}
