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

  <!-- CONTENT -->
  <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
    <!-- Heading -->
    <h1 id="ViewHeading" class="text-4xl text-center font-light">
      Discover Projects & People
    </h1>

    <OnboardingDialog ref="onboardingDialog" />

    <!-- Quick Search -->
    <div
      id="QuickSearch"
      class="mt-8 mb-4 flex"
      :style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
    >
      <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"
        v-on:keyup.enter="searchSelected()"
      />
      <button
        class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
        @click="searchSelected()"
      >
        <fa icon="magnifying-glass" class="fa-fw"></fa>
      </button>
    </div>

    <!-- Result Tabs -->
    <!-- Top Level Selection -->
    <div class="text-center text-slate-500 border-b border-slate-300 mb-4">
      <ul class="flex flex-wrap justify-center gap-4 -mb-px">
        <li>
          <a
            href="#"
            @click="
              projects = [];
              userProfiles = [];
              isProjectsActive = true;
              isPeopleActive = false;
              searchSelected();
            "
            v-bind:class="computedProjectsTabStyleClassNames()"
          >
            Projects
          </a>
        </li>
        <li>
          <a
            href="#"
            @click="
              projects = [];
              userProfiles = [];
              isProjectsActive = false;
              isPeopleActive = true;
              searchSelected();
            "
            v-bind:class="computedPeopleTabStyleClassNames()"
          >
            People
          </a>
        </li>
      </ul>
    </div>

    <!-- Secondary 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 = [];
              userProfiles = [];
              isLocalActive = true;
              isMappedActive = false;
              isAnywhereActive = false;
              isSearchVisible = true;
              tempSearchBox = null;
              searchLocal();
            "
            v-bind:class="computedLocalTabStyleClassNames()"
          >
            Nearby
            <!-- restore when the links don't jump around for different numbers
            <span
              class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
              v-if="isLocalActive"
            >
              {{ localCount > -1 ? localCount : "?" }}
            </span>
            -->
          </a>
        </li>
        <li>
          <a
            href="#"
            @click="
              projects = [];
              userProfiles = [];
              isLocalActive = false;
              isMappedActive = true;
              isAnywhereActive = false;
              isSearchVisible = false;
              searchTerms = '';
              tempSearchBox = null;
            "
            v-bind:class="computedMappedTabStyleClassNames()"
          >
            <!-- search is triggered when map component gets to "ready" state -->
            Mapped
          </a>
        </li>
        <li>
          <a
            href="#"
            @click="
              projects = [];
              userProfiles = [];
              isLocalActive = false;
              isMappedActive = false;
              isAnywhereActive = true;
              isSearchVisible = true;
              tempSearchBox = null;
              searchAll();
            "
            v-bind:class="computedRemoteTabStyleClassNames()"
          >
            Anywhere
            <!-- restore when the links don't jump around for different numbers
            <span
              class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
              v-if="isAnywhereActive"
            >
              {{ remoteCount > -1 ? remoteCount : "?" }}
            </span>
            -->
          </a>
        </li>
      </ul>
    </div>

    <div v-if="isLocalActive">
      <div class="text-center">
        <button
          class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
          @click="$router.push({ name: 'search-area' })"
        >
          <fa icon="location-dot" class="fa-fw" />
          Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
        </button>
      </div>
    </div>

    <div v-if="isMappedActive && !tempSearchBox">
      <div class="mt-4 h-96 w-5/6 mx-auto">
        <l-map
          ref="projectMap"
          @ready="onMapReady"
          @moveend="onMoveEnd"
          @movestart="onMoveStart"
          @zoomend="onZoomEnd"
          @zoomstart="onZoomStart"
        >
          <l-tile-layer
            url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
            layer-type="base"
            name="OpenStreetMap"
          />
        </l-map>
      </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>
    <div
      v-else-if="projects.length === 0 && userProfiles.length === 0"
      class="text-center mt-8"
    >
      <p class="text-lg text-slate-500">
        <span v-if="isLocalActive">
          <span v-if="searchBox"> None found in the selected area. </span>
          <!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. -->
        </span>
        <span v-else-if="isAnywhereActive"
          >No projects were found with that search.</span
        >
      </p>
    </div>

    <!-- Results List -->
    <InfiniteScroll @reached-bottom="loadMoreData">
      <ul id="listDiscoverResults">
        <!-- Projects List -->
        <template v-if="isProjectsActive">
          <li
            class="border-b border-slate-300"
            v-for="project in projects"
            :key="project.handleId"
          >
            <a
              @click="onClickLoadItem(project.handleId)"
              class="block py-4 flex gap-4 cursor-pointer"
            >
              <div>
                <ProjectIcon
                  :entityId="project.handleId"
                  :iconSize="48"
                  :imageUrl="project.image"
                  class="block border border-slate-300 rounded-md max-h-12 max-w-12"
                />
              </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>
        </template>

        <!-- Profiles List -->
        <template v-else>
          <li
            class="border-b border-slate-300"
            v-for="profile in userProfiles"
            :key="profile.issuerDid"
          >
            <a
              @click="onClickLoadItem(profile?.rowId || '')"
              class="block py-4 flex gap-4 cursor-pointer"
            >
              <div class="grow">
                <div class="text-sm">
                  <fa icon="user" class="fa-fw text-slate-400"></fa>
                  {{
                    didInfo(
                      profile.issuerDid,
                      activeDid,
                      allMyDids,
                      allContacts,
                    )
                  }}
                </div>
                <p
                  v-if="profile.description"
                  class="mt-1 text-sm text-slate-600"
                >
                  {{ profile.description }}
                </p>
                <div
                  v-if="isAnywhereActive && profile.locLat && profile.locLon"
                  class="mt-1 text-xs text-slate-500"
                >
                  <fa icon="location-dot" class="fa-fw"></fa>
                  {{
                    (profile.locLat > 0 ? "North" : "South") +
                    " in " +
                    (profile.locLon > 0 ? "Eastern" : "Western") +
                    " Hemisphere"
                  }}
                </div>
              </div>
            </a>
          </li>
        </template>
      </ul>
    </InfiniteScroll>
  </section>
</template>

<script lang="ts">
import "leaflet/dist/leaflet.css";
import * as L from "leaflet";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";

import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue";
import ProjectIcon from "@/components/ProjectIcon.vue";
import OnboardingDialog from "@/components/OnboardingDialog.vue";
import TopMessage from "@/components/TopMessage.vue";
import { DEFAULT_PARTNER_API_SERVER, NotificationIface } from "@/constants/app";
import {
  db,
  logConsoleAndDb,
  retrieveSettingsForActiveAccount,
} from "@/db/index";
import { Contact } from "@/db/tables/contacts";
import { BoundingBox } from "@/db/tables/settings";
import {
  didInfo,
  errorStringForLog,
  getHeaders,
  PlanData,
} from "@/libs/endorserServer";
import { UserProfile } from "@/libs/partnerServer";
import { OnboardPage, retrieveAccountDids } from "@/libs/util";

interface Tile {
  indexLat: number;
  indexLon: number;
  minFoundLat: number;
  maxFoundLat: number;
  minFoundLon: number;
  maxFoundLon: number;
  recordCount: number;
}

@Component({
  components: {
    InfiniteScroll,
    LMap,
    LTileLayer,
    OnboardingDialog,
    ProjectIcon,
    QuickNav,
    TopMessage,
  },
})
export default class DiscoverView extends Vue {
  $notify!: (notification: NotificationIface, timeout?: number) => void;
  $router!: Router;
  $route!: RouteLocationNormalizedLoaded;

  activeDid = "";
  allContacts: Array<Contact> = [];
  allMyDids: Array<string> = [];
  apiServer = "";
  isLoading = false;
  isLocalActive = true;
  isMappedActive = false;
  isAnywhereActive = false;
  isProjectsActive = true;
  isPeopleActive = false;
  isSearchVisible = true;
  localCenterLat = 0;
  localCenterLong = 0;
  localCount = -1;
  markers: { [key: string]: L.Marker } = {};
  partnerApiServer = DEFAULT_PARTNER_API_SERVER;
  projects: PlanData[] = [];
  remoteCount = -1;
  searchBox: { name: string; bbox: BoundingBox } | null = null;
  searchTerms = "";
  tempSearchBox: BoundingBox | null = null;
  userProfiles: UserProfile[] = [];
  zoomedSoDoNotMove = false;

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

  async mounted() {
    const settings = await retrieveSettingsForActiveAccount();
    this.activeDid = (settings.activeDid as string) || "";
    this.apiServer = (settings.apiServer as string) || "";
    this.partnerApiServer =
      (settings.partnerApiServer as string) || this.partnerApiServer;
    this.searchBox = settings.searchBoxes?.[0] || null;

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

    this.allMyDids = await retrieveAccountDids();

    this.searchTerms = this.$route.query["searchText"]?.toString() || "";

    if (!settings.finishedOnboarding) {
      (this.$refs.onboardingDialog as OnboardingDialog).open(
        OnboardPage.Discover,
      );
    }

    if (this.searchBox) {
      await this.searchLocal();

      const bbox = this.searchBox.bbox;
      this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
      this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
    } else {
      this.isLocalActive = false;
      this.isMappedActive = false;
      this.isAnywhereActive = true;
      await this.searchAll();
    }
  }

  public resetCounts() {
    this.localCount = -1;
    this.remoteCount = -1;
  }

  public async searchSelected() {
    if (this.isLocalActive) {
      await this.searchLocal();
    } else if (this.isMappedActive) {
      const mapRef = this.$refs.projectMap as L.Map;
      this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
    } else {
      await this.searchAll();
    }
  }

  public async searchAll(beforeId?: string) {
    this.resetCounts();

    if (!beforeId) {
      // this was an initial search so clear any previous results
      this.projects = [];
      this.userProfiles = [];
    }

    let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);

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

    const endpoint = this.isProjectsActive
      ? this.apiServer + "/api/v2/report/plans"
      : this.partnerApiServer + "/api/partner/userProfile";

    try {
      this.isLoading = true;
      const response = await fetch(endpoint + "?" + queryParams, {
        method: "GET",
        headers: await getHeaders(this.activeDid),
      });

      if (response.status !== 200) {
        const details = await response.text();
        throw details;
      }

      const results = await response.json();

      if (this.isProjectsActive) {
        this.userProfiles = [];
        const plans: PlanData[] = results.data;
        if (plans) {
          this.projects.push(...plans);
          this.remoteCount = this.projects.length;
        } else {
          throw JSON.stringify(results);
        }
      } else {
        this.projects = [];
        const profiles: UserProfile[] = results.data;
        if (profiles) {
          this.userProfiles.push(...profiles);
          this.remoteCount = this.userProfiles.length;
        } else {
          throw JSON.stringify(results);
        }
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      console.error("Error with search all:", e);
      // this sometimes gives different information
      console.error("Error with search all (error added): " + e);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error Searching",
          text:
            e.userMessage ||
            "There was a problem retrieving " +
              (this.isProjectsActive ? "projects" : "profiles") +
              ".",
        },
        5000,
      );
    } finally {
      this.isLoading = false;
    }
  }

  public async searchLocal(beforeId?: string) {
    this.resetCounts();

    const searchBox =
      (this.isMappedActive && this.tempSearchBox) ||
      (this.isLocalActive && this.searchBox?.bbox);

    if (!searchBox) {
      this.projects = [];
      this.userProfiles = [];
      return;
    }

    if (!beforeId) {
      // this was an initial search so clear any previous results
      this.projects = [];
      this.userProfiles = [];
    }

    const claimContents =
      "claimContents=" + encodeURIComponent(this.searchTerms);

    let queryParams = [
      claimContents,
      "minLocLat=" + searchBox.minLat,
      "maxLocLat=" + searchBox.maxLat,
      "minLocLon=" + searchBox.westLong,
      "maxLocLon=" + searchBox.eastLong,
    ].join("&");

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

    const endpoint = this.isProjectsActive
      ? this.apiServer + "/api/v2/report/plansByLocation"
      : this.partnerApiServer + "/api/partner/userProfile";

    try {
      this.isLoading = true;
      const response = await fetch(endpoint + "?" + queryParams, {
        method: "GET",
        headers: await getHeaders(this.activeDid),
      });

      if (response.status !== 200) {
        const details = await response.text();
        throw details;
      }

      const results = await response.json();

      if (this.isProjectsActive) {
        this.userProfiles = [];
        const plans: PlanData[] = results.data;
        if (plans) {
          this.projects.push(...plans);
          this.localCount = this.projects.length;
        } else {
          throw JSON.stringify(results);
        }
      } else {
        this.projects = [];
        const profiles: UserProfile[] = results.data;
        if (profiles) {
          this.userProfiles.push(...profiles);
          this.localCount = this.userProfiles.length;
        } else {
          throw JSON.stringify(results);
        }
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
      console.error("Error with search local:", e);
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Error",
          text:
            e.userMessage ||
            "There was a problem retrieving " +
              (this.isProjectsActive ? "projects" : "profiles") +
              ".",
        },
        5000,
      );
    } 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 (payload) {
      if (this.isProjectsActive && this.projects.length > 0) {
        const latestProject = this.projects[this.projects.length - 1];
        if (this.isLocalActive || this.isMappedActive) {
          this.searchLocal(latestProject.rowId);
        } else if (this.isAnywhereActive) {
          this.searchAll(latestProject.rowId);
        }
      } else if (this.isPeopleActive && this.userProfiles.length > 0) {
        const latestProfile = this.userProfiles[this.userProfiles.length - 1];
        if (this.isLocalActive || this.isMappedActive) {
          this.searchLocal(latestProfile.rowId || "");
        } else if (this.isAnywhereActive) {
          this.searchAll(latestProfile.rowId || "");
        }
      }
    }
  }

  clearMarkers() {
    Object.values(this.markers).forEach((marker) => marker.remove());
    this.markers = {};
  }

  async onMapReady(map: L.Map) {
    // doing this here instead of the l-map element avoids a recentering after the first drag
    map.setView([this.localCenterLat, this.localCenterLong], 2);
    this.requestTiles(map);
  }

  // Tried but failed to use other vue-leaflet methods update:zoom and update:bounds
  // To access the from this.$refs, use this.$refs.projectMap.leafletObject (or maybe mapObject)

  onMoveStart(/* event: L.LocationEvent */) {
    // don't remove markers because they follow the map when moving (and the experience is jarring)
  }

  async onMoveEnd(event: L.LocationEvent) {
    if (this.zoomedSoDoNotMove) {
      // since a zoom triggers a moveend, too, don't duplicate a tile request
      this.zoomedSoDoNotMove = false;
    } else {
      // not part of a zoom so request tiles
      await this.requestTiles(event.target);
    }
  }

  onZoomStart(/* event: L.LocationEvent */) {
    // remove markers because otherwise they jump around at zoom end
    this.clearMarkers();

    this.zoomedSoDoNotMove = true;
  }

  async onZoomEnd(event: L.LocationEvent) {
    await this.requestTiles(event.target);
  }

  async requestTiles(targetMap: L.Map) {
    try {
      const bounds = targetMap.getBounds();
      const queryParams = [
        "minLocLat=" + bounds?.getSouthWest().lat,
        "maxLocLat=" + bounds?.getNorthEast().lat,
        "westLocLon=" + bounds?.getSouthWest().lng,
        "eastLocLon=" + bounds?.getNorthEast().lng,
      ].join("&");
      const endpoint = this.isProjectsActive
        ? this.apiServer + "/api/v2/report/planCountsByBBox"
        : this.partnerApiServer + "/api/partner/userProfileCountsByBBox";
      const response = await fetch(endpoint + "?" + queryParams);
      if (response.status === 200) {
        this.clearMarkers();
        const results = await response.json();
        if (results.data?.tiles?.length > 0) {
          for (const tile: Tile of results.data.tiles) {
            const pinLat = (tile.minFoundLat + tile.maxFoundLat) / 2;
            const pinLon = (tile.minFoundLon + tile.maxFoundLon) / 2;
            const numberIcon = L.divIcon({
              className: "numbered-marker",
              html: `<strong>${tile.recordCount}</strong>`,
              iconSize: [24, 24],
              // Why isn't this showing?
              iconAnchor: [12, 12], // coordinates of the tip of the icon relative to the top-left corner of the icon
            });
            const marker = L.marker([pinLat, pinLon], { icon: numberIcon });
            marker.addTo(targetMap);
            marker.on("click", () => {
              this.tempSearchBox = {
                minLat: tile.minFoundLat,
                maxLat: tile.maxFoundLat,
                westLong: tile.minFoundLon,
                eastLong: tile.maxFoundLon,
              };
              this.searchLocal();
            });
            this.markers[
              "" +
                tile.indexLat +
                "X" +
                tile.indexLon +
                "_" +
                tile.minFoundLat +
                "X" +
                tile.minFoundLon +
                "-" +
                tile.maxFoundLat +
                "X" +
                tile.maxFoundLon
            ] = marker;
          }
        }
      } else {
        throw {
          message: "Got an error loading projects on the map.",
          response: {
            status: response.status,
            statusText: response.statusText,
            url: response.url,
          },
        };
      }
    } catch (e) {
      logConsoleAndDb(
        "Error loading projects on the map: " + errorStringForLog(e),
        true,
      );
      this.$notify(
        {
          group: "alert",
          type: "danger",
          title: "Map Error",
          text: "There was a problem loading projects on the map.",
        },
        3000,
      );
    }
  }

  /**
   * Handle clicking on a project or profile entry found in the list
   * @param id of the project or profile
   **/
  onClickLoadItem(id: string) {
    const route = {
      path: this.isProjectsActive
        ? "/project/" + encodeURIComponent(id)
        : "/userProfile/" + encodeURIComponent(id),
    };
    this.$router.push(route);
  }

  public computedLocalTabStyleClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,

      active: this.isLocalActive,
      "text-black": this.isLocalActive,
      "border-black": this.isLocalActive,
      "font-semibold": this.isLocalActive,

      "text-blue-600": !this.isLocalActive,
      "border-transparent": !this.isLocalActive,
      "hover:border-slate-400": !this.isLocalActive,
    };
  }

  public computedMappedTabStyleClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,

      active: this.isMappedActive,
      "text-black": this.isMappedActive,
      "border-black": this.isMappedActive,
      "font-semibold": this.isMappedActive,

      "text-blue-600": !this.isMappedActive,
      "border-transparent": !this.isMappedActive,
      "hover:border-slate-400": !this.isMappedActive,
    };
  }

  public computedRemoteTabStyleClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,

      active: this.isAnywhereActive,
      "text-black": this.isAnywhereActive,
      "border-black": this.isAnywhereActive,
      "font-semibold": this.isAnywhereActive,

      "text-blue-600": !this.isAnywhereActive,
      "border-transparent": !this.isAnywhereActive,
      "hover:border-slate-400": !this.isAnywhereActive,
    };
  }

  public computedProjectsTabStyleClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,

      active: this.isProjectsActive,
      "text-black": this.isProjectsActive,
      "border-black": this.isProjectsActive,
      "font-semibold": this.isProjectsActive,

      "text-blue-600": !this.isProjectsActive,
      "border-transparent": !this.isProjectsActive,
      "hover:border-slate-400": !this.isProjectsActive,
    };
  }

  public computedPeopleTabStyleClassNames() {
    return {
      "inline-block": true,
      "py-3": true,
      "rounded-t-lg": true,
      "border-b-2": true,

      active: this.isPeopleActive,
      "text-black": this.isPeopleActive,
      "border-black": this.isPeopleActive,
      "font-semibold": this.isPeopleActive,

      "text-blue-600": !this.isPeopleActive,
      "border-transparent": !this.isPeopleActive,
      "hover:border-slate-400": !this.isPeopleActive,
    };
  }
}
</script>
<style>
.numbered-marker {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 14px;
  font-weight: bold;
  color: white;
  background: blue;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid white;
}
</style>
