///|
/// Multi-thumb slider uses only typed native touch and page-local layout measurement.
priv struct SliderDrag {
  token : Int
  pointer : Int
  x : Double
  y : Double
  initial : Array[Int]
  last : Array[Int]
  index : Int
  bounds : @minimoon.NodeRect?
  ended : Bool
} derive(Eq)

///|
priv struct SliderState {
  token : Int
  drag : SliderDrag?
} derive(Eq)

///|
priv enum SliderMessage {
  StartDrag(@minimoon.TouchDetail)
  MoveDrag(@minimoon.TouchDetail)
  EndDrag(@minimoon.TouchDetail)
  CancelDrag
  Measured(Int, Result[Array[@minimoon.NodeRect?], @minimoon.HostError])
}

///|
fn slider_point(
  points : Array[@minimoon.TouchPoint],
  identifier : Int,
) -> @minimoon.TouchPoint? {
  for point in points {
    if point.identifier == identifier {
      return Some(point)
    }
  }
  None
}

///|
fn slider_position(
  spec : @headless.SliderSpec,
  rect : @minimoon.NodeRect,
  x : Double,
  y : Double,
  orientation : Orientation,
  direction : @theme.TextDirection,
) -> Double {
  let fraction = if orientation == Vertical {
    (rect.bottom - y) / rect.height
  } else if direction == @theme.Rtl {
    (rect.right - x) / rect.width
  } else {
    (x - rect.left) / rect.width
  }
  spec.min.to_double() +
  fraction.clamp(min=0.0, max=1.0) * (spec.max - spec.min).to_double()
}

///|
pub fn slider_values_controlled(
  context : UiContext,
  id~ : String,
  values~ : @minimoon.Val[Array[Int]],
  on_change~ : @minimoon.Emit[Array[Int]],
  on_commit? : @minimoon.Emit[Array[Int]],
  spec? : @headless.SliderSpec = @headless.slider_spec(),
  orientation? : Orientation = Horizontal,
  direction? : @theme.TextDirection = @theme.Ltr,
  disabled? : Bool = false,
  label? : String = "Value",
) -> @minimoon.Val[@minimoon.Node] {
  validate_measured_id(id)
  let (state, emit) = @minimoon.create_state_with_input(
    input=values,
    init=(_, _) => @minimoon.no_cmd(SliderState::{ token: 0, drag: None, }),
    update=(state, input, message, emit) => {
      let current = spec.normalize(input)
      match message {
        CancelDrag => {
          let restore = state.drag
            .map(drag => on_change(drag.initial))
            .unwrap_or(@minimoon.none)
          ({ ..state, drag: None, }, restore)
        }
        StartDrag(detail) => {
          if disabled ||
            state.drag is Some(_) ||
            detail.changed_touches.is_empty() {
            return @minimoon.no_cmd(state)
          }
          let point = detail.changed_touches[0]
          let token = state.token + 1
          let drag = SliderDrag::{
            token,
            pointer: point.identifier,
            x: point.client_x,
            y: point.client_y,
            initial: current,
            last: current,
            index: 0,
            bounds: None,
            ended: false,
          }
          (
            { token, drag: Some(drag), },
            @minimoon.measure_nodes(
              [id + "-track"],
              emit.map(result => Measured(token, result)),
            ),
          )
        }
        Measured(token, result) => {
          guard state.drag is Some(drag) && drag.token == token else {
            return @minimoon.no_cmd(state)
          }
          guard result is Ok([Some(bounds)]) &&
            bounds.width > 0.0 &&
            bounds.height > 0.0 else {
            return @minimoon.no_cmd({ ..state, drag: None, })
          }
          let position = slider_position(
            spec,
            bounds,
            drag.x,
            drag.y,
            orientation,
            direction,
          )
          let index = spec.nearest(current, position)
          let next = spec.update(current, index, position)
          let commands = [on_change(next)]
          if drag.ended {
            if on_commit is Some(commit) {
              commands.push(commit(next))
            }
            ({ ..state, drag: None, }, @minimoon.batch(commands))
          } else {
            (
              {
                ..state,
                drag: Some({ ..drag, index, bounds: Some(bounds), last: next, }),
              },
              @minimoon.batch(commands),
            )
          }
        }
        MoveDrag(detail) | EndDrag(detail) => {
          guard state.drag is Some(drag) else { return @minimoon.no_cmd(state) }
          let ended = message is EndDrag(_)
          let points = if ended {
            detail.changed_touches
          } else {
            detail.touches
          }
          guard slider_point(points, drag.pointer) is Some(point) else {
            return @minimoon.no_cmd(state)
          }
          match drag.bounds {
            None =>
              @minimoon.no_cmd({
                ..state,
                drag: Some({
                  ..drag,
                  x: point.client_x,
                  y: point.client_y,
                  ended,
                }),
              })
            Some(bounds) => {
              let next = spec.update(
                current,
                drag.index,
                slider_position(
                  spec,
                  bounds,
                  point.client_x,
                  point.client_y,
                  orientation,
                  direction,
                ),
              )
              let commands = [on_change(next)]
              if ended {
                if on_commit is Some(commit) {
                  commands.push(commit(next))
                }
                ({ ..state, drag: None, }, @minimoon.batch(commands))
              } else {
                (
                  {
                    ..state,
                    drag: Some({
                      ..drag,
                      x: point.client_x,
                      y: point.client_y,
                      last: next,
                    }),
                  },
                  @minimoon.batch(commands),
                )
              }
            }
          }
        }
      }
    },
    subscriptions=(_, _, emit) => context.page.on_hide(emit(CancelDrag)),
  )
  @minimoon.Val::view2(values, state, (values, state) => {
    let values = spec.normalize(values)
    // Native slider is exact only for the single horizontal divisible-step case.
    if values.length() == 1 &&
      orientation == Horizontal &&
      direction == @theme.Ltr &&
      (spec.max - spec.min) % spec.step == 0 {
      @minimoon.slider(
        id~,
        class="mmui-slider-native",
        value=values[0],
        min=spec.min,
        max=spec.max,
        step=spec.step,
        disabled~,
        event_key=id + "/change",
        changing_key=id + "/changing",
        on_changing=@minimoon.Emit(value => {
          if disabled {
            @minimoon.none
          } else {
            on_change([value])
          }
        }),
        on_change=@minimoon.Emit(value => {
          if disabled {
            @minimoon.none
          } else {
            @minimoon.batch([
              on_change([value]),
              on_commit.map(commit => commit([value])).unwrap_or(@minimoon.none),
            ])
          }
        }),
        semantics=@minimoon.semantics(
          role=@minimoon.SliderRole,
          label~,
          value_min=spec.min,
          value_max=spec.max,
          value_now=values[0],
          disabled~,
        ),
      )
    } else {
      let start = if values.length() > 1 {
        spec.fraction(values[0]) * 100.0
      } else {
        0.0
      }
      let end = spec.fraction(values[values.length() - 1]) * 100.0
      let side = if orientation == Vertical {
        "bottom"
      } else if direction == @theme.Rtl {
        "right"
      } else {
        "left"
      }
      let nodes : Array[@minimoon.Node] = [
        ui_container(
          "slider-range",
          [],
          style=side +
            ":" +
            start.to_string() +
            "%;" +
            (if orientation == Vertical { "height:" } else { "width:" }) +
            (end - start).to_string() +
            "%;",
        ),
      ]
      for index, value in values {
        nodes.push(
          ui_container(
            "slider-thumb",
            [],
            id=id + "-thumb-" + index.to_string(),
            style=side + ":" + (spec.fraction(value) * 100.0).to_string() + "%;",
            class=if state.drag is Some(drag) && drag.index == index {
              "mmui-active"
            } else {
              ""
            },
            semantics=@minimoon.semantics(
              role=@minimoon.SliderRole,
              label=label + " " + (index + 1).to_string(),
              value_min=spec.min,
              value_max=spec.max,
              value_now=value,
              disabled~,
            ),
          ),
        )
      }
      @minimoon.touch_view(
        id~,
        class="mmui-slider " +
          orientation_class(orientation) +
          (if disabled { " mmui-disabled" } else { "" }),
        event_key=id + "/gesture",
        catch_move=true,
        on_touch_start=emit.map(detail => StartDrag(detail)),
        on_touch_move=emit.map(detail => MoveDrag(detail)),
        on_touch_end=emit.map(detail => EndDrag(detail)),
        on_touch_cancel=emit.map(_ => CancelDrag),
        [ui_container("slider-track", nodes, id=id + "-track")],
      )
    }
  })
}

///|
pub fn slider_values(
  context : UiContext,
  id~ : String,
  default_values~ : Array[Int],
  spec? : @headless.SliderSpec = @headless.slider_spec(),
  orientation? : Orientation = Horizontal,
  direction? : @theme.TextDirection = @theme.Ltr,
  disabled? : Bool = false,
  label? : String = "Value",
  on_value_change? : @minimoon.Emit[Array[Int]],
  on_value_commit? : @minimoon.Emit[Array[Int]],
) -> @minimoon.Val[@minimoon.Node] {
  let (values, update) = @minimoon.create_variable(
    spec.normalize(default_values),
  )
  slider_values_controlled(
    context,
    id~,
    values~,
    on_change=@minimoon.Emit(values => {
      @minimoon.batch([
        update(_ => values),
        on_value_change.map(emit => emit(values)).unwrap_or(@minimoon.none),
      ])
    }),
    on_commit?=on_value_commit,
    spec~,
    orientation~,
    direction~,
    disabled~,
    label~,
  )
}

///|
pub fn slider_controlled(
  context : UiContext,
  id~ : String,
  value~ : @minimoon.Val[Int],
  on_change~ : @minimoon.Emit[Int],
  on_commit? : @minimoon.Emit[Int],
  spec? : @headless.SliderSpec = @headless.slider_spec(),
  orientation? : Orientation = Horizontal,
  direction? : @theme.TextDirection = @theme.Ltr,
  disabled? : Bool = false,
  label? : String = "Value",
) -> @minimoon.Val[@minimoon.Node] {
  slider_values_controlled(
    context,
    id~,
    values=value.map(value => [value]),
    on_change=on_change.map(values => values[0]),
    on_commit?=on_commit.map(emit => emit.map(values => values[0])),
    spec~,
    orientation~,
    direction~,
    disabled~,
    label~,
  )
}

///|
pub fn slider(
  context : UiContext,
  id~ : String,
  default_value? : Int = 0,
  spec? : @headless.SliderSpec = @headless.slider_spec(),
  orientation? : Orientation = Horizontal,
  direction? : @theme.TextDirection = @theme.Ltr,
  disabled? : Bool = false,
  label? : String = "Value",
  on_value_change? : @minimoon.Emit[Int],
  on_value_commit? : @minimoon.Emit[Int],
) -> @minimoon.Val[@minimoon.Node] {
  slider_values(
    context,
    id~,
    default_values=[default_value],
    spec~,
    orientation~,
    direction~,
    disabled~,
    label~,
    on_value_change?=on_value_change.map(emit => emit.map(values => values[0])),
    on_value_commit?=on_value_commit.map(emit => emit.map(values => values[0])),
  )
}