///|
priv struct TapGestureResult {
  next_state : PressableState
  activated : Bool
}

///|
priv enum LongPressGesturePhase {
  Idle
  Pressing
  Recognized
  Cancelled
} derive(Eq)

///|
priv struct LongPressGestureState {
  phase : LongPressGesturePhase
  origin : Point?
  start_time_ms : Double
  duration_ms : Double
  max_movement : Double
}

///|
priv struct LongPressGestureResult {
  state : LongPressGestureState
  recognized : Bool
  cancelled : Bool
}

///|
test {
  ignore(
    LongPressGestureResult::{
      state: LongPressGestureState::new(),
      recognized: true,
      cancelled: true,
    }.state,
  )
}

///|
priv struct DoubleTapGestureState {
  first_position : Point?
  first_time_ms : Double
  timeout_ms : Double
  max_distance : Double
}

///|
priv struct DoubleTapGestureResult {
  state : DoubleTapGestureState
  recognized : Bool
}

///|
test {
  ignore(
    DoubleTapGestureResult::{
      state: DoubleTapGestureState::new(),
      recognized: false,
    }.state,
  )
}

///|
pub(all) enum DragGesturePhase {
  Idle
  Pending
  Started
  Changed
  Ended
  Cancelled
} derive(Eq, Debug, ToJson)

///|
pub(all) struct DragGestureEvent {
  phase : DragGesturePhase
  start : Point
  position : Point
  delta : Point
  translation : Point
} derive(Eq, Debug, ToJson)

///|
priv struct DragGestureState {
  phase : DragGesturePhase
  origin : Point?
  last_position : Point?
  min_distance : Double
}

///|
impl ViewStateValue for DragGestureState with fn view_state_type_key() {
  DeclarationKey::string("moui.core.DragGestureState.v1")
}

///|
impl ViewStateValue for DragGestureState with fn encode_view_state(self) {
  let output = Buffer(size_hint=43)
  output.write_byte(
    match self.phase {
      DragGesturePhase::Idle => 0
      DragGesturePhase::Pending => 1
      DragGesturePhase::Started => 2
      DragGesturePhase::Changed => 3
      DragGesturePhase::Ended => 4
      DragGesturePhase::Cancelled => 5
    },
  )
  match self.origin {
    Some(point) => {
      output.write_byte(1)
      output.write_double_be(point.x)
      output.write_double_be(point.y)
    }
    None => {
      output.write_byte(0)
      output.write_double_be(0.0)
      output.write_double_be(0.0)
    }
  }
  match self.last_position {
    Some(point) => {
      output.write_byte(1)
      output.write_double_be(point.x)
      output.write_double_be(point.y)
    }
    None => {
      output.write_byte(0)
      output.write_double_be(0.0)
      output.write_double_be(0.0)
    }
  }
  output.write_double_be(self.min_distance)
  output.to_bytes()
}

///|
impl ViewStateValue for DragGestureState with fn decode_view_state(_self, bytes) {
  if bytes.length() != 43 {
    return None
  }
  let phase = match bytes[0] {
    0 => Some(DragGesturePhase::Idle)
    1 => Some(DragGesturePhase::Pending)
    2 => Some(DragGesturePhase::Started)
    3 => Some(DragGesturePhase::Changed)
    4 => Some(DragGesturePhase::Ended)
    5 => Some(DragGesturePhase::Cancelled)
    _ => None
  }
  let origin_x = ViewStateValue::decode_view_state(0.0, bytes[2:10].to_owned())
  let origin_y = ViewStateValue::decode_view_state(0.0, bytes[10:18].to_owned())
  let last_x = ViewStateValue::decode_view_state(0.0, bytes[19:27].to_owned())
  let last_y = ViewStateValue::decode_view_state(0.0, bytes[27:35].to_owned())
  let min_distance = ViewStateValue::decode_view_state(
    0.0,
    bytes[35:43].to_owned(),
  )
  match (phase, origin_x, origin_y, last_x, last_y, min_distance) {
    (
      Some(phase),
      Some(origin_x),
      Some(origin_y),
      Some(last_x),
      Some(last_y),
      Some(min_distance),
    ) =>
      Some({
        phase,
        origin: if bytes[1] == 1 {
          Some(Point::new(x=origin_x, y=origin_y))
        } else if bytes[1] == 0 {
          None
        } else {
          return None
        },
        last_position: if bytes[18] == 1 {
          Some(Point::new(x=last_x, y=last_y))
        } else if bytes[18] == 0 {
          None
        } else {
          return None
        },
        min_distance,
      })
    _ => None
  }
}

///|
fn drag_gesture_view_state_slot() -> ViewStateSlot[DragGestureState] {
  ViewStateSlot::new(
    key=DeclarationKey::string("moui.core.drag-gesture"),
    initial=DragGestureState::new(),
  )
}

///|
priv struct DragGestureResult {
  state : DragGestureState
  event : DragGestureEvent?
}

///|
test {
  ignore(
    DragGestureResult::{ state: DragGestureState::new(), event: None }.state,
  )
}

///|
pub(all) enum CommandIntent {
  Activate
  Submit
  Cancel
  Copy
  Cut
  Paste
  Undo
  Redo
  SelectAll
  OpenContextMenu
} derive(Eq, Debug, ToJson)

///|
pub(all) struct ActionCommand {
  intent : CommandIntent
  shortcut : KeyboardShortcut?
  label : String
  enabled : Bool
  group : String
  description : String
  context_menu : Bool
} derive(Eq, Debug, ToJson)

///|
struct CommandBinding {
  command : ActionCommand
  handle : () -> Unit
}

///|
struct ActionCommandMap {
  bindings : Array[CommandBinding]
}

///|
fn recognize_press_tap(
  current : PressableState,
  inside : Bool,
  phase : PointerPhase,
) -> TapGestureResult {
  let next_state = match phase {
    Move =>
      if current == PressableState::Pressed && inside {
        PressableState::Pressed
      } else if inside {
        PressableState::Hovered
      } else {
        PressableState::Normal
      }
    Down =>
      if inside {
        PressableState::Pressed
      } else {
        PressableState::Normal
      }
    Up => if inside { PressableState::Hovered } else { PressableState::Normal }
    // Synthetic runtime phase: the pointer left the element, so drop any hover
    // or press state and return to rest.
    Exit | Cancel => PressableState::Normal
  }
  {
    next_state,
    activated: phase == Up && inside && current == PressableState::Pressed,
  }
}

///|
fn LongPressGestureState::new(
  duration_ms? : Double = 500.0,
  max_movement? : Double = 8.0,
) -> LongPressGestureState {
  {
    phase: LongPressGesturePhase::Idle,
    origin: None,
    start_time_ms: 0.0,
    duration_ms: max_double(0.0, duration_ms),
    max_movement: max_double(0.0, max_movement),
  }
}

///|
fn LongPressGestureState::update(
  self : LongPressGestureState,
  event : PointerEvent,
  inside? : Bool = true,
) -> LongPressGestureResult {
  match event.phase {
    Down => {
      let next = {
        ..self,
        phase: if inside {
          LongPressGesturePhase::Pressing
        } else {
          LongPressGesturePhase::Idle
        },
        origin: if inside {
          Some(event.position)
        } else {
          None
        },
        start_time_ms: event.time_ms,
      }
      { state: next, recognized: false, cancelled: false }
    }
    Move =>
      match self.origin {
        Some(origin) => {
          let outside = !inside ||
            point_distance(origin, event.position) > self.max_movement
          let due = event.time_ms - self.start_time_ms >= self.duration_ms
          if outside {
            {
              state: { ..self, phase: LongPressGesturePhase::Cancelled },
              recognized: false,
              cancelled: true,
            }
          } else if self.phase == LongPressGesturePhase::Pressing && due {
            {
              state: { ..self, phase: LongPressGesturePhase::Recognized },
              recognized: true,
              cancelled: false,
            }
          } else {
            { state: self, recognized: false, cancelled: false }
          }
        }
        None => { state: self, recognized: false, cancelled: false }
      }
    Up | Cancel | Exit => {
      let cancelled = self.phase == LongPressGesturePhase::Pressing
      {
        state: LongPressGestureState::new(
          duration_ms=self.duration_ms,
          max_movement=self.max_movement,
        ),
        recognized: false,
        cancelled,
      }
    }
  }
}

///|
fn DoubleTapGestureState::new(
  timeout_ms? : Double = 300.0,
  max_distance? : Double = 18.0,
) -> DoubleTapGestureState {
  {
    first_position: None,
    first_time_ms: 0.0,
    timeout_ms: max_double(0.0, timeout_ms),
    max_distance: max_double(0.0, max_distance),
  }
}

///|
fn DoubleTapGestureState::tap(
  self : DoubleTapGestureState,
  position : Point,
  time_ms : Double,
) -> DoubleTapGestureResult {
  match self.first_position {
    Some(first) => {
      let close_enough = point_distance(first, position) <= self.max_distance
      let soon_enough = time_ms - self.first_time_ms <= self.timeout_ms
      if close_enough && soon_enough {
        {
          state: DoubleTapGestureState::new(
            timeout_ms=self.timeout_ms,
            max_distance=self.max_distance,
          ),
          recognized: true,
        }
      } else {
        {
          state: {
            ..self,
            first_position: Some(position),
            first_time_ms: time_ms,
          },
          recognized: false,
        }
      }
    }
    None =>
      {
        state: {
          ..self,
          first_position: Some(position),
          first_time_ms: time_ms,
        },
        recognized: false,
      }
  }
}

///|
fn DoubleTapGestureState::update(
  self : DoubleTapGestureState,
  event : PointerEvent,
  inside? : Bool = true,
) -> DoubleTapGestureResult {
  if inside && event.phase == PointerPhase::Up {
    self.tap(event.position, event.time_ms)
  } else {
    { state: self, recognized: false }
  }
}

///|
pub fn DragGestureEvent::new(
  phase~ : DragGesturePhase,
  start~ : Point,
  position~ : Point,
  delta~ : Point,
  translation~ : Point,
) -> DragGestureEvent {
  { phase, start, position, delta, translation }
}

///|
pub fn DragGesturePhase::label(self : DragGesturePhase) -> String {
  match self {
    Idle => "Idle"
    Pending => "Pending"
    Started => "Started"
    Changed => "Changed"
    Ended => "Ended"
    Cancelled => "Cancelled"
  }
}

///|
fn DragGestureState::new(min_distance? : Double = 3.0) -> DragGestureState {
  {
    phase: DragGesturePhase::Idle,
    origin: None,
    last_position: None,
    min_distance: max_double(0.0, min_distance),
  }
}

///|
fn DragGestureState::update(
  self : DragGestureState,
  event : PointerEvent,
  inside? : Bool = true,
) -> DragGestureResult {
  match event.phase {
    Down => {
      let next = if inside {
        {
          ..self,
          phase: DragGesturePhase::Pending,
          origin: Some(event.position),
          last_position: Some(event.position),
        }
      } else {
        DragGestureState::new(min_distance=self.min_distance)
      }
      { state: next, event: None }
    }
    Move =>
      match (self.origin, self.last_position) {
        (Some(origin), Some(last)) => {
          let translation = point_delta(origin, event.position)
          let delta = point_delta(last, event.position)
          let distance = point_distance(origin, event.position)
          if self.phase == DragGesturePhase::Pending &&
            distance < self.min_distance {
            {
              state: { ..self, last_position: Some(event.position) },
              event: None,
            }
          } else {
            let phase = if self.phase == DragGesturePhase::Pending {
              DragGesturePhase::Started
            } else {
              DragGesturePhase::Changed
            }
            let drag_event = DragGestureEvent::new(
              phase~,
              start=origin,
              position=event.position,
              delta~,
              translation~,
            )
            {
              state: { ..self, phase, last_position: Some(event.position) },
              event: Some(drag_event),
            }
          }
        }
        _ => { state: self, event: None }
      }
    Up =>
      match (self.origin, self.last_position) {
        (Some(origin), Some(last)) => {
          let phase = if self.phase == DragGesturePhase::Pending {
            DragGesturePhase::Cancelled
          } else {
            DragGesturePhase::Ended
          }
          let drag_event = if phase == DragGesturePhase::Ended {
            Some(
              DragGestureEvent::new(
                phase~,
                start=origin,
                position=event.position,
                delta=point_delta(last, event.position),
                translation=point_delta(origin, event.position),
              ),
            )
          } else {
            None
          }
          {
            state: DragGestureState::new(min_distance=self.min_distance),
            event: drag_event,
          }
        }
        _ =>
          {
            state: DragGestureState::new(min_distance=self.min_distance),
            event: None,
          }
      }
    Cancel | Exit => {
      let drag_event = match (self.origin, self.last_position) {
        (Some(origin), Some(last)) =>
          if self.phase == DragGesturePhase::Started ||
            self.phase == DragGesturePhase::Changed {
            Some(
              DragGestureEvent::new(
                phase=DragGesturePhase::Cancelled,
                start=origin,
                position=event.position,
                delta=point_delta(last, event.position),
                translation=point_delta(origin, event.position),
              ),
            )
          } else {
            None
          }
        _ => None
      }
      {
        state: DragGestureState::new(min_distance=self.min_distance),
        event: drag_event,
      }
    }
  }
}

///|
fn point_delta(from : Point, to : Point) -> Point {
  Point::new(x=to.x - from.x, y=to.y - from.y)
}

///|
fn point_distance(a : Point, b : Point) -> Double {
  let dx = a.x - b.x
  let dy = a.y - b.y
  (dx * dx + dy * dy).sqrt()
}

///|
pub fn ActionCommand::new(
  intent~ : CommandIntent,
  label? : String = "",
  shortcut? : KeyboardShortcut? = None,
  enabled? : Bool = true,
  group? : String = "",
  description? : String = "",
  context_menu? : Bool = false,
) -> ActionCommand {
  { intent, shortcut, label, enabled, group, description, context_menu }
}

///|
pub fn ActionCommand::display_label(self : ActionCommand) -> String {
  if self.label != "" {
    self.label
  } else {
    self.intent.display_label()
  }
}

///|
pub fn ActionCommand::shortcut_label(self : ActionCommand) -> String {
  match self.shortcut {
    Some(shortcut) => shortcut.display_label()
    None => ""
  }
}

///|
pub fn CommandIntent::display_label(self : CommandIntent) -> String {
  match self {
    Activate => "Activate"
    Submit => "Submit"
    Cancel => "Cancel"
    Copy => "Copy"
    Cut => "Cut"
    Paste => "Paste"
    Undo => "Undo"
    Redo => "Redo"
    SelectAll => "Select All"
    OpenContextMenu => "Open Context Menu"
  }
}

///|
pub fn KeyboardShortcut::display_label(self : KeyboardShortcut) -> String {
  let parts : Array[String] = []
  if self.modifiers.control {
    parts.push("Ctrl")
  }
  if self.modifiers.alt {
    parts.push("Alt")
  }
  if self.modifiers.shift {
    parts.push("Shift")
  }
  if self.modifiers.meta {
    parts.push("Meta")
  }
  parts.push(keyboard_shortcut_key_label(self.key))
  parts.join("+")
}

///|
fn keyboard_shortcut_key_label(key : String) -> String {
  match key {
    " " => "Space"
    "Escape" => "Esc"
    "ArrowLeft" => "Left"
    "ArrowRight" => "Right"
    "ArrowUp" => "Up"
    "ArrowDown" => "Down"
    _ => key
  }
}

///|
pub fn ActionCommand::matches(
  self : ActionCommand,
  event : KeyboardEvent,
) -> Bool {
  if !self.enabled {
    false
  } else {
    match self.shortcut {
      Some(shortcut) => shortcut.matches(event)
      None => false
    }
  }
}

///|
pub fn CommandBinding::new(
  command~ : ActionCommand,
  handle~ : () -> Unit,
) -> CommandBinding {
  { command, handle }
}

///|
pub fn ActionCommandMap::new(
  bindings? : Array[CommandBinding] = [],
) -> ActionCommandMap {
  { bindings, }
}

///|
pub fn ActionCommandMap::commands(
  self : ActionCommandMap,
) -> Array[ActionCommand] {
  self.bindings.map(binding => binding.command)
}

///|
pub fn ActionCommandMap::find_shortcut(
  self : ActionCommandMap,
  event : KeyboardEvent,
) -> ActionCommand? {
  for binding in self.bindings {
    if binding.command.matches(event) {
      return Some(binding.command)
    }
  }
  None
}

///|
pub fn ActionCommandMap::dispatch_shortcut(
  self : ActionCommandMap,
  event : KeyboardEvent,
) -> CommandIntent? {
  for binding in self.bindings {
    if binding.command.matches(event) {
      (binding.handle)()
      return Some(binding.command.intent)
    }
  }
  None
}

///|
pub fn ActionCommandMap::dispatch_intent(
  self : ActionCommandMap,
  intent : CommandIntent,
) -> Bool {
  for binding in self.bindings {
    if binding.command.enabled && binding.command.intent == intent {
      (binding.handle)()
      return true
    }
  }
  false
}

///|
pub fn ActionCommandMap::dispatch_command(
  self : ActionCommandMap,
  command : ActionCommand,
) -> Bool {
  for binding in self.bindings {
    if binding.command.enabled && binding.command == command {
      (binding.handle)()
      return true
    }
  }
  false
}

///|
pub fn default_text_commands() -> Array[ActionCommand] {
  [
    ActionCommand::new(
      intent=CommandIntent::Copy,
      label="Copy",
      group="Edit",
      description="Copy the current selection.",
      shortcut=Some(
        KeyboardShortcut::new(key="c", modifiers=KeyModifiers::new(meta=true)),
      ),
    ),
    ActionCommand::new(
      intent=CommandIntent::Cut,
      label="Cut",
      group="Edit",
      description="Cut the current selection.",
      shortcut=Some(
        KeyboardShortcut::new(key="x", modifiers=KeyModifiers::new(meta=true)),
      ),
    ),
    ActionCommand::new(
      intent=CommandIntent::Paste,
      label="Paste",
      group="Edit",
      description="Paste clipboard text.",
      shortcut=Some(
        KeyboardShortcut::new(key="v", modifiers=KeyModifiers::new(meta=true)),
      ),
    ),
    ActionCommand::new(
      intent=CommandIntent::Undo,
      label="Undo",
      group="Edit",
      description="Undo the last text edit.",
      shortcut=Some(
        KeyboardShortcut::new(key="z", modifiers=KeyModifiers::new(meta=true)),
      ),
    ),
    ActionCommand::new(
      intent=CommandIntent::Redo,
      label="Redo",
      group="Edit",
      description="Redo the last undone text edit.",
      shortcut=Some(
        KeyboardShortcut::new(
          key="z",
          modifiers=KeyModifiers::new(shift=true, meta=true),
        ),
      ),
    ),
    ActionCommand::new(
      intent=CommandIntent::SelectAll,
      label="Select All",
      group="Edit",
      description="Select all text in the focused input.",
      shortcut=Some(
        KeyboardShortcut::new(key="a", modifiers=KeyModifiers::new(meta=true)),
      ),
    ),
  ]
}