<script lang="ts">
  import { S } from '$lib';
  import Page from '$lib/Page.svelte';
  import Titlebar from '$lib/Titlebar.svelte';
  import { onDestroy, onMount } from 'svelte';
  import type { Match, StoptimesResponse } from '$lib/motis-types';
  import { pushState } from '$app/navigation';
  import { base } from '$app/paths';
  import Timetable from '$lib/Timetable.svelte';
  import { page } from '$app/state';
  import { m } from '$lib/paraglide/messages';
  import motis from '$lib/motis-api';
  import { placeNameMap } from '$lib/aliases';

  const normaliseGermanUmlauts = (n: string) => {
    return n
      .replace(/ü/gu, 'ue')
      .replace(/Ü/gu, 'UE')
      .replace(/ä/gu, 'ae')
      .replace(/Ä/gu, 'AE')
      .replace(/ö/gu, 'oe')
      .replace(/ß/gu, 'ss');
  };
  const normalisePlaceName = (n: string) =>
    placeNameMap.has(n.toLowerCase()) ? placeNameMap.get(n.toLowerCase())! : n;
  const arePlacenamesEqual = (n1: string, n2: string) =>
    normalisePlaceName(
      normaliseGermanUmlauts(n1).toUpperCase()
    ).toUpperCase() ===
    normalisePlaceName(normaliseGermanUmlauts(n2)).toUpperCase();

  let searchQuery = $state('');
  let searchSuggestionIdx = $state(0);
  let _lastSearchQuery = $state('');
  let _lastSearchQueryAt = $state(0);
  let _searchQueryAbortController: AbortController | null = null;
  let _searchResults = $state([] as Match[]);
  let searchResults = $derived.by(() => {
    const filtered = _searchResults
      .filter(
        (v, i, a) =>
          v.type === 'STOP' ||
          v.type === 'ADDRESS' ||
          a.findIndex((v2) => arePlacenamesEqual(v.name, v2.name)) === i
      )
      .filter((v) => v);
    filtered.sort((a, b) => {
      const typeScores: Record<typeof a.type, number> = {
        PLACE: 0, // POI
        ADDRESS: 15, // Physical Address, Street
        STOP: 30, // OV Stop
      };
      const aScore = typeScores[a.type];
      const bScore = typeScores[b.type];
      return bScore - aScore;
    });
    return filtered;
  });
  let searchField: HTMLInputElement | null = null;
  let searchFieldFocused = $state(false);
  let objectId = $state(null as null | string);
  let isArrivals = $state(false);
  let objectIdName = $state('');

  const handleNavigationTo = (newUrl: URL) => {
    console.debug('Navigated to ', newUrl);

    const newObjIdName = newUrl.searchParams.get('placename');
    const newObjId = newUrl.searchParams.get('placeid');

    if (newObjId === null || newObjId === '') {
      if (newObjIdName === null || newObjIdName === '') {
        searchQuery = '';
        objectIdName = '';
        objectId = null;
        refetchResultsLoop(true);
      } else {
        searchQuery = newObjIdName;
        objectIdName = newObjIdName;
        objectId = null;
        refetchResultsLoop(true);
        search(newObjIdName);

        searchFieldFocused = true;
        if (searchField) searchField.focus();
      }
    } else {
      if (newObjIdName !== objectIdName)
        (objectIdName = newObjIdName ?? ''), (searchQuery = newObjIdName ?? '');
      if (newObjId !== objectId) {
        objectId = newObjId;
        refetchResultsLoop(true);
      }
    }
  };
  $effect(() => {
    void page.url; // listen to this
    handleNavigationTo(new URL(location.href)); // and run this (page.url isnt always accurate for some reason)
  });

  let fetchDelay = $state(10000);
  let now = $state(0);
  let lastFetchAt = $state(0);
  let nextFetchAt = $derived(lastFetchAt + fetchDelay);
  let progressKind = $state('dead' as 'dead' | 'waiting' | 'fetch');
  let hqProgressIndicator = $state(false);
  let stopTimes = $state(null as null | StoptimesResponse);
  let hasQueriedResults = $state(false);
  let renderedObjectIdName = $derived(
    normalisePlaceName(objectIdName || (stopTimes?.place?.name ?? ''))
  );

  const search = async (query: string) => {
    if (placeNameMap.has(query.toLowerCase()))
      query = placeNameMap.get(query.toLowerCase()) ?? query;
    if (query === '') {
      _lastSearchQueryAt = performance.now();
      _searchQueryAbortController?.abort('Search Cancelled');
      _searchResults = [];
      return;
    }
    console.debug('Querying', query);
    await new Promise((rs) => setTimeout(rs, 50));
    for (let i = 0; i < 2; i++)
      if (_lastSearchQueryAt + 150 > performance.now())
        await new Promise((rs) =>
          setTimeout(rs, _lastSearchQueryAt + 150 - performance.now())
        );
      else break;
    if (query !== searchQuery) return; // typed again, new func call
    if (query === _lastSearchQuery) return; // results are up-to-date, ignore
    _searchQueryAbortController?.abort('Newer Search Started'); // we raced it
    _searchQueryAbortController = new AbortController();
    _lastSearchQueryAt = performance.now();
    const response = await motis
      .fetch(
        `/api/v1/geocode?text=${encodeURIComponent(query)}${
          localStorage.getItem('geocode-query-options') ?? // e.g. &type=STOP
          '&type=STOP'
        }&language=${m.lang_short()}`,
        {
          signal: _searchQueryAbortController.signal,
        }
      )
      .catch((e) => {
        _searchQueryAbortController = null;
        throw e;
      });
    if (response.status !== 200)
      throw new Error(
        `Got non-200 status code ${response.status} - ${await response.text().catch((e) => `Failed to get text: ${e}`)}`
      );
    _lastSearchQueryAt = performance.now();
    _searchQueryAbortController = null;
    if (searchQuery === query) {
      // still nothing typed
      _lastSearchQuery = query;
      // FIXME: validate this
      _searchResults = await response.json();
      searchSuggestionIdx = 0;
    }
  };
  $effect(() => {
    if (searchQuery)
      console.debug('searchQuery changed to', searchQuery, '- searching...');

    search(searchQuery);
  });

  let debounce = false;
  let stackLen = 0;
  let destroyed = false;
  const stackOverflowPreventionQueue = [] as (() => void)[];
  let resultsLoopCancel: AbortController | null = null;
  const refetchResultsLoop = async (reset = false) => {
    if (destroyed) return;
    if (debounce && !reset) return;
    if (reset) resultsLoopCancel?.abort(), (hasQueriedResults = false);
    debounce = true;
    await (async () => {
      if (objectId === null) {
        progressKind = 'dead';
        lastFetchAt = 0;
        stopTimes = null;
      } else {
        if (lastFetchAt === 0 || reset) {
          lastFetchAt = performance.now() - fetchDelay;
        }
        if (performance.now() >= nextFetchAt) {
          progressKind = 'fetch';
          try {
            resultsLoopCancel = new AbortController();
            const json = await motis.getStopTimes(
              objectId,
              resultsLoopCancel.signal,
              isArrivals
              // 500,
              // new Date('2025-07-26T00:03:11Z')
            );
            hasQueriedResults = true;
            if (!json?.stopTimes) {
              stopTimes = null;
              throw new Error(
                `No stopTimes on object (got response ${JSON.stringify(json)})`
              );
            }
            // json.stopTimes = json.stopTimes.map((v) => ({
            //   ...v,
            //   cancelled: Math.random() > 0.5,
            // }));
            stopTimes = json;
            lastFetchAt = performance.now();
          } catch (error) {
            console.warn('Failed to update timetable: ', error);
            await new Promise((rs) => setTimeout(rs, 5000));
          }
          progressKind = 'waiting';
        }
        // update now every 75ms for smoother animation
        if (performance.now() > now + 75) now = performance.now();
        if (hqProgressIndicator)
          requestAnimationFrame(() => {
            if (stackLen > 1000) {
              stackLen = 0;
              stackOverflowPreventionQueue.push(() => {
                refetchResultsLoop(false);
              });
            } else {
              refetchResultsLoop(false);
              stackLen++;
            }
          });
        else
          setTimeout(() => {
            if (stackLen > 1000) {
              stackLen = 0;
              stackOverflowPreventionQueue.push(() => {
                refetchResultsLoop(false);
              });
            } else {
              refetchResultsLoop(false);
              stackLen++;
            }
          }, 10);
      }
    })().catch((e) => console.error(e));
    debounce = false;
  };
  onMount(
    () => ((globalThis as any)['refetchResultsLoop'] = refetchResultsLoop)
  );
  $effect(() => {
    if (
      stopTimes?.place?.name?.length &&
      searchQuery === '' &&
      searchFieldFocused === false
    )
      searchQuery = stopTimes.place.name;
  });
  let storageUpdateInterval: ReturnType<typeof setInterval> | undefined =
    undefined;
  let stackOverflowPreventionQueueInterval:
    | ReturnType<typeof setInterval>
    | undefined = undefined;
  let mountedAt = 0;
  onMount(() => {
    destroyed = false;
    const urlPlaceId = new URL(location.href).searchParams.get('placeid');
    if (urlPlaceId) objectId = urlPlaceId;
    const urlSearchQuery = new URL(location.href).searchParams.get('placename');
    if (urlSearchQuery) {
      objectIdName = urlSearchQuery;
      searchQuery = urlSearchQuery;
    }

    const storageUpdate = () => {
      const oldFetchDelay = fetchDelay;
      const lsFetchDelay = localStorage.getItem('fetch-delay-ms');
      fetchDelay = Math.floor(
        parseFloat(lsFetchDelay ?? fetchDelay.toString())
      );
      if (isNaN(fetchDelay)) fetchDelay = oldFetchDelay;
      if (fetchDelay !== oldFetchDelay || lsFetchDelay === null)
        localStorage.setItem('fetch-delay-ms', fetchDelay.toString());

      const newHqProgressIndicator = localStorage.getItem(
        'use-high-refresh-rate-progress-indicator'
      );
      if (
        newHqProgressIndicator === 'false' ||
        newHqProgressIndicator === 'true' ||
        newHqProgressIndicator === '0' ||
        newHqProgressIndicator === '1'
      )
        hqProgressIndicator =
          newHqProgressIndicator === 'true' || newHqProgressIndicator === '1';
      else
        localStorage.setItem(
          'use-high-refresh-rate-progress-indicator',
          hqProgressIndicator.toString()
        );

      const newMotisBackend = localStorage.getItem('motis-backend');
      try {
        if (!newMotisBackend) throw '';
        motis.backend = new URL(newMotisBackend).href;
      } catch (error) {
        localStorage.setItem('motis-backend', motis.backend.toString());
      }
    };
    storageUpdateInterval = setInterval(storageUpdate, 30000);
    storageUpdate();
    if (objectId) setTimeout(() => refetchResultsLoop(false), 10);
    mountedAt = performance.now();
    isArrivals = location.search.includes('arrivals');

    stackOverflowPreventionQueueInterval = setInterval(() => {
      stackOverflowPreventionQueue.shift()?.();
    }, 100);
  });
  onDestroy(() => clearInterval(storageUpdateInterval));
  onDestroy(() => (destroyed = true));
  const pushHistory = () => {
    if (
      objectId &&
      objectIdName &&
      mountedAt &&
      performance.now() > mountedAt + 1000
    ) {
      const newUrl = new URL(location.href);
      newUrl.pathname = base + `/fahrplan`;
      newUrl.searchParams.set('placename', objectIdName);
      newUrl.searchParams.set('placeid', objectId);
      if (newUrl.href !== location.href) {
        console.debug('Pushing', newUrl, 'to history');
        pushState(newUrl, {
          objectId,
          objectIdName,
        });
      }
    }
  };
</script>

<svelte:head>
  <noscript>
    <meta http-equiv="refresh" content="0; url=/fahrplan.html/{objectId}" />
  </noscript>
</svelte:head>

<Page
  title="{renderedObjectIdName ? `${renderedObjectIdName} - ` : ''}Fahrplan"
  class="p-8"
>
  <!-- {#snippet sidepanel()}
    <div class="{S.window_topbar} rounded-tl-xl">Fahrplan</div>
    <div class="my-2"></div>
  {/snippet} -->
  <div class={S.window_content_fullscreen}>
    <Titlebar>
      <div class="ctr relative flex-1 flex items-stretch justify-stretch">
        <input
          type="text"
          class="{S.input} rounded-xl w-full text-center font-medium"
          bind:value={searchQuery}
          bind:this={searchField}
          onfocus={() => (searchFieldFocused = true)}
          onblur={() => {
            searchFieldFocused = false;
            if (renderedObjectIdName) searchQuery = renderedObjectIdName;
          }}
          onkeydown={(e) => {
            const resultIdx = Math.max(
              Math.min(searchSuggestionIdx, searchResults.length - 1),
              0
            );
            let dir = 1;
            switch (e.key) {
              case 'Enter':
              case 'Return': {
                const result = searchResults[resultIdx];
                if (result) {
                  objectId = result.id;
                  objectIdName = result.name;
                  searchQuery = result.name;
                  e.currentTarget.blur();
                  pushHistory();
                  setTimeout(() => refetchResultsLoop(true), 10);
                }
                break;
              }
              case 'ArrowUp':
                dir = -1;
              // fall through
              case 'ArrowDown': {
                if (searchResults.length !== 0) {
                  e.preventDefault();
                  searchSuggestionIdx =
                    dir === -1 && resultIdx % 1 !== 0
                      ? Math.floor(resultIdx)
                      : Math.floor(resultIdx + dir);
                  if (searchSuggestionIdx < 0)
                    searchSuggestionIdx = searchResults.length - 1;
                  else if (searchSuggestionIdx >= searchResults.length)
                    searchSuggestionIdx = 0;
                }
                break;
              }
            }
          }}
        />
        <div
          class="absolute top-[calc(100%_+_1rem)] left-0 w-full h-max flex items-center justify-center z-20"
        >
          {#if searchFieldFocused && searchResults.length !== 0}
            <div
              class="w-xl bg-[#27272BAA] backdrop-blur-3xl p-4 rounded-2xl flex flex-col gap-2"
            >
              {#each searchResults as result, idx}
                <!-- svelte-ignore a11y_no_static_element_interactions -->
                <!-- svelte-ignore a11y_click_events_have_key_events -->
                <div
                  class={{
                    'w-full p-4 rounded-xl cursor-pointer': true,
                    'bg-[#3b3b41]': idx === searchSuggestionIdx,
                  }}
                  onmouseenter={() => {
                    searchSuggestionIdx = idx;
                  }}
                  onmouseleave={() => {
                    searchSuggestionIdx = idx + 0.000001;
                  }}
                  onmousedown={(e) => {
                    e.preventDefault(); // prevent early unfocusing
                  }}
                  onclick={(e) => {
                    objectId = result.id;
                    objectIdName = result.name;
                    searchQuery = result.name;
                    pushHistory();
                    if (searchField) {
                      setTimeout(() => {
                        searchField?.blur();
                      }, 10);
                      searchField.value = result.name;
                      setTimeout(() => refetchResultsLoop(true), 10);
                    }
                  }}
                >
                  {result.name}
                  <span class="opacity-50 font-normal ml-1"
                    >{[
                      result.areas.find((v) => v.default)?.name,
                      result.type.charAt(0).toUpperCase() +
                        result.type.substring(1).toLowerCase(),
                    ]
                      .filter((v) => v)
                      .join(', ')}</span
                  >
                </div>
              {/each}
            </div>
          {/if}
        </div>
      </div>
    </Titlebar>
    <div class="w-full h-[2px] rounded-[2px] relative">
      {#if progressKind === 'fetch'}
        <div
          class="h-[2px] rounded-[2px] bg-white/50 w-full left-0 absolute"
          style="animation:loadingbar infinite;animation-play-state:playing;animation-duration:2s;animation-timing-function:ease-in-out;"
        ></div>
      {:else if progressKind === 'waiting'}
        <div
          class="h-[2px] rounded-[2px] bg-white/20 transition-[all_100ms]"
          style="width:{Math.min(
            ((now - lastFetchAt) * 100) / fetchDelay,
            100
          )}%"
        ></div>
      {/if}
    </div>
    <div class="p-3">
      <div class="flex flex-col gap-2 max-w-[100%]">
        <Timetable
          {stopTimes}
          {isArrivals}
          placeName={renderedObjectIdName}
          placeId={objectId}
          isResultsPage={objectId !== null && hasQueriedResults}
          setSearch={(q) => {
            searchQuery = q;
            searchField?.focus();
          }}
        />
      </div>
    </div>
  </div>
</Page>

<!-- svelte-ignore a11y_consider_explicit_label -->
<button
  class="fixed bottom-4 right-4 {S.button('secondary')
    .replace('not-disabled:bg-[#0000]', 'not-disabled:bg-[#2E2E3299]')
    .replace(
      /rounded-[0-9a-z]+/,
      'rounded-full'
    )} p-4 backdrop-blur-3xl flex sm:hidden"
  onclick={() => {
    searchField?.focus();
  }}
>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="24"
    height="24"
    fill="none"
    viewBox="0 0 24 24"
    ><path
      fill="#fff"
      fill-rule="evenodd"
      d="M3 10.5a6.5 6.5 0 1 1 13 0 6.5 6.5 0 0 1-13 0M9.5 3a7.5 7.5 0 1 0 5.022 13.07l6.151 5.308.654-.757-6.107-5.27A7.5 7.5 0 0 0 9.5 3"
      clip-rule="evenodd"
    ></path></svg
  >
</button>
