package ocelot.desktop.ui.widget

import ocelot.desktop.ColorScheme
import ocelot.desktop.audio.{ClickSoundSourceFactory, SoundSource}
import ocelot.desktop.color.Color
import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.{Graphics, IconSource}
import ocelot.desktop.ui.event.handlers.{HoverHandler, MouseHandler}
import ocelot.desktop.ui.event.{ClickEvent, HoverEvent, MouseEvent}
import ocelot.desktop.ui.widget.IconButton.{AnimationSpeed, Mode}
import ocelot.desktop.ui.widget.tooltip.LabelTooltip
import ocelot.desktop.ui.widget.traits.HoverAnimation
import ocelot.desktop.util.animation.{ColorAnimation, ValueAnimation}
import ocelot.desktop.util.{DrawUtils, Spritesheet}

class IconButton(
  releasedIconSource: IconSource,
  pressedIconSource: IconSource,
  releasedColor: Color = Color.White,
  pressedColor: Color = Color.White,
  sizeMultiplier: Float = 1,
  mode: Mode = Mode.Regular,
  drawBackground: Boolean = false,
  padding: Float = 0,
  tooltip: Option[String] = None,
  darkenActiveColorFactor: Float = 0f,
  val model: IconButton.Model = IconButton.DefaultModel(false),
) extends Widget with MouseHandler with HoverHandler with HoverAnimation {

  override protected def receiveClickEvents: Boolean = true

  private var prevModelValue = model.pressed

  eventHandlers += {
    case HoverEvent(HoverEvent.State.Enter) => onHoverEnter()
    case HoverEvent(HoverEvent.State.Leave) => onHoverLeave()

    case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) =>
      mode match {
        case Mode.Regular => handlePress()
        case _ => // the other modes are triggered on click.
      }

      clickSoundSource.press.play()

    case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) => mode match {
        case Mode.Regular if model.pressed =>
          handleRelease()
          clickSoundSource.release.play()

        case _ => // the other modes are triggered on click.
      }

    case ClickEvent(MouseEvent.Button.Left, _) =>
      mode match {
        case Mode.Regular => // regular buttons are handled above.
        case Mode.Switch if model.pressed =>
          handleRelease()
          clickSoundSource.release.play()

        case Mode.Radio if model.pressed => // the radio button is already pressed; ignore.
        case Mode.Radio | Mode.Switch =>
          handlePress()
          clickSoundSource.release.play()
      }

      onClicked()
  }

  /**
    * Called when the state is changed to pressed.
    *
    * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons.
    *
    * @see [[onClicked]] for [[Mode.Regular]] buttons.
    */
  def onPressed(): Unit = {}

  /**
    * Called when the state is changed to released.
    *
    * UX note: override this for [[Mode.Switch]] and [[Mode.Radio]] buttons.
    *
    * @see [[onClicked]] for [[Mode.Regular]] buttons.
    */
  def onReleased(): Unit = {}

  /**
    * Called when the button is clicked (the mouse button has been released while it's within the button's bounds),
    * regardless of the state.
    *
    * UX note: override this for [[Mode.Regular]] buttons.
    *
    * @see [[onPressed]] and [[onReleased]] for [[Mode.Switch]] and [[Mode.Radio]] buttons.
    */
  def onClicked(): Unit = {}

  private def onHoverEnter(): Unit = {
    updateColorAnimationTargets()

    for (tooltip <- labelTooltip) {
      root.get.tooltipPool.addTooltip(tooltip)
    }
  }

  private def onHoverLeave(): Unit = {
    updateColorAnimationTargets()

    for (tooltip <- labelTooltip) {
      root.get.tooltipPool.closeTooltip(tooltip)
    }
  }

  protected def pressedActiveColor: Color = pressedColor.toRGBANorm.mapRgb(_ * (1 - darkenActiveColorFactor))
  protected def releasedActiveColor: Color = releasedColor.toRGBANorm.mapRgb(_ * (1 - darkenActiveColorFactor))

  private def targetPressedColor: Color = if (mouseOver) pressedActiveColor else pressedColor
  private def targetReleasedColor: Color = if (mouseOver) releasedActiveColor else releasedColor

  private def targetIconMix: Float = if (model.pressed) 1f else 0f

  private val pressedColorAnimation = new ColorAnimation(targetPressedColor, AnimationSpeed)
  private val releasedColorAnimation = new ColorAnimation(targetReleasedColor, AnimationSpeed)
  private val iconMixAnimation = new ValueAnimation(targetIconMix, AnimationSpeed)

  private def handlePress(): Unit = {
    model.pressed = true
    onPressed()
  }

  private def handleRelease(): Unit = {
    model.pressed = false
    onReleased()
  }

  size = minimumSize

  private def releasedIconSize: Size2D = Spritesheet.spriteSize(releasedIconSource) * sizeMultiplier

  private def pressedIconSize: Size2D = Spritesheet.spriteSize(pressedIconSource) * sizeMultiplier

  override def minimumSize: Size2D = releasedIconSize.max(pressedIconSize) + (padding * 2.0f)
  override def maximumSize: Size2D = minimumSize

  protected val labelTooltip: Option[LabelTooltip] = tooltip.map(label => new LabelTooltip(label))

  def borderColor: Color = ColorScheme("ButtonBorder")

  override def draw(g: Graphics): Unit = {
    if (drawBackground) {
      g.rect(bounds, hoverAnimation.color)
      DrawUtils.ring(g, position.x, position.y, width, height, 2, borderColor)
    }

    if (iconMixAnimation.value < 1f) {
      g.sprite(
        releasedIconSource,
        position + (size - releasedIconSize) / 2f,
        releasedIconSize,
        releasedColorAnimation.color.toRGBANorm.mapA(_ * (1f - iconMixAnimation.value)),
      )
    }

    if (iconMixAnimation.value > 0f) {
      g.sprite(
        pressedIconSource,
        position + (size - pressedIconSize) / 2f,
        pressedIconSize,
        pressedColorAnimation.color.toRGBANorm.mapA(_ * iconMixAnimation.value),
      )
    }
  }

  private def updateColorAnimationTargets(): Unit = {
    pressedColorAnimation.goto(targetPressedColor)
    releasedColorAnimation.goto(targetReleasedColor)
  }

  protected def updateAnimationTargets(): Unit = {
    updateColorAnimationTargets()
    iconMixAnimation.goto(targetIconMix)
  }

  override def update(): Unit = {
    super.update()

    val nextModelValue = model.pressed

    if (prevModelValue != nextModelValue) {
      prevModelValue = nextModelValue
      updateAnimationTargets()
    }

    pressedColorAnimation.update()
    releasedColorAnimation.update()
    iconMixAnimation.update()
  }

  protected def clickSoundSource: ClickSoundSourceFactory = SoundSource.InterfaceClick
}

object IconButton {
  val AnimationSpeed = 10f

  sealed trait Mode

  object Mode {
    /**
      * Your regular, run-of-the-mill button you love.
      *
      * When the LMB is depressed, [[IconButton.onPressed]] is called.
      * After it's released, [[IconButton.onReleased]] is called.
      */
    case object Regular extends Mode

    /**
      * A toggleable button, or a switch.
      *
      * Clicking the LMB while the button is pressed releases it, and [[IconButton.onReleased]] is called.
      * Clicking the LMB while the button is released depresses it, and [[IconButton.onPressed]] is called.
      */
    case object Switch extends Mode

    /**
      * A radio button is like a switch except clicking on it while it's depressed doesn't do anything.
      *
      * [[IconButton.onReleased]] is never called (unless you do it yourself).
      *
      * Managing a group of radio buttons is your responsibility.
      */
    case object Radio extends Mode
  }

  /**
    * This is the visible button state.
    *
    * When [[IconButton.update]] notices a state change, it runs its animations.
    *
    * Note that callbacks are run after running the setter regardless of whether it does anything.
    */
  trait Model {
    /**
      * Whether to display the pressed button state.
      */
    def pressed: Boolean

    /**
      * This setter is used by [[IconButton]] to respond to user interaction.
      *
      * You may want to override this method to do nothing
      * if you're proxying another model and updating it in callbacks.
      */
    def pressed_=(newValue: Boolean): Unit
  }

  /**
    * This is the default model that simply reads from a variable and writes to it.
    */
  case class DefaultModel(override var pressed: Boolean) extends Model

  /**
    * This is a model that ignores user input (presumably you handle it in callbacks).
    */
  class ReadOnlyModel(f: () => Boolean) extends Model {
    override def pressed: Boolean = f()

    override def pressed_=(newValue: Boolean): Unit = {}
  }

  object ReadOnlyModel {
    /**
      * A utility method to create a new instance of [[ReadOnlyModel]].
      *
      * The value returned by [[ReadOnlyModel.pressed]] is obtained
      * by evaluating the provided expression '''every time'''.
      */
    def apply(value: => Boolean): ReadOnlyModel = new ReadOnlyModel(value _)
  }
}
