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

<script lang="ts">
  import * as uuid from "uuid";

  import {
    type Coords,
    type Encodable,
    type Field,
    type Row,
    type FieldType,
    coords_eq,
  } from "./field.svelte";

  type CommittedChange = {
    coords_initial: Coords;
    // This will be identical to coords_initial, unless the change altered a
    // primary key.
    coords_updated: Coords;
    value_initial: Encodable;
    value_updated: Encodable;
  };

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

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

  type ParsedPkey = Record<string, Encodable>;

  let selections = $state<Selection[]>([]);
  let editing = $state(false);
  let editor_input_value = $state("");
  let committed_changes = $state<CommittedChange[][]>([]);
  let reverted_changes = $state<CommittedChange[][]>([]);
  let editor_input_element = $state<HTMLInputElement | 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;
    }
  }

  function try_parse_editor_value(
    field_type: FieldType,
  ): Encodable | undefined {
    if (field_type.t === "Text") {
      return {
        t: "Text",
        c: editor_input_value,
      };
    }
    if (field_type.t === "Uuid") {
      try {
        return {
          t: "Uuid",
          c: uuid.stringify(uuid.parse(editor_input_value)),
        };
      } catch {
        // uuid.parse() throws a TypeError if unsuccessful.
        return undefined;
      }
    }
    throw new Error("Unknown field type");
  }

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

  function set_selections(arr: Omit<Selection, "original_value">[]) {
    selections = arr.map((sel) => {
      let cell_data: Encodable | 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: Encodable | 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]];
      }
      if (cell_data?.t === "Text" || cell_data?.t === "Uuid") {
        editor_input_value = cell_data.c ?? "";
      } else {
        editor_input_value = "";
      }
    } else {
      editor_input_value = "";
    }
  }

  function try_move_selection(direction: "Down" | "Left" | "Right" | "Up") {
    if (lazy_data && !editing && selections.length > 0) {
      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 && editing && selections.length === 1) {
      const [sel] = selections;
      const parsed = try_parse_editor_value(
        lazy_data.fields[sel.coords[1]].field_type,
      );
      if (parsed !== undefined) {
        if (sel.region === "main") {
          lazy_data.rows[sel.coords[0]].data[sel.coords[1]] = parsed;
        } else if (sel.region === "inserter") {
          inserter_rows[sel.coords[0]].data[sel.coords[1]] = parsed;
        } else {
          throw new Error("Unknown region");
        }
      }
    }
  }

  function try_start_edit() {
    if (!editing) {
      editing = true;
      editor_input_element?.focus();
    }
  }

  function try_commit_edit() {
    (async function () {
      if (lazy_data && editing && selections.length === 1) {
        const [sel] = selections;
        const field = lazy_data.fields[sel.coords[1]];
        const parsed = try_parse_editor_value(field.field_type);
        if (parsed !== undefined) {
          if (sel.region === "main") {
            const pkey = JSON.parse(
              lazy_data.rows[sel.coords[0]].key as string,
            ) as ParsedPkey;
            const resp = await fetch("update-value", {
              method: "post",
              headers: { "Content-Type": "application/json" },
              body: JSON.stringify({
                column: field.name,
                pkeys: pkey,
                value: parsed,
              }),
            });
            if (resp.status >= 200 && resp.status < 300) {
              committed_changes.push([
                {
                  coords_initial: sel.coords,
                  coords_updated: sel.coords, // TODO: this assumes no inserted/deleted rows
                  value_initial: sel.original_value,
                  value_updated: parsed,
                },
              ]);
              editing = false;
              selections = [{ ...sel, original_value: parsed }];
              table_element?.focus();
            } else {
              // TODO display feedback to user
              console.error(resp);
              console.error(await resp.text());
            }
          } else if (sel.region === "inserter") {
            table_element?.focus();
            editing = false;
            selections = [{ ...sel, original_value: parsed }];
          } else {
            throw new Error("Unknown region");
          }
        } else {
          // TODO
        }
      }
    })().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);
    editing = false;
    table_element?.focus();
  }

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

  function handle_table_keydown(ev: KeyboardEvent) {
    const arrow_direction = arrow_key_direction(ev.key);
    if (arrow_direction) {
      try_move_selection(arrow_direction);
      ev.preventDefault();
    }
    if (ev.key === "Enter") {
      try_start_edit();
    }
  }

  function handle_table_cell_dblclick(_: Coords) {
    try_start_edit();
  }

  function handle_table_focus() {
    if (selections.length === 0 && (lazy_data?.rows[0]?.data.length ?? 0) > 0) {
      set_selections([{ region: "main", coords: [0, 0] }]);
    }
  }

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

  function handle_main_cell_click(ev: MouseEvent, coords: Coords) {
    if (!editing) {
      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 (!editing) {
      if (ev.metaKey || ev.ctrlKey) {
        // TODO
        // selections = [...selections.filter((prev) => !coords_eq(prev, coords)), coords];
        // editor_input_value = "";
      } else {
        set_selections([{ region: "inserter", coords }]);
      }
    }
  }

  // -------- Event Handlers: Editor -------- //

  function handle_editor_blur() {
    try_commit_edit();
  }

  function handle_editor_focus() {
    try_start_edit();
  }

  function handle_editor_input() {
    try_sync_edit_to_cells();
  }

  function handle_editor_keydown(ev: KeyboardEvent) {
    if (ev.key === "Enter") {
      try_commit_edit();
    } else if (ev.key === "Escape") {
      cancel_edit();
    }
  }

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

  (async function () {
    interface GetDataResponse {
      data: {
        pkey: string;
        data: Encodable[];
      }[];
      fields: Field[];
    }
    const resp = await fetch("get-data");
    const body: GetDataResponse = await resp.json();
    lazy_data = {
      fields: body.fields,
      rows: body.data.map(({ data, pkey }) => ({ data, key: pkey })),
    };
    inserter_rows = [
      {
        key: 0,
        data: body.fields.map(() => ({ t: "Text", c: undefined })),
      },
    ];
  })().catch(console.error);
</script>

{#snippet table_region({
  region_name,
  rows,
  on_cell_click,
}: {
  region_name: string;
  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),
          )}
          <!-- 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",
              cell_selected && "lens-table__cell--selected",
            ]}
            onmousedown={(ev) => on_cell_click(ev, cell_coords)}
            ondblclick={() => handle_table_cell_dblclick(cell_coords)}
            role="gridcell"
            style:width={`${field.width_px}px`}
            tabindex="-1"
          >
            {#if cell_data.t === "Text"}
              <div
                class={[
                  "lens-table__cell-content",
                  "lens-table__cell-content--text",
                  (cell_data.c ?? undefined) === undefined &&
                    "lens-table__cell-content--null",
                ]}
              >
                {cell_data.c ?? "Null"}
              </div>
            {:else if cell_data.t === "Uuid"}
              <div
                class={[
                  "lens-table__cell-content",
                  "lens-table__cell-content--uuid",
                  (cell_data.c ?? undefined) === undefined &&
                    "lens-table__cell-content--null",
                ]}
              >
                {cell_data.c ?? "Null"}
              </div>
            {:else}
              <div
                class={[
                  "lens-table__cell-content",
                  "lens-table__cell-content--unknown",
                ]}
              >
                UNKNOWN
              </div>
            {/if}
          </div>
        {/each}
      </div>
    {/each}
  {/if}
{/snippet}

<div class="lens-grid">
  {#if lazy_data}
    <div
      bind:this={table_element}
      class="lens-table"
      onfocus={handle_table_focus}
      onkeydown={handle_table_keydown}
      role="grid"
      tabindex="0"
    >
      <div class={["lens-table__headers"]}>
        {#each lazy_data.fields as field, field_index}
          <div
            aria-colindex={field_index}
            class="lens-table__header"
            role="columnheader"
            style:width={`${field.width_px}px`}
          >
            <div>{field.label ?? field.name}</div>
          </div>
        {/each}
        <div class="lens-table__header-actions">TODO</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>
      <div class="lens-table__inserter">
        {@render table_region({
          region_name: "inserter",
          rows: inserter_rows,
          on_cell_click: handle_inserter_cell_click,
        })}
      </div>
    </div>
    <div class="lens-editor">
      <input
        bind:this={editor_input_element}
        bind:value={editor_input_value}
        class={[
          "lens-editor__input",
          selections.length !== 1 && "lens-editor__input--hidden",
        ]}
        onblur={handle_editor_blur}
        onfocus={handle_editor_focus}
        oninput={handle_editor_input}
        onkeydown={handle_editor_keydown}
      />
    </div>
  {/if}
</div>
