<template>
  <QuickNav selected="Discover"></QuickNav>

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24">
    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
      Discover
    </h1>

    <!-- Quick Search -->
    <div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
      <input
        type="text"
        v-model="searchTerms"
        placeholder="Search…"
        class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
      />
      <button
        @click="searchAll()"
        class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
      >
        <fa icon="magnifying-glass" class="fa-fw"></fa>
      </button>
    </div>

    <!-- Result Tabs -->
    <div class="text-center text-slate-500 border-b border-slate-300">
      <ul class="flex flex-wrap justify-center gap-4 -mb-px">
        <li>
          <a
            href="#"
            @click="
              projects = [];
              isLocalActive = true;
              isRemoteActive = false;
              searchLocal();
            "
            v-bind:class="computedLocalTabClassNames()"
          >
            Nearby
            <span
              class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
              >{{ localCount }}</span
            >
          </a>
        </li>
        <li>
          <a
            href="#"
            v-bind:class="computedRemoteTabClassNames()"
            @click="
              projects = [];
              isRemoteActive = true;
              isLocalActive = false;
              searchAll();
            "
          >
            Anywhere
            <span
              class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
              >{{ remoteCount }}</span
            >
          </a>
        </li>
      </ul>
    </div>

    <div v-if="isLocalActive">
      <div v-if="!isChoosingSearchBox">
        <button
          class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="isChoosingSearchBox = true"
        >
          Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
        </button>
      </div>
      <div v-else>
        <button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
          Choose Location Below for Nearby Search
        </button>
        <button
          v-if="isNewMarkerSet"
          class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="storeSearchBox"
        >
          Store This Location for Nearby Search
        </button>
        <button
          v-if="searchBox"
          class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="forgetSearchBox"
        >
          Delete Stored Location
        </button>
        <button
          v-if="isNewMarkerSet"
          class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="resetLatLong"
        >
          Reset Marker
        </button>
        <button
          class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="cancelSearchBoxSelect"
        >
          Cancel
        </button>
      </div>
    </div>

    <!-- Loading Animation -->
    <div
      class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
      v-if="isLoading"
    >
      <fa icon="spinner" class="fa-spin-pulse"></fa>
    </div>

    <!-- Results List -->
    <InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
      <ul>
        <li
          class="border-b border-slate-300"
          v-for="project in projects"
          :key="project.handleId"
        >
          <a
            @click="onClickLoadProject(project.handleId)"
            class="block py-4 flex gap-4"
          >
            <div class="w-12">
              <EntityIcon
                :entityId="project.handleId"
                :iconSize="48"
                class="block border border-slate-300 rounded-md"
              ></EntityIcon>
            </div>

            <div class="grow">
              <h2 class="text-base font-semibold">{{ project.name }}</h2>
              <div class="text-sm">
                <fa icon="user" class="fa-fw text-slate-400"></fa>
                {{
                  didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
                }}
              </div>
            </div>
          </a>
        </li>
      </ul>
    </InfiniteScroll>

    <div
      v-if="isLocalActive && isChoosingSearchBox"
      style="height: 600px; width: 800px"
    >
      <l-map
        ref="map"
        :center="[localCenterLat, localCenterLong]"
        v-model:zoom="localZoom"
        @click="setMapPoint"
      >
        <l-tile-layer
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
          layer-type="base"
          name="OpenStreetMap"
        />
        <l-marker
          v-if="isNewMarkerSet"
          :lat-lng="[localCenterLat, localCenterLong]"
          @click="isNewMarkerSet = false"
        />
        <l-rectangle
          v-if="isNewMarkerSet"
          :bounds="[
            [localCenterLat - localLatDiff, localCenterLong - localLongDiff],
            [localCenterLat + localLatDiff, localCenterLong + localLongDiff],
          ]"
          :weight="1"
        />
      </l-map>
    </div>
  </section>
</template>

<script lang="ts">
import { LeafletMouseEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { Component, Vue } from "vue-facing-decorator";
import {
  LMap,
  LMarker,
  LRectangle,
  LTileLayer,
} from "@vue-leaflet/vue-leaflet";

import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { didInfo, ProjectData } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import EntityIcon from "@/components/EntityIcon.vue";

const DEFAULT_LAT_LONG_DIFF = 0.01;
const WORLD_ZOOM = 2;
const DEFAULT_ZOOM = 2;

interface Notification {
  group: string;
  type: string;
  title: string;
  text: string;
}

@Component({
  components: {
    LRectangle,
    QuickNav,
    InfiniteScroll,
    EntityIcon,
    LMap,
    LMarker,
    LTileLayer,
  },
})
export default class DiscoverView extends Vue {
  $notify!: (notification: Notification, timeout?: number) => void;

  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: Array<string> = [];
  apiServer = "";
  searchTerms = "";
  projects: ProjectData[] = [];
  isChoosingSearchBox = false;
  isLocalActive = true;
  isRemoteActive = false;
  isNewMarkerSet = false;
  localCenterLat = 0;
  localCenterLong = 0;
  localLatDiff = DEFAULT_LAT_LONG_DIFF;
  localLongDiff = DEFAULT_LAT_LONG_DIFF;
  localCount = 0;
  localZoom = DEFAULT_ZOOM;
  remoteCount = 0;
  searchBox: { name: string; bbox: BoundingBox } | null = null;
  isLoading = false;

  // make this function available to the Vue template
  didInfo = didInfo;

  async mounted() {
    await db.open();
    const settings = await db.settings.get(MASTER_SETTINGS_KEY);
    this.activeDid = settings?.activeDid || "";
    this.apiServer = settings?.apiServer || "";
    this.searchBox = settings?.searchBoxes?.[0] || null;
    this.resetLatLong();

    this.allContacts = await db.contacts.toArray();

    await accountsDB.open();
    const allAccounts = await accountsDB.accounts.toArray();
    this.allMyDids = allAccounts.map((acc) => acc.did);

    this.searchLocal();
  }

  public async buildHeaders(): Promise<HeadersInit> {
    const headers: HeadersInit = {
      "Content-Type": "application/json",
    };

    if (this.activeDid) {
      await accountsDB.open();
      const allAccounts = await accountsDB.accounts.toArray();
      const account = allAccounts.find((acc) => acc.did === this.activeDid);
      const identity = JSON.parse(account?.identity || "null");

      if (!identity) {
        throw new Error(
          "An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
        );
      }

      headers["Authorization"] = "Bearer " + (await accessToken(identity));
    } else {
      // it's OK without auth... we just won't get any identifiers
    }
    return headers;
  }

  public async searchAll(beforeId?: string) {
    let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);

    if (beforeId) {
      queryParams = queryParams + `&beforeId=${beforeId}`;
    }

    try {
      this.isLoading = true;
      const response = await fetch(
        this.apiServer + "/api/v2/report/plans?" + queryParams,
        {
          method: "GET",
          headers: await this.buildHeaders(),
        },
      );

      if (response.status !== 200) {
        const details = await response.text();
        console.log("Problem with full search:", details);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: `There was a problem accessing the server. Try again later.`,
          },
          -1,
        );

        throw details;
      }

      const results = await response.json();

      const plans: ProjectData[] = results.data;
      if (plans) {
        for (const plan of plans) {
          const { name, description, handleId, rowid } = plan;
          this.projects.push({ name, description, handleId, rowid });
        }
        this.remoteCount = this.projects.length;
      } else {
        throw JSON.stringify(results);
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      console.log("Error with feed load:", e);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: e.userMessage || "There was a problem retrieving projects.",
        },
        -1,
      );
    } finally {
      this.isLoading = false;
    }
  }

  public async searchLocal(beforeId?: string) {
    if (!this.searchBox) {
      this.projects = [];
      return;
    }

    const claimContents =
      "claimContents=" + encodeURIComponent(this.searchTerms);
    let queryParams = [
      claimContents,
      "minLocLat=" + this.searchBox.bbox.minLat,
      "maxLocLat=" + this.searchBox.bbox.maxLat,
      "westLocLon=" + this.searchBox.bbox.westLong,
      "eastLocLon=" + this.searchBox.bbox.eastLong,
    ].join("&");

    if (beforeId) {
      queryParams = queryParams + `&beforeId=${beforeId}`;
    }

    try {
      this.isLoading = true;
      const response = await fetch(
        this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
        {
          method: "GET",
          headers: await this.buildHeaders(),
        },
      );

      if (response.status !== 200) {
        const details = await response.text();
        console.log("Problem with nearby search:", details);
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error",
            text: "There was a problem accessing the server. Try again later.",
          },
          -1,
        );
        throw await response.text();
      }

      const results = await response.json();

      if (results.data) {
        if (beforeId) {
          const plans: ProjectData[] = results.data;
          for (const plan of plans) {
            const { name, description, handleId = plan.handleId, rowid } = plan;
            if (beforeId !== plan["rowid"]) {
              this.projects.push({ name, description, handleId, rowid });
            }
          }
        } else {
          this.projects = results.data;
        }
        this.localCount = this.projects.length;
      } else {
        throw JSON.stringify(results);
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      console.log("Error with feed load:", e);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text: e.userMessage || "There was a problem retrieving projects.",
        },
        -1,
      );
    } finally {
      this.isLoading = false;
    }
  }

  /**
   * Data loader used by infinite scroller
   * @param payload is the flag from the InfiniteScroll indicating if it should load
   **/
  async loadMoreData(payload: boolean) {
    if (this.projects.length > 0 && payload) {
      const latestProject = this.projects[this.projects.length - 1];
      if (this.isLocalActive) {
        this.searchLocal(latestProject["rowid"]);
      } else if (this.isRemoteActive) {
        this.searchAll(latestProject["rowid"]);
      }
    }
  }

  /**
   * Handle clicking on a project entry found in the list
   * @param id of the project
   **/
  onClickLoadProject(id: string) {
    localStorage.setItem("projectId", id);
    const route = {
      name: "project",
    };
    this.$router.push(route);
  }

  setMapPoint(event: LeafletMouseEvent) {
    if (this.isNewMarkerSet) {
      this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
      this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
    } else {
      // marker is not set
      this.localCenterLat = event.latlng.lat;
      this.localCenterLong = event.latlng.lng;

      let latDiff = DEFAULT_LAT_LONG_DIFF;
      let longDiff = DEFAULT_LAT_LONG_DIFF;
      // Guess at a size for the bounding box.
      // This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
      const bounds = event.target.boxZoom?._map?.getBounds();
      if (bounds) {
        latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
        longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
      }
      this.localLatDiff = latDiff;
      this.localLongDiff = longDiff;
      this.isNewMarkerSet = true;
    }
  }

  public resetLatLong() {
    if (this.searchBox?.bbox) {
      const bbox = this.searchBox.bbox;
      this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
      this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
      this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
      this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
      this.localZoom = WORLD_ZOOM;
      this.isNewMarkerSet = true;
    } else {
      this.isNewMarkerSet = false;
    }
  }

  public async storeSearchBox() {
    if (this.localCenterLong || this.localCenterLat) {
      try {
        const newSearchBox = {
          name: "Local",
          bbox: {
            eastLong: this.localCenterLong + this.localLongDiff,
            maxLat: this.localCenterLat + this.localLatDiff,
            minLat: this.localCenterLat - this.localLatDiff,
            westLong: this.localCenterLong - this.localLongDiff,
          },
        };
        await db.open();
        db.settings.update(MASTER_SETTINGS_KEY, {
          searchBoxes: [newSearchBox],
        });
        this.searchBox = newSearchBox;
        this.isChoosingSearchBox = false;
        this.searchLocal();
      } catch (err) {
        this.$notify(
          {
            group: "alert",
            type: "danger",
            title: "Error Updating Search Settings",
            text: "Try going to a different page and then coming back.",
          },
          -1,
        );
        console.error(
          "Telling user to retry the location search setting because:",
          err,
        );
      }
    } else {
      this.$notify(
        {
          group: "alert",
          type: "warning",
          title: "No Location Selected",
          text: "Select a location on the map.",
        },
        -1,
      );
    }
  }

  public async forgetSearchBox() {
    try {
      await db.open();
      db.settings.update(MASTER_SETTINGS_KEY, {
        searchBoxes: [],
      });
      this.searchBox = null;
      this.localCenterLat = 0;
      this.localCenterLong = 0;
      this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
      this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
      this.localZoom = DEFAULT_ZOOM;
      this.isChoosingSearchBox = false;
      this.isNewMarkerSet = false;
      this.searchLocal();
    } catch (err) {
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Updating Search Settings",
          text: "Try going to a different page and then coming back.",
        },
        -1,
      );
      console.error(
        "Telling user to retry the location search setting because:",
        err,
      );
    }
  }

  public cancelSearchBoxSelect() {
    this.isChoosingSearchBox = false;
    this.localZoom = WORLD_ZOOM;
  }

  public computedLocalTabClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,
      active: this.isLocalActive,
      "text-blue-600": this.isLocalActive,
      "border-blue-600": this.isLocalActive,
      "font-semibold": this.isLocalActive,
      "border-transparent": !this.isLocalActive,
      "hover:text-slate-600": !this.isLocalActive,
      "hover:border-slate-300": !this.isLocalActive,
    };
  }

  public computedRemoteTabClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,
      active: this.isRemoteActive,
      "text-blue-600": this.isRemoteActive,
      "border-blue-600": this.isRemoteActive,
      "font-semibold": this.isRemoteActive,
      "border-transparent": !this.isRemoteActive,
      "hover:text-slate-600": !this.isRemoteActive,
      "hover:border-slate-300": !this.isRemoteActive,
    };
  }
}
</script>
