package ocelot.desktop.ui.widget

import ocelot.desktop.ColorScheme
import ocelot.desktop.color.RGBAColorNorm
import ocelot.desktop.geometry.{Padding2D, Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.ui.UiHandler
import ocelot.desktop.ui.event.handlers.MouseHandler
import ocelot.desktop.ui.event.{ClickEvent, MouseEvent}
import ocelot.desktop.ui.layout.{CopyLayout, Layout}
import ocelot.desktop.ui.widget.LogWidget.{BorderThickness, EntryMargin, EntryPadding, LogEntry}
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
import ocelot.desktop.util.DrawUtils

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

abstract class LogWidget extends Widget {
  override protected val layout: Layout = new CopyLayout(this)

  private object MessageListWidget extends Widget with MouseHandler { messageList =>
    override protected val layout: Layout = new Layout(this) {
      override def recalculateBounds(): Unit = {
        super.recalculateBounds()
      }
    }

    private val entries = mutable.ArrayDeque.empty[Entry]
    private var needRelayout = false

    val dummyEntry: Entry = new RxEntry("", nextMessageY)

    override def receiveClickEvents: Boolean = true

    eventHandlers += {
      case ClickEvent(MouseEvent.Button.Right, mouse) =>
        visibleEntries.find(_.absoluteBounds.contains(mouse)).foreach {
          case entry: MessageEntry =>
            val menu = new ContextMenu
            menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) {
              UiHandler.clipboard = entry.message
            })
            root.get.contextMenus.open(menu)
        }
    }

    // when entries are removed from the head, all consecutive entries need to be shifted up.
    // doing that for every single removal is **very** inefficient.
    // instead, we keep the widgets where they are without shifting, having the offset accumulate, and update their
    // positions only once per frame (in `draw`).
    private def removedOffset: Float = entries.headOption.map(_.y - EntryMargin).getOrElse(0)

    private def applyRemovedOffset(): Unit = {
      val offset = removedOffset
      
      if (offset == 0) {
        return
      }

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

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

    override def maximumSize: Size2D = minimumSize

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

    def addEntry(entry: LogEntry): Unit = {
      entries += (entry match {
        case LogEntry.Rx(message) => new RxEntry(message, nextMessageY)
        case LogEntry.Tx(message) => new TxEntry(message, nextMessageY)
      })

      needRelayout = true
    }

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

      needRelayout = true
    }

    @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)
      }
    }

    private def visibleEntries: Iterator[Entry] = {
      val firstVisibleIdx = firstVisibleIdxSearch(parent.get.asInstanceOf[ScrollView].offset.y)
      entries.iterator.drop(firstVisibleIdx).takeWhile(_.absoluteBounds.y <= clippedBounds.max.y)
    }

    override def update(): Unit = {
      if (needRelayout) {
        recalculateBoundsAndRelayout()
        needRelayout = false
      }

      super.update()
    }

    override def draw(g: Graphics): Unit = {
      applyRemovedOffset()
      for (entry <- visibleEntries) {
        entry.draw(g)
      }
      super.draw(g)
    }

    trait Entry {
      var y: Float

      def maxY: Float

      def draw(g: Graphics): Unit

      def minimumSize: Size2D

      def size: Size2D

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

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

    private abstract class MessageEntry(val message: String, override var y: Float) extends Entry {
      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,
      )

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

      override def maxY: Float = y + minimumSize.height

      def background: RGBAColorNorm
      def foreground: RGBAColorNorm
      def border: RGBAColorNorm

      override def draw(g: Graphics): Unit = {
        val innerBounds = absoluteBounds.inflated(-BorderThickness)
        g.rect(innerBounds, background)
        DrawUtils.ring(
          g,
          absoluteBounds.x,
          absoluteBounds.y,
          absoluteBounds.w,
          absoluteBounds.h,
          BorderThickness,
          border,
        )

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

    private class RxEntry(message: String, _y: Float) extends MessageEntry(message, _y) {
      override def background: RGBAColorNorm = ColorScheme("LogEntryBackground")
      override def foreground: RGBAColorNorm = ColorScheme("Label")
      override def border: RGBAColorNorm = ColorScheme("LogEntryBorder")
    }

    private class TxEntry(message: String, _y: Float) extends MessageEntry(message, _y) {
      override def background: RGBAColorNorm = ColorScheme("LogEntryTxBackground")
      override def foreground: RGBAColorNorm = ColorScheme("LogEntryTxForeground")
      override def border: RGBAColorNorm = ColorScheme("LogEntryTxBorder")
    }
  }

  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 textWidth: Int

  override def minimumSize: Size2D = Size2D(
    MessageListWidget.minimumSize.width + 2 * BorderThickness,
    MessageListWidget.dummyEntry.maxY +
      EntryMargin +
      BorderThickness * 2, // outer border (top + bottom)
  )

  def addEntries(entries: Iterable[LogEntry]): Unit = {
    for (entry <- entries) {
      MessageListWidget.addEntry(entry)
    }
  }

  def removeFirstEntries(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

  sealed trait LogEntry

  sealed trait TextLogEntry extends LogEntry {
    val message: String
  }

  object LogEntry {

    /** A message received from a log source. */
    case class Rx(message: String) extends TextLogEntry

    /** A message sent to a log source. */
    case class Tx(message: String) extends TextLogEntry
  }
}
