<script lang="ts">
  import UpObject from "../components/display/UpObject.svelte";
  import { putEntityAttribute, putEntry, queryOnce } from "../lib/api";
  import Selector from "../components/utils/Selector.svelte";
  import { onMount } from "svelte";
  import type { ZoomTransform } from "d3";
  import Spinner from "../components/utils/Spinner.svelte";
  import UpObjectCard from "../components/display/UpObjectCard.svelte";
  import BlobPreview from "../components/display/BlobPreview.svelte";
  import { i18n } from "../i18n";
  import type { IValue } from "upend/types";
  import { useNavigate } from "svelte-navigator";
  const navigate = useNavigate();

  const urlParams = new URLSearchParams(
    window.location.href.substring(window.location.href.indexOf("?"))
  );

  export let x: string = urlParams.get("x");
  export let y: string = urlParams.get("y");
  $: if (x && y) navigate(`/surface?x=${x}&y=${y}`, { replace: true });

  let viewMode = "link";

  let currentX = NaN;
  let currentY = NaN;

  let loaded = false;
  let viewHeight = 0;
  let viewWidth = 0;

  interface IPoint {
    address: string;
    x: number;
    y: number;
  }
  let points: IPoint[] = [];
  async function loadPoints() {
    points = [];
    const result = await queryOnce(`(matches ? (in "${x}" "${y}") ?)`);

    points = Object.entries(result.objects)
      .map(([address, obj]) => {
        let objX = parseInt(String(obj.get(x)));
        let objY = parseInt(String(obj.get(y)));

        if (objX && objY) {
          return {
            address,
            x: objX,
            y: objY,
          };
        }
      })
      .filter(Boolean);
  }

  $: {
    if (x && y) {
      loadPoints();
    }
  }

  let selectorCoords: [number, number] | null = null;

  onMount(async () => {
    const d3 = await import("d3");

    const view = d3.select(".view");

    const svg = view.append("svg");

    const xScale = d3
      .scaleLinear()
      .domain([0, viewWidth])
      .range([0, viewWidth]);

    const xAxis = d3
      .axisBottom(xScale)
      .ticks(15)
      .tickSize(viewHeight)
      .tickPadding(5 - viewHeight);

    const yScale = d3
      .scaleLinear()
      .domain([0, viewHeight])
      .range([viewHeight, 0]);

    const yAxis = d3
      .axisRight(yScale)
      .ticks(viewHeight / (viewWidth / 15))
      .tickSize(viewWidth)
      .tickPadding(5 - viewWidth);

    const gX = svg.append("g").attr("class", "axis axis--x").call(xAxis);
    const gY = svg.append("g").attr("class", "axis axis--y").call(yAxis);

    const zoom = d3
      .zoom()
      // .scaleExtent([1, 40])
      // .translateExtent([
      //   [-100, -100],
      //   [width + 90, height + 100],
      // ])
      .on("zoom", zoomed);

    function zoomed({ transform }: { transform: ZoomTransform }) {
      const points = d3.select(".content");
      points.style(
        "transform",
        `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`
      );
      const allPoints = d3.selectAll(".point");
      allPoints.style("transform", `scale(${1 / transform.k})`);

      gX.call(xAxis.scale(transform.rescaleX(xScale)));
      gY.call(yAxis.scale(transform.rescaleY(yScale)));
    }

    function reset() {
      svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity);
    }

    view.on("mousemove", (ev: MouseEvent) => {
      // not using offsetXY because `translate` transforms on .inner mess it up
      const viewBBox = (view.node() as HTMLElement).getBoundingClientRect();
      const [x, y] = d3
        .zoomTransform(d3.select(".content").node() as HTMLElement)
        .invert([ev.clientX - viewBBox.left, ev.clientY - viewBBox.top]);

      currentX = xScale.invert(x);
      currentY = yScale.invert(y);
    });

    d3.select(".view")
      .call(zoom)
      .on("dblclick.zoom", (ev: MouseEvent) => {
        selectorCoords = [currentX, currentY];
      });
    loaded = true;
  });

  async function onSelectorInput(ev: CustomEvent<IValue>) {
    const [xValue, yValue] = selectorCoords;
    selectorCoords = null;
    await Promise.all(
      [
        [x, xValue],
        [y, yValue],
      ].map(([axis, value]: [string, number]) =>
        putEntityAttribute(ev.detail.c as string, axis, {
          t: "Number",
          c: value,
        })
      )
    );
    await loadPoints();
  }
</script>

<div class="surface">
  <div class="header">
    <div class="axis-selector">
      X: <Selector type="attribute" bind:attribute={x} />
    </div>
    <div class="axis-selector">
      Y: <Selector type="attribute" bind:attribute={y} />
    </div>
    <div class="view-mode-selector">
      {$i18n.t("View as")}
      <select bind:value={viewMode}>
        <option value="link">{$i18n.t("Link")}</option>
        <option value="card">{$i18n.t("Card")}</option>
        <!-- <option value="preview">{$i18n.t("Preview")}</option> -->
      </select>
    </div>
    <div class="position">
      {$i18n.t("Current position")}:
      <div class="label">
        <em>X:</em>
        {x || "?"} = {(Math.round(currentX * 100) / 100).toLocaleString("en", {
          useGrouping: false,
          minimumFractionDigits: 2,
        })}
      </div>
      <div class="label">
        <em>Y:</em>
        {y || "?"} = {(Math.round(currentY * 100) / 100).toLocaleString("en", {
          useGrouping: false,
          minimumFractionDigits: 2,
        })}
      </div>
    </div>
  </div>
  <div
    class="view"
    class:loaded
    bind:clientWidth={viewWidth}
    bind:clientHeight={viewHeight}
  >
    {#if !loaded}
      <div class="loading">
        <Spinner centered="absolute" />
      </div>
    {/if}
    <div class="content">
      {#if selectorCoords !== null}
        <div
          class="point selector"
          style="
            left: {selectorCoords[0]}px;
            top: {viewHeight - selectorCoords[1]}px"
        >
          <Selector
            type="value"
            valueTypes={["Address"]}
            on:input={onSelectorInput}
            on:focus={(ev) => {
              if (!ev.detail) selectorCoords = null;
            }}
          />
        </div>
      {/if}
      {#each points as point}
        <div
          class="point"
          style="left: {point.x}px; top: {viewHeight - point.y}px"
        >
          <div class="inner">
            {#if viewMode == "link"}
              <UpObject link address={point.address} />
            {:else if viewMode == "card"}
              <UpObjectCard address={point.address} />
            {:else if viewMode == "preview"}
              <BlobPreview address={point.address} />
            {/if}
          </div>
        </div>
      {/each}
    </div>
  </div>
</div>

<style lang="scss">
  .surface {
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  .header {
    display: flex;
    gap: 2em;
    padding: 1em;
    border-bottom: 1px solid var(--foreground);
    align-items: center;

    .axis-selector {
      display: flex;
      gap: 1em;
      align-items: center;
    }

    .position {
      display: flex;
      gap: 1em;
      em {
        font-weight: bold;
        font-style: normal;
      }
    }
  }

  .view {
    flex-grow: 1;
    position: relative;

    overflow: hidden;

    :global(svg) {
      width: 100%;
      height: 100%;
    }

    :global(.tick text) {
      color: var(--foreground-lightest);
      font-size: 1rem;
    }

    .content {
      transform-origin: 0 0;
    }

    .point {
      position: absolute;
      transform-origin: 0 0;

      .inner {
        transform: translate(-50%, -50%);
      }
    }

    &:not(.loaded) {
      pointer-events: none;
    }
  }

  .loading {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 99;
    transform: scale(3);
  }
</style>
