src/datastar_gleam/event.gleam

/// Builders for `PatchElements`, `PatchSignals`, and `ExecuteScript` events.
///
/// This module is the primary way to construct Datastar events. Each builder
/// returns an intermediate record that you can customise with `with_*` helpers
/// before converting to a `DatastarEvent`.

import datastar_gleam.{type DatastarEvent, DatastarEvent}
import datastar_gleam/consts.{
  type ElementPatchMode, Append, Outer,
  default_elements_use_view_transitions, default_patch_signals_only_if_missing,
  default_sse_retry_duration, elements_literal, mode_literal,
  only_if_missing_literal, selector_literal, signals_literal,
  use_view_transition_literal,
}
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/string

// -- PatchElements --

/// Configuration for a `PatchElements` event.
///
/// Use `new_elements` or `new_remove` to create a value, then customise it
/// with `with_*` functions and finally call `patch_elements_to_datastar_event`.
pub type PatchElements {
  PatchElements(
    id: Option(String),
    retry: Int,
    elements: Option(String),
    selector: Option(String),
    mode: ElementPatchMode,
    use_view_transition: Bool,
  )
}

/// Create a `PatchElements` builder that inserts the given HTML fragment.
///
/// ```gleam
/// let patch =
///   event.new_elements("<div id='message'>Hello!</div>")
///   |> event.with_selector("#target")
///   |> event.with_mode(event.Append)
/// ```
pub fn new_elements(html: String) -> PatchElements {
  PatchElements(
    id: None,
    retry: default_sse_retry_duration,
    elements: Some(html),
    selector: None,
    mode: Outer,
    use_view_transition: default_elements_use_view_transitions,
  )
}

/// Create a `PatchElements` builder that removes the element matched by
/// `selector`.
pub fn new_remove(selector: String) -> PatchElements {
  PatchElements(
    id: None,
    retry: default_sse_retry_duration,
    elements: None,
    selector: Some(selector),
    mode: consts.Remove,
    use_view_transition: default_elements_use_view_transitions,
  )
}

/// Set the SSE event ID.
pub fn with_id(p: PatchElements, id: String) -> PatchElements {
  PatchElements(..p, id: Some(id))
}

/// Set the SSE retry duration in milliseconds.
pub fn with_retry(p: PatchElements, retry: Int) -> PatchElements {
  PatchElements(..p, retry: retry)
}

/// Set the CSS selector that identifies the target element.
pub fn with_selector(p: PatchElements, selector: String) -> PatchElements {
  PatchElements(..p, selector: Some(selector))
}

/// Set the insertion mode (default `Outer`).
pub fn with_mode(p: PatchElements, mode: ElementPatchMode) -> PatchElements {
  PatchElements(..p, mode: mode)
}

/// Enable or disable view transitions.
pub fn with_use_view_transition(p: PatchElements, enabled: Bool) -> PatchElements {
  PatchElements(..p, use_view_transition: enabled)
}

/// Convert a `PatchElements` builder into a `DatastarEvent`.
pub fn patch_elements_to_datastar_event(p: PatchElements) -> DatastarEvent {
  let data = []
  let data = case p.selector {
    Some(sel) -> list.append(data, [selector_literal <> " " <> sel])
    None -> data
  }
  let data = case p.mode {
    Outer -> data
    _ ->
      list.append(data, [
        mode_literal <> " " <> consts.element_patch_mode_string(p.mode),
      ])
  }
  let data = case p.use_view_transition {
    True -> list.append(data, [use_view_transition_literal <> " True"])
    False -> data
  }
  let data = case p.elements {
    Some(el) -> {
      string.split(el, on: "\n")
      |> list.map(fn(line) { elements_literal <> " " <> line })
      |> list.append(data, _)
    }
    None -> data
  }
  DatastarEvent(
    event: consts.PatchElements,
    id: p.id,
    retry: p.retry,
    data: data,
  )
}

// -- PatchSignals --

/// Configuration for a `PatchSignals` event.
pub type PatchSignals {
  PatchSignals(
    id: Option(String),
    retry: Int,
    signals: String,
    only_if_missing: Bool,
  )
}

/// Create a `PatchSignals` builder with the given JSON-encoded signals.
///
/// ```gleam
/// let patch =
///   event.new_signals("{ \"count\": 42 }")
///   |> event.with_only_if_missing(True)
/// ```
pub fn new_signals(signals: String) -> PatchSignals {
  PatchSignals(
    id: None,
    retry: default_sse_retry_duration,
    signals: signals,
    only_if_missing: default_patch_signals_only_if_missing,
  )
}

/// Set the SSE event ID.
pub fn with_signals_id(p: PatchSignals, id: String) -> PatchSignals {
  PatchSignals(..p, id: Some(id))
}

/// Set the SSE retry duration in milliseconds.
pub fn with_signals_retry(p: PatchSignals, retry: Int) -> PatchSignals {
  PatchSignals(..p, retry: retry)
}

/// Only patch signals that are not already present in the frontend store.
pub fn with_only_if_missing(p: PatchSignals, only_if_missing: Bool) -> PatchSignals {
  PatchSignals(..p, only_if_missing: only_if_missing)
}

/// Convert a `PatchSignals` builder into a `DatastarEvent`.
pub fn patch_signals_to_datastar_event(p: PatchSignals) -> DatastarEvent {
  let data = []
  let data = case p.only_if_missing {
    True -> list.append(data, [only_if_missing_literal <> " True"])
    False -> data
  }
  let data = {
    string.split(p.signals, on: "\n")
    |> list.map(fn(line) { signals_literal <> " " <> line })
    |> list.append(data, _)
  }
  DatastarEvent(
    event: consts.PatchSignals,
    id: p.id,
    retry: p.retry,
    data: data,
  )
}

// -- ExecuteScript --

/// Configuration for an `ExecuteScript` event.
///
/// This is a convenience wrapper around `PatchElements` that injects a
/// `<script>` tag into the page and optionally removes it after execution.
pub type ExecuteScript {
  ExecuteScript(
    id: Option(String),
    retry: Int,
    script: String,
    auto_remove: Option(Bool),
    attributes: List(String),
  )
}

/// Create an `ExecuteScript` builder with the given JavaScript source.
///
/// ```gleam
/// let script =
///   event.new_script("console.log('Hello from Gleam!')")
///   |> event.with_auto_remove(True)
/// ```
pub fn new_script(script: String) -> ExecuteScript {
  ExecuteScript(
    id: None,
    retry: default_sse_retry_duration,
    script: script,
    auto_remove: None,
    attributes: [],
  )
}

/// Set the SSE event ID.
pub fn with_script_id(s: ExecuteScript, id: String) -> ExecuteScript {
  ExecuteScript(..s, id: Some(id))
}

/// Set the SSE retry duration in milliseconds.
pub fn with_script_retry(s: ExecuteScript, retry: Int) -> ExecuteScript {
  ExecuteScript(..s, retry: retry)
}

/// Automatically remove the `<script>` tag after it executes.
pub fn with_auto_remove(s: ExecuteScript, auto_remove: Bool) -> ExecuteScript {
  ExecuteScript(..s, auto_remove: Some(auto_remove))
}

/// Add extra attributes to the generated `<script>` tag.
pub fn with_attributes(
  s: ExecuteScript,
  attributes: List(String),
) -> ExecuteScript {
  ExecuteScript(..s, attributes: attributes)
}

/// Convert an `ExecuteScript` builder into a `DatastarEvent`.
pub fn execute_script_to_datastar_event(s: ExecuteScript) -> DatastarEvent {
  let data = [selector_literal <> " body"]
  let data =
    list.append(data, [
      mode_literal <> " " <> consts.element_patch_mode_string(Append),
    ])

  let script_tag = {
    let auto_remove_attr = case s.auto_remove {
      Some(False) -> ""
      _ -> " data-effect=\"el.remove()\""
    }
    let attrs = string.join(s.attributes, with: " ")
    let attr_str = case attrs {
      "" -> ""
      _ -> " " <> attrs
    }
    "<script" <> auto_remove_attr <> attr_str <> ">"
  }

  let lines = string.split(s.script, on: "\n")
  let #(first, rest) = case lines {
    [f, ..r] -> #(f, r)
    [] -> #("", [])
  }

  let first_el = elements_literal <> " " <> script_tag <> first
  let rest_els = list.map(rest, fn(l) { elements_literal <> " " <> l })
  let all_els = [first_el, ..rest_els]
  let all_els = append_to_last(all_els, "</script>")

  let data = list.append(data, all_els)

  DatastarEvent(
    event: consts.PatchElements,
    id: s.id,
    retry: s.retry,
    data: data,
  )
}

fn append_to_last(items: List(String), suffix: String) -> List(String) {
  case list.reverse(items) {
    [last, ..front] -> list.reverse([last <> suffix, ..front])
    [] -> []
  }
}