///|
struct DataColumn[T] {
  key : String
  label : String
  text : (T) -> String
  render : (T) -> @minimoon.Node
  sortable : Bool
  searchable : Bool
  hideable : Bool
  compare : (T, T) -> Int
}

///|
pub fn[T] data_column(
  key~ : String,
  label~ : String,
  text~ : (T) -> String,
  render? : (T) -> @minimoon.Node,
  sortable? : Bool = true,
  searchable? : Bool = true,
  hideable? : Bool = true,
  compare? : (T, T) -> Int,
  numeric_value? : (T) -> Double,
) -> DataColumn[T] {
  {
    key,
    label,
    text,
    render: render.unwrap_or(row => @minimoon.text(text(row))),
    sortable,
    searchable,
    hideable,
    compare: compare.unwrap_or((a, b) => {
      match numeric_value {
        Some(value) => value(a).compare(value(b))
        None => compare_text(text(a), text(b))
      }
    }),
  }
}

///|
pub(all) struct TableState {
  query : String
  page : Int
  page_size : Int
  sort_key : String
  descending : Bool
  selected_keys : Array[String]
  hidden_columns : Array[String]
} derive(Eq, Debug)

///|
pub fn table_state(page_size? : Int = 10) -> TableState {
  {
    query: "",
    page: 0,
    page_size: page_size.max(1),
    sort_key: "",
    descending: false,
    selected_keys: [],
    hidden_columns: [],
  }
}

///|
pub fn[C : @minimoon.IsChildren] table(
  children : C,
  id? : String = "",
  class? : String = "",
) -> @minimoon.Node {
  @minimoon.scroll_view(
    scroll_x=true,
    id~,
    class="mmui-table-scroll " + class,
    ui_container(
      "table",
      semantics=@minimoon.semantics(role=@minimoon.TableRole),
      children.to_nodes(),
    ),
  )
}

///|
pub fn[C : @minimoon.IsChildren] table_header(children : C) -> @minimoon.Node {
  ui_container("table-header", children.to_nodes())
}

///|
pub fn[C : @minimoon.IsChildren] table_body(children : C) -> @minimoon.Node {
  ui_container("table-body", children.to_nodes())
}

///|
pub fn[C : @minimoon.IsChildren] table_footer(children : C) -> @minimoon.Node {
  ui_container("table-footer", children.to_nodes())
}

///|
pub fn[C : @minimoon.IsChildren] table_row(
  children : C,
  selected? : Bool = false,
) -> @minimoon.Node {
  ui_container(
    "table-row",
    semantics=@minimoon.semantics(role=@minimoon.RowRole, selected~),
    children.to_nodes(),
  )
}

///|
pub fn[C : @minimoon.IsChildren] table_cell(children : C) -> @minimoon.Node {
  ui_container(
    "table-cell",
    semantics=@minimoon.semantics(role=@minimoon.CellRole),
    children.to_nodes(),
  )
}

///|
pub fn[C : @minimoon.IsChildren] table_head(children : C) -> @minimoon.Node {
  ui_container(
    "table-head",
    semantics=@minimoon.semantics(role=@minimoon.ColumnHeaderRole),
    children.to_nodes(),
  )
}

///|
pub fn table_caption(text : String) -> @minimoon.Node {
  ui_container("table-caption", [@minimoon.text(text)])
}

///|
pub fn pagination_controlled(
  id~ : String,
  page~ : @minimoon.Val[Int],
  total~ : @minimoon.Val[Int],
  on_change~ : @minimoon.Emit[Int],
  page_size? : Int = 10,
) -> @minimoon.Val[@minimoon.Node] {
  let dispatch = form_dispatch(
    @minimoon.Val::map2(page, total, (page, total) => (page, total)),
    (input, request : PaginationRequest) => {
      let (page, total) = input
      let maximum = @headless.page_count(total, page_size) - 1
      let current = page.clamp(min=0, max=maximum)
      let next = match request {
        Target(index) => index.clamp(min=0, max=maximum)
        Step(delta) => (current + delta).clamp(min=0, max=maximum)
      }
      if next == current {
        @minimoon.none
      } else {
        on_change(next)
      }
    },
  )
  @minimoon.Val::view2(page, total, (page, total) => {
    let count = @headless.page_count(total, page_size)
    let current = page.clamp(min=0, max=count - 1)
    let nodes : Array[@minimoon.Node] = [
      @minimoon.button(
        disabled=current == 0,
        event_key=id + "/previous",
        on_tap=if current == 0 { @minimoon.none } else { dispatch(Step(-1)) },
        "Previous",
      ),
    ]
    for index = (current - 2).max(0)
        index <= (current + 2).min(count - 1)
        index = index + 1 {
      nodes.push(
        @minimoon.button(
          event_key=id + "/" + index.to_string(),
          on_tap=dispatch(Target(index)),
          semantics=@minimoon.semantics(selected=index == current),
          (index + 1).to_string(),
        ),
      )
    }
    nodes.push(
      @minimoon.button(
        disabled=current + 1 == count,
        event_key=id + "/next",
        on_tap=if current + 1 == count {
          @minimoon.none
        } else {
          dispatch(Step(1))
        },
        "Next",
      ),
    )
    ui_container("pagination", id~, nodes)
  })
}

///|
pub fn pagination(
  id~ : String,
  total~ : @minimoon.Val[Int],
  page_size? : Int = 10,
) -> @minimoon.Val[@minimoon.Node] {
  let (page, emit) = @minimoon.create_variable(0)
  pagination_controlled(
    id~,
    page~,
    total~,
    on_change=emit.map(value => _ => value),
    page_size~,
  )
}

///|
fn compare_text(a : String, b : String) -> Int {
  let aa = a.iter().collect()
  let bb = b.iter().collect()
  for i = 0; i < aa.length().min(bb.length()); i = i + 1 {
    if aa[i] < bb[i] {
      return -1
    }
    if aa[i] > bb[i] {
      return 1
    }
  }
  aa.length().compare(bb.length())
}

///|
pub fn[T : Eq] data_table_controlled(
  id~ : String,
  rows~ : @minimoon.Val[Array[T]],
  columns~ : Array[DataColumn[T]],
  row_key~ : (T) -> String,
  state~ : @minimoon.Val[TableState],
  on_change~ : @minimoon.Emit[TableState],
  selectable? : Bool = true,
) -> @minimoon.Val[@minimoon.Node] {
  let column_keys : Map[String, Bool] = Map([])
  for column in columns {
    guard column.key != "" && !column_keys.contains(column.key) else {
      abort("data table requires unique nonempty column keys")
    }
    column_keys[column.key] = true
  }
  let dispatch = data_table_dispatch(
    rows, state, columns, row_key, on_change, selectable,
  )
  @minimoon.Val::view2(rows, state, (rows, state) => {
    let keys : Map[String, Bool] = Map([])
    for row in rows {
      let key = row_key(row)
      guard key != "" && !keys.contains(key) else {
        abort("data table requires unique nonempty row keys")
      }
      keys[key] = true
    }
    let filtered = rows.filter(row => {
      state.query == "" ||
      columns.any(column => {
        column.searchable && (column.text)(row).contains(state.query)
      })
    })
    for column in columns {
      if column.key == state.sort_key && column.sortable {
        filtered.sort_by((a, b) => {
          // Normalize user comparators before reversal, including Int minimum.
          let order = (column.compare)(a, b).compare(0)
          if state.descending {
            -order
          } else {
            order
          }
        })
      }
    }
    let page_size = state.page_size.max(1)
    let page_count = @headless.page_count(filtered.length(), page_size)
    let page = state.page.clamp(min=0, max=page_count - 1)
    let visible_columns = columns.filter(column => {
      !column.hideable || !state.hidden_columns.contains(column.key)
    })
    let header : Array[@minimoon.Node] = []
    if selectable {
      header.push(table_head("Selected"))
    }
    for column in visible_columns {
      header.push(
        table_head(
          @minimoon.button(
            disabled=!column.sortable,
            event_key=id + "/sort/" + column.key,
            on_tap=if column.sortable {
              dispatch(Sort(column.key))
            } else {
              @minimoon.none
            },
            column.label,
          ),
        ),
      )
    }
    let body : Array[@minimoon.KeyedNode] = []
    for index = page * page_size
        index < ((page + 1) * page_size).min(filtered.length())
        index = index + 1 {
      let row = filtered[index]
      let key = row_key(row)
      let selected = state.selected_keys.contains(key)
      let cells : Array[@minimoon.Node] = []
      if selectable {
        cells.push(
          table_cell(
            @minimoon.button(
              event_key=id + "/select/" + key,
              on_tap=dispatch(Select(key)),
              semantics=@minimoon.semantics(checked=selected),
              if selected {
                "✓"
              } else {
                "○"
              },
            ),
          ),
        )
      }
      for column in visible_columns {
        cells.push(table_cell((column.render)(row)))
      }
      body.push(@minimoon.keyed(key, table_row(cells, selected~)))
    }
    ui_container("data-table", id~, [
      @minimoon.input(
        value=state.query,
        on_input=dispatch.map(query => Query(query)),
        event_key=id + "/filter",
        placeholder="Filter rows",
      ),
      ui_container(
        "data-table-columns",
        columns.filter_map(column => {
          if !column.hideable {
            return None
          }
          let hidden = state.hidden_columns.contains(column.key)
          Some(
            @minimoon.button(
              event_key=id + "/column/" + column.key,
              on_tap=dispatch(Column(column.key)),
              semantics=@minimoon.semantics(checked=!hidden),
              column.label,
            ),
          )
        }),
      ),
      table([
        table_header(table_row(header)),
        table_body(@minimoon.keyed_fragment(body)),
      ]),
      ui_container("pagination", [
        @minimoon.button(
          event_key=id + "/previous",
          disabled=page == 0,
          on_tap=if page == 0 { @minimoon.none } else { dispatch(PageStep(-1)) },
          "Previous",
        ),
        @minimoon.text((page + 1).to_string() + " / " + page_count.to_string()),
        @minimoon.button(
          event_key=id + "/next",
          disabled=page + 1 == page_count,
          on_tap=if page + 1 == page_count {
            @minimoon.none
          } else {
            dispatch(PageStep(1))
          },
          "Next",
        ),
      ]),
    ])
  })
}

///|
pub fn[T : Eq] data_table(
  id~ : String,
  rows~ : @minimoon.Val[Array[T]],
  columns~ : Array[DataColumn[T]],
  row_key~ : (T) -> String,
  page_size? : Int = 10,
  selectable? : Bool = true,
) -> @minimoon.Val[@minimoon.Node] {
  let (state, emit) = @minimoon.create_variable(table_state(page_size~))
  data_table_controlled(
    id~,
    rows~,
    columns~,
    row_key~,
    state~,
    on_change=emit.map(value => _ => value),
    selectable~,
  )
}