// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
extern "js" fn configure_canvas_touch_input(
  canvas : @dom.HTMLCanvasElement,
) -> Unit =
  #| (canvas) => {
  #|   if (canvas && canvas.style) {
  #|     canvas.style.touchAction = 'none';
  #|     canvas.style.userSelect = 'none';
  #|     canvas.style.webkitUserSelect = 'none';
  #|     canvas.style.webkitTouchCallout = 'none';
  #|     canvas.style.webkitTapHighlightColor = 'transparent';
  #|   }
  #|   canvas?.addEventListener?.('contextmenu', event => event.preventDefault());
  #| }

///|
extern "js" fn pointer_event_id(event : @dom.Event) -> Int =
  #| (event) => Number(event.pointerId ?? 0) | 0

///|
extern "js" fn pointer_event_type(event : @dom.Event) -> String =
  #| (event) => String(event.pointerType ?? '')

///|
extern "js" fn pointer_event_offset_x(event : @dom.Event) -> Double =
  #| (event) => {
  #|   if (Number.isFinite(event.offsetX)) return Number(event.offsetX);
  #|   const rect = event.currentTarget?.getBoundingClientRect?.();
  #|   if (!rect) return 0;
  #|   return Number(event.clientX ?? 0) - rect.left;
  #| }

///|
extern "js" fn pointer_event_offset_y(event : @dom.Event) -> Double =
  #| (event) => {
  #|   if (Number.isFinite(event.offsetY)) return Number(event.offsetY);
  #|   const rect = event.currentTarget?.getBoundingClientRect?.();
  #|   if (!rect) return 0;
  #|   return Number(event.clientY ?? 0) - rect.top;
  #| }

///|
extern "js" fn pointer_event_is_primary(event : @dom.Event) -> Bool =
  #| (event) => Boolean(event.isPrimary ?? true)

///|
extern "js" fn prevent_pointer_default(event : @dom.Event) -> Unit =
  #| (event) => event.preventDefault?.()

///|
extern "js" fn capture_pointer(
  canvas : @dom.HTMLCanvasElement,
  pointer_id : Int,
) -> Unit =
  #| (canvas, pointerId) => {
  #|   try { canvas.setPointerCapture?.(pointerId); } catch (_err) {}
  #| }

///|
extern "js" fn release_pointer(
  canvas : @dom.HTMLCanvasElement,
  pointer_id : Int,
) -> Unit =
  #| (canvas, pointerId) => {
  #|   try { canvas.releasePointerCapture?.(pointerId); } catch (_err) {}
  #| }

///|
let touch_handlers_registered : Ref[Bool] = Ref(false)

///|
pub fn register_mouse_events(
  mouse : @inputs.Mouse,
  mouse_relative_delta : Ref[@smath.Vec2?],
  mouse_wheel : @inputs.MouseWheel,
) -> Unit {
  window.add_event_listener("mousemove", event => {
    let mouse_event = event.to_mouse_event().unwrap()
    mouse.pos = Vec2(
      mouse_event.get_offset_x().to_double(),
      mouse_event.get_offset_y().to_double(),
    )
    mouse_relative_delta.val = Some(
      Vec2(
        mouse_event.get_movement_x().to_double(),
        mouse_event.get_movement_y().to_double(),
      ),
    )
  })
  window.add_event_listener("mousedown", event => {
    note_audio_user_activation()
    let mouse_event = event.to_mouse_event().unwrap()
    match mouse_event.get_button() {
      0 => mouse.left_button = true
      1 => mouse.middle_button = true
      2 => mouse.right_button = true
      _ => ()
    }
  })
  window.add_event_listener("touchstart", _event => note_audio_user_activation())
  window.add_event_listener("mouseup", event => {
    let mouse_event = event.to_mouse_event().unwrap()
    match mouse_event.get_button() {
      0 => mouse.left_button = false
      1 => mouse.middle_button = false
      2 => mouse.right_button = false
      _ => ()
    }
  })
  window.add_event_listener("wheel", event => {
    let wheel_event = event.to_wheel_event().unwrap()
    mouse_wheel.delta = Vec2(
      wheel_event.get_delta_x(),
      wheel_event.get_delta_y(),
    )
  })
}

///|
fn contact_pointer_source(kind : String) -> @inputs.PointerSource? {
  match kind {
    "touch" => Some(PointerTouch)
    "pen" => Some(PointerPen)
    _ => None
  }
}

///|
fn touch_position_from_pointer_event(event : @dom.Event) -> @smath.Vec2 {
  Vec2(pointer_event_offset_x(event), pointer_event_offset_y(event))
}

///|
fn push_touch_change(
  changes : Array[@inputs.TouchEvent],
  id : @inputs.TouchId,
  source : @inputs.PointerSource,
  phase : @inputs.TouchPhase,
  position : @smath.Vec2,
  previous_position : @smath.Vec2,
  primary : Bool,
) -> Unit {
  changes.push({
    id,
    source,
    phase,
    position,
    previous_position,
    delta: position - previous_position,
    primary,
  })
}

///|
pub fn register_touch_events(
  touches : Map[@inputs.TouchId, @inputs.TouchPoint],
  changes : Array[@inputs.TouchEvent],
) -> Unit {
  if touch_handlers_registered.val {
    return
  }
  touch_handlers_registered.val = true
  let canvas = require_canvas()
  configure_canvas_touch_input(canvas)
  canvas.add_event_listener("pointerdown", event => {
    let kind = pointer_event_type(event)
    guard contact_pointer_source(kind) is Some(source) else { return }
    prevent_pointer_default(event)
    note_audio_user_activation()
    let id = @inputs.TouchId(pointer_event_id(event))
    let position = touch_position_from_pointer_event(event)
    let primary = pointer_event_is_primary(event)
    touches.set(id, {
      id,
      source,
      position,
      previous_position: position,
      delta: @smath.Vec2::zero(),
      primary,
    })
    push_touch_change(changes, id, source, Started, position, position, primary)
    capture_pointer(canvas, id.value)
  })
  canvas.add_event_listener("pointermove", event => {
    let kind = pointer_event_type(event)
    guard contact_pointer_source(kind) is Some(source) else { return }
    prevent_pointer_default(event)
    let id = @inputs.TouchId(pointer_event_id(event))
    guard touches.get(id) is Some(previous_touch) else { return }
    let position = touch_position_from_pointer_event(event)
    let previous_position = previous_touch.position
    let primary = previous_touch.primary
    let delta = position - previous_position
    touches.set(id, { id, source, position, previous_position, delta, primary })
    if delta[X] != 0.0 || delta[Y] != 0.0 {
      push_touch_change(
        changes,
        id,
        source,
        Moved,
        position,
        previous_position,
        primary,
      )
    }
  })
  canvas.add_event_listener("pointerup", event => {
    let kind = pointer_event_type(event)
    guard contact_pointer_source(kind) is Some(source) else { return }
    prevent_pointer_default(event)
    let id = @inputs.TouchId(pointer_event_id(event))
    let position = touch_position_from_pointer_event(event)
    let previous_position = touches
      .get(id)
      .map_or(position, fn(touch) { touch.position })
    let primary = touches
      .get(id)
      .map_or(pointer_event_is_primary(event), fn(touch) { touch.primary })
    push_touch_change(
      changes,
      id,
      source,
      Ended,
      position,
      previous_position,
      primary,
    )
    ignore(touches.remove(id))
    release_pointer(canvas, id.value)
  })
  canvas.add_event_listener("pointercancel", event => {
    let kind = pointer_event_type(event)
    guard contact_pointer_source(kind) is Some(source) else { return }
    prevent_pointer_default(event)
    let id = @inputs.TouchId(pointer_event_id(event))
    let position = touch_position_from_pointer_event(event)
    let previous_position = touches
      .get(id)
      .map_or(position, fn(touch) { touch.position })
    let primary = touches
      .get(id)
      .map_or(pointer_event_is_primary(event), fn(touch) { touch.primary })
    push_touch_change(
      changes,
      id,
      source,
      Cancelled,
      position,
      previous_position,
      primary,
    )
    ignore(touches.remove(id))
    release_pointer(canvas, id.value)
  })
}

///|
pub fn lock_mouse(requested_lock_ref : Ref[Bool]) -> Unit {
  let canvas = require_canvas()
  canvas.add_event_listener("click", fn(_event) {
    note_audio_user_activation()
    request_pointer_lock_if_enabled(canvas, requested_lock_ref.val)
  })
}

///|
pub fn sync_mouse_lock_state(lock_requested : Bool) -> Bool {
  let canvas = require_canvas()
  let lock_active = is_pointer_locked(canvas)
  if !lock_requested && lock_active {
    exit_pointer_lock()
    return false
  }
  lock_active
}