<svelte:options
  customElement={{
    props: {
      columns: { type: "Array" },
    },
    shadow: "none",
    tag: "table-viewer",
  }}
/>

<script lang="ts">
  import { z } from "zod";

  import icon_cloud_arrow_up from "../assets/heroicons/20/solid/cloud-arrow-up.svg?raw";
  import icon_cube_transparent from "../assets/heroicons/20/solid/cube-transparent.svg?raw";
  import icon_exclamation_circle from "../assets/heroicons/20/solid/exclamation-circle.svg?raw";
  import icon_sparkles from "../assets/heroicons/20/solid/sparkles.svg?raw";
  import { type Datum, datum_schema } from "./datum.svelte";
  import DatumEditor from "./datum-editor.svelte";
  import {
    type Coords,
    type Row,
    type FieldInfo,
    coords_eq,
    field_info_schema,
  } from "./field.svelte";
  import FieldAdder from "./field-adder.svelte";
  import FieldHeader from "./field-header.svelte";
  import { get_empty_datum_for } from "./presentation.svelte";

  type Props = {
    columns?: {
      name: string;
      regtype: string;
    }[];
  };

  let { columns = [] }: Props = $props();

  type CellDelta = {
    // Assumes that primary keys are immutable and that rows are only added or
    // removed upon a refresh.
    coords: Coords;
    value_initial: Datum;
    value_updated: Datum;
  };

  type Delta = {
    cells: CellDelta[];
  };

  type LazyData = {
    rows: Row[];
    fields: FieldInfo[];
  };

  type Selection = {
    region: "main" | "inserter";
    coords: Coords;
    original_value: Datum;
  };

  let selections = $state<Selection[]>([]);
  // While the datum editor is focused and while updated values are being pushed
  // to the server, other actions such as changing the set of selected cells are
  // restricted.
  let editor_value = $state<Datum | undefined>(undefined);
  let deltas = $state<{
    commit_queued: Delta[];
    commit_pending: Delta[];
    committed: Delta[];
    revert_queued: Delta[];
    revert_pending: Delta[];
    reverted: Delta[];
  }>({
    commit_queued: [],
    commit_pending: [],
    committed: [],
    revert_queued: [],
    revert_pending: [],
    reverted: [],
  });
  let datum_editor = $state<DatumEditor | undefined>();
  let table_element = $state<HTMLDivElement | undefined>();
  let inserter_rows = $state<Row[]>([]);
  let lazy_data = $state<LazyData | undefined>();

  // -------- Helper Functions -------- //

  function arrow_key_direction(
    key: string,
  ): "Down" | "Left" | "Right" | "Up" | undefined {
    if (key === "ArrowDown") {
      return "Down";
    } else if (key === "ArrowLeft") {
      return "Left";
    } else if (key === "ArrowRight") {
      return "Right";
    } else if (key === "ArrowUp") {
      return "Up";
    } else {
      return undefined;
    }
  }

  // -------- Updates and Effects -------- //

  function set_selections(arr: Omit<Selection, "original_value">[]) {
    selections = arr.map((sel) => {
      let cell_data: Datum | undefined;
      if (sel.region === "main") {
        cell_data = lazy_data?.rows[sel.coords[0]].data[sel.coords[1]];
      } else if (sel.region === "inserter") {
        cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]];
      } else {
        throw new Error("invalid region");
      }
      return {
        ...sel,
        original_value: cell_data!,
      };
    });
    if (arr.length === 1) {
      const [sel] = arr;
      let cell_data: Datum | undefined;
      if (sel.region === "main") {
        cell_data = lazy_data?.rows[sel.coords[0]].data[sel.coords[1]];
      } else if (sel.region === "inserter") {
        cell_data = inserter_rows[sel.coords[0]].data[sel.coords[1]];
      }
      editor_value = cell_data;
    } else {
      editor_value = undefined;
    }
  }

  function move_selection(direction: "Down" | "Left" | "Right" | "Up") {
    if (!lazy_data || selections.length === 0) {
      console.warn("move_selection() preconditions not met");
      return;
    }

    const last_selection = selections[selections.length - 1];
    if (
      direction === "Right" &&
      last_selection.coords[1] < lazy_data.fields.length - 1
    ) {
      set_selections([
        {
          region: last_selection.region,
          coords: [last_selection.coords[0], last_selection.coords[1] + 1],
        },
      ]);
    } else if (direction === "Left" && last_selection.coords[1] > 0) {
      set_selections([
        {
          region: last_selection.region,
          coords: [last_selection.coords[0], last_selection.coords[1] - 1],
        },
      ]);
    } else if (direction === "Down") {
      if (last_selection.region === "main") {
        if (last_selection.coords[0] < lazy_data.rows.length - 1) {
          set_selections([
            {
              region: "main",
              coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
            },
          ]);
        } else {
          // At bottom of main table.
          set_selections([
            {
              region: "inserter",
              coords: [0, last_selection.coords[1]],
            },
          ]);
        }
      } else if (last_selection.region === "inserter") {
        if (last_selection.coords[0] < inserter_rows.length - 1) {
          set_selections([
            {
              region: "inserter",
              coords: [last_selection.coords[0] + 1, last_selection.coords[1]],
            },
          ]);
        }
      }
    } else if (direction === "Up") {
      if (last_selection.region === "main") {
        if (last_selection.coords[0] > 0) {
          set_selections([
            {
              region: "main",
              coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
            },
          ]);
        }
      } else if (last_selection.region === "inserter") {
        if (last_selection.coords[0] > 0) {
          set_selections([
            {
              region: "inserter",
              coords: [last_selection.coords[0] - 1, last_selection.coords[1]],
            },
          ]);
        } else {
          // At top of inserter table.
          set_selections([
            {
              region: "main",
              coords: [lazy_data.rows.length - 1, last_selection.coords[1]],
            },
          ]);
        }
      }
    }
  }

  function try_sync_edit_to_cells() {
    if (lazy_data && selections.length === 1) {
      const [sel] = selections;
      if (editor_value !== undefined) {
        if (sel.region === "main") {
          lazy_data.rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
        } else if (sel.region === "inserter") {
          inserter_rows[sel.coords[0]].data[sel.coords[1]] = editor_value;
        } else {
          throw new Error("Unknown region");
        }
      }
    }
  }

  function try_queue_delta() {
    // Copy `editor_value` so that it can be used intuitively within closures.
    const editor_value_scoped = editor_value;
    if (editor_value_scoped === undefined) {
      cancel_edit();
    } else {
      if (selections.length > 0) {
        deltas.commit_queued = [
          ...deltas.commit_queued,
          {
            cells: selections
              .filter(({ region }) => region === "main")
              .map((sel) => ({
                coords: sel.coords,
                value_initial: sel.original_value,
                value_updated: editor_value_scoped,
              })),
          },
        ];
        selections = selections.map((sel) => ({
          ...sel,
          original_value: editor_value_scoped,
        }));
      }
    }
  }

  async function commit_delta(delta: Delta) {
    // Copy `lazy_data` so that it can be used intuitively within closures.
    const lazy_data_scoped = lazy_data;
    if (!lazy_data_scoped) {
      console.warn("sync_delta() preconditions not met");
      return;
    }
    deltas.commit_pending = [...deltas.commit_pending, delta];
    const resp = await fetch("update-values", {
      method: "post",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        cells: delta.cells.map((cell) => ({
          pkey: JSON.parse(lazy_data_scoped.rows[cell.coords[0]].key as string),
          column: lazy_data_scoped.fields[cell.coords[1]].field.name,
          value: cell.value_updated,
        })),
      }),
    });
    if (resp.status >= 200 && resp.status < 300) {
      deltas.commit_pending = deltas.commit_pending.filter((x) => x !== delta);
      deltas.committed = [...deltas.committed, delta];
    } else {
      // TODO display feedback to user
      console.error(resp);
      console.error(await resp.text());
    }
  }

  function tick_delta_queue() {
    const front_of_queue: Delta | undefined = deltas.commit_queued[0];
    if (front_of_queue) {
      deltas.commit_queued = deltas.commit_queued.filter(
        (x) => x !== front_of_queue,
      );
      commit_delta(front_of_queue).catch(console.error);
    }
  }

  function cancel_edit() {
    selections.forEach(({ coords, original_value, region }) => {
      if (region === "main") {
        if (lazy_data) {
          lazy_data.rows[coords[0]].data[coords[1]] = original_value;
        }
      } else if (region === "inserter") {
        inserter_rows[coords[0]].data[coords[1]] = original_value;
      } else {
        throw new Error("Unknown region");
      }
    });
    // Reset editor input value
    set_selections(selections);
    table_element?.focus();
  }

  // -------- Event Handlers: Both Tables -------- //

  function handle_table_keydown(ev: KeyboardEvent) {
    if (lazy_data) {
      const arrow_direction = arrow_key_direction(ev.key);
      if (arrow_direction) {
        move_selection(arrow_direction);
        ev.preventDefault();
      }
      if (ev.key === "Enter") {
        if (ev.shiftKey) {
          if (selections[0]?.region === "main") {
            set_selections([
              {
                region: "inserter",
                coords: [0, selections[0]?.coords[1] ?? 0],
              },
            ]);
          } else {
            inserter_rows = [
              ...inserter_rows,
              {
                key: inserter_rows.length,
                data: lazy_data.fields.map(({ field: { presentation } }) =>
                  get_empty_datum_for(presentation),
                ),
              },
            ];
          }
        } else {
          datum_editor?.focus();
        }
      }
    }
  }

  // -------- Event Handlers: Main Table -------- //

  function handle_main_cell_click(ev: MouseEvent, coords: Coords) {
    if (ev.metaKey || ev.ctrlKey) {
      // TODO
      // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
      // editor_input_value = "";
    } else {
      set_selections([{ region: "main", coords }]);
    }
  }

  // -------- Event Handlers: Inserter Table -------- //

  function handle_inserter_cell_click(ev: MouseEvent, coords: Coords) {
    if (ev.metaKey || ev.ctrlKey) {
      // TODO
      // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
      // editor_input_value = "";
    } else {
      set_selections([{ region: "inserter", coords }]);
    }
  }

  // -------- Initial API Fetch -------- //

  (async function () {
    const get_data_response_schema = z.object({
      rows: z.array(
        z.object({
          pkey: z.string(),
          data: z.array(datum_schema),
        }),
      ),
      fields: z.array(field_info_schema),
    });
    const resp = await fetch("get-data");
    const body = get_data_response_schema.parse(await resp.json());
    lazy_data = {
      fields: body.fields,
      rows: body.rows.map(({ data, pkey }) => ({ data, key: pkey })),
    };
    inserter_rows = [
      {
        key: 0,
        data: body.fields.map(({ field: { presentation } }) =>
          get_empty_datum_for(presentation),
        ),
      },
    ];
  })().catch(console.error);

  setInterval(tick_delta_queue, 500);
</script>

{#snippet table_region({
  region_name,
  rows,
  on_cell_click,
}: {
  region_name: "main" | "inserter";
  rows: Row[];
  on_cell_click(ev: MouseEvent, coords: Coords): void;
})}
  {#if lazy_data}
    {#each rows as row, row_index}
      <div class="lens-table__row" role="row">
        {#each lazy_data.fields as field, field_index}
          {@const cell_data = row.data[field_index]}
          {@const cell_coords: Coords = [row_index, field_index]}
          {@const cell_selected = selections.some(
            (sel) =>
              sel.region === region_name && coords_eq(sel.coords, cell_coords),
          )}
          {@const null_value_html =
            region_name === "inserter" && field.has_default
              ? icon_sparkles
              : icon_cube_transparent}
          {@const invalid_value =
            field.not_null && !field.has_default && cell_data.c === undefined}
          <!-- svelte-ignore a11y_click_events_have_key_events -->
          <div
            aria-colindex={field_index}
            aria-rowindex={row_index}
            aria-selected={cell_selected}
            class="lens-table__cell"
            onmousedown={(ev) => on_cell_click(ev, cell_coords)}
            ondblclick={() => {
              datum_editor?.focus();
            }}
            role="gridcell"
            style:width={`${field.field.table_width_px}px`}
            tabindex="-1"
          >
            <div
              class="lens-cell__container"
              class:lens-cell__container--selected={cell_selected}
            >
              {#if cell_data.t === "Text"}
                <div
                  class="lens-cell__content lens-cell__content--text"
                  class:lens-cell__content--null={cell_data.c === undefined}
                >
                  {#if cell_data.c === undefined}
                    {@html null_value_html}
                  {:else}
                    {cell_data.c}
                  {/if}
                </div>
              {:else if cell_data.t === "Uuid"}
                <div
                  class="lens-cell__content lens-cell__content--uuid"
                  class:lens-cell__content--null={cell_data.c === undefined}
                >
                  {#if cell_data.c === undefined}
                    {@html null_value_html}
                  {:else}
                    {cell_data.c}
                  {/if}
                </div>
              {:else}
                <div class="lens-cell__content lens-cell__content--unknown">
                  <div>UNKNOWN</div>
                </div>
              {/if}
              {#if invalid_value}
                <div class="lens-cell__notice">
                  {@html icon_exclamation_circle}
                </div>
              {/if}
            </div>
          </div>
        {/each}
      </div>
    {/each}
  {/if}
{/snippet}

<div class="lens-grid">
  {#if lazy_data}
    <div
      bind:this={table_element}
      class="lens-table"
      onfocus={() => {
        try_queue_delta();
      }}
      onkeydown={handle_table_keydown}
      role="grid"
      tabindex="0"
    >
      <div class={["lens-table__headers"]}>
        {#each lazy_data.fields as _, field_index}
          <FieldHeader
            bind:field={lazy_data.fields[field_index]}
            index={field_index}
          />
        {/each}
        <div class="lens-table__header-actions">
          <FieldAdder {columns}></FieldAdder>
        </div>
      </div>
      <div class="lens-table__main">
        {@render table_region({
          region_name: "main",
          rows: lazy_data.rows,
          on_cell_click: handle_main_cell_click,
        })}
      </div>
      <form method="post" action="insert">
        <div class="lens-table__inserter">
          <div class="lens-inserter__rows">
            {@render table_region({
              region_name: "inserter",
              rows: inserter_rows,
              on_cell_click: handle_inserter_cell_click,
            })}
          </div>
          <button
            class="lens-inserter__submit"
            onkeydown={(ev) => {
              // Prevent keypress (e.g. pressing Enter on the button to submit
              // it) from triggering a table interaction.
              ev.stopPropagation();
            }}
            type="submit"
          >
            {@html icon_cloud_arrow_up}
          </button>
        </div>
        {#each inserter_rows as row}
          {#each lazy_data.fields as field, field_index}
            <input
              type="hidden"
              name={field.field.name}
              value={JSON.stringify(row.data[field_index])}
            />
          {/each}
        {/each}
      </form>
    </div>
    <div class="table-viewer__datum-editor">
      {#if selections.length === 1}
        <DatumEditor
          bind:this={datum_editor}
          bind:value={editor_value}
          field_info={lazy_data.fields[selections[0].coords[1]]}
          on_change={() => {
            try_sync_edit_to_cells();
          }}
        />
      {/if}
    </div>
  {/if}
</div>
