package ocelot.desktop.ui.widget

import ocelot.desktop.ColorScheme
import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics
import ocelot.desktop.ui.layout.{Layout, LinearLayout}
import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPadding}
import ocelot.desktop.util.{DrawUtils, Orientation}

import scala.annotation.tailrec
import scala.collection.mutable.ArrayBuffer

abstract class LogWidget extends Widget {
  override protected val layout: Layout = new LinearLayout(this, orientation = Orientation.Vertical)

  private object MessageListWidget extends Widget { messageList =>
    override protected val layout: Layout = new Layout(this)

    // NOTE: access to entries must be synchronized!
    private val entries = ArrayBuffer.empty[Entry]

    val dummyEntry = new Entry("", nextMessageY)

    override def minimumSize: Size2D = Size2D(dummyEntry.minimumSize.width + 2 * EntryMargin, nextMessageY)

    private def nextMessageY: Float = entries.synchronized {
      entries.lastOption.map(_.maxY).getOrElse(0f) + EntryMargin
    }

    def addEntry(message: String): Unit = entries.synchronized {
      entries += new Entry(message, nextMessageY)
      parent.get.recalculateBoundsAndRelayout()
    }

    def removeFirst(count: Int): Unit = entries.synchronized {
      if (count >= entries.length) {
        entries.clear()
      } else {
        entries.dropInPlace(count)

        val offset = entries(0).y - EntryMargin

        if (offset > 0) {
          for (entry <- entries) {
            entry.y -= offset
          }
        }
      }

      parent.get.recalculateBoundsAndRelayout()
    }

    @tailrec
    private def firstVisibleIdxSearch(y: Float, from: Int = 0, to: Int = entries.length): Int = {
      // search the range [from, to)
      if (to <= from) to
      else {
        val idx = from + (to - from - 1) / 2
        val entryY = entries(idx).maxY

        if (entryY <= y) firstVisibleIdxSearch(y, idx + 1, to)
        else firstVisibleIdxSearch(y, from, idx)
      }
    }

    override def draw(g: Graphics): Unit = entries.synchronized {
      val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y)

      for (entry <- entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y)) {
        entry.draw(g)
      }

      super.draw(g)
    }

    class Entry(message: String, var y: Float) {
      private val lines = Text.wrap(message, Some(textWidth))

      def minimumSize: Size2D = Size2D(
        textWidth * 8 +
          EntryPadding * 2 +
          BorderThickness * 2,
        Text.CharHeight * (lines.length max 1) +
          EntryPadding * 2 +
          BorderThickness * 2
      )

      private def size: Size2D = Size2D(
        minimumSize.width max (messageList.size.width - 2 * EntryMargin),
        minimumSize.height
      )

      private def position: Vector2D = Vector2D(EntryMargin, y)

      def maxY: Float = y + minimumSize.height

      def absoluteBounds: Rect2D = Rect2D(position + messageList.position, size)

      def draw(g: Graphics): Unit = {
        val innerBounds = absoluteBounds.inflate(-BorderThickness)
        g.rect(innerBounds, ColorScheme("LogEntryBackground"))
        DrawUtils.ring(
          g,
          absoluteBounds.x,
          absoluteBounds.y,
          absoluteBounds.w,
          absoluteBounds.h,
          BorderThickness,
          ColorScheme("LogEntryBorder"),
        )

        g.foreground = ColorScheme("Label")
        DrawUtils.drawTextLines(
          g,
          absoluteBounds.x + BorderThickness + EntryPadding,
          absoluteBounds.y + BorderThickness + EntryPadding,
          lines
        )
      }
    }
  }

  private val scrollView = new ScrollView(MessageListWidget)

  children :+= new PaddingBox(scrollView, Padding2D.equal(BorderThickness))

  def scrollToEnd: Boolean = scrollView.scrollToEnd

  def scrollToEnd_=(value: Boolean): Unit = {
    scrollView.scrollToEnd = value
  }

  protected def messages: Iterable[String]

  protected def textWidth: Int

  override def minimumSize: Size2D = Size2D(
    MessageListWidget.minimumSize.width + 2 * BorderThickness,

    MessageListWidget.dummyEntry.maxY +
      EntryMargin +
      BorderThickness * 2, // outer border (top + bottom)
  )

  def onMessagesAdded(count: Int): Unit = {
    for (message <- messages.view.takeRight(count)) {
      MessageListWidget.addEntry(message)
    }
  }

  def onMessagesRemoved(count: Int): Unit = {
    MessageListWidget.removeFirst(count)
  }

  override def draw(g: Graphics): Unit = {
    g.rect(bounds, ColorScheme("LogBackground"))
    DrawUtils.ring(g, bounds.x, bounds.y, bounds.w, bounds.h, BorderThickness, ColorScheme("LogBorder"))

    super.draw(g)
  }
}

object LogWidget {
  val BorderThickness = 2
  val EntryPadding = 2
  val EntryMargin = 2
}
