import {
  mdiCrosshairsGps,
  mdiMagnify,
  mdiMapMarker,
  mdiMapSearchOutline,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-list";
import "../components/ha-list-item";
import "../components/ha-spinner";
import "../components/input/ha-input";
import type { HaInput } from "../components/input/ha-input";
import "../components/map/ha-locations-editor";
import type {
  HaLocationsEditor,
  MarkerLocation,
} from "../components/map/ha-locations-editor";
import type { ConfigUpdateValues } from "../data/core";
import { detectCoreConfig } from "../data/core";
import type { OpenStreetMapPlace } from "../data/openstreetmap";
import { reverseGeocode, searchPlaces } from "../data/openstreetmap";
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../types";
import { onBoardingStyles } from "./styles";

const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
const darkMql = matchMedia("(prefers-color-scheme: dark)");
const LOCATION_MARKER_ID = "location";

@customElement("onboarding-location")
class OnboardingLocation extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ attribute: false }) public onboardingLocalize!: LocalizeFunc;

  @state() private _working = false;

  @state() private _location: [number, number] = AMSTERDAM;

  @state() private _places?: OpenStreetMapPlace[] | null;

  @state() private _error?: string;

  @state() private _search = false;

  @state() private _highlightedMarker?: number;

  private _elevation?: string;

  private _unitSystem?: ConfigUpdateValues["unit_system"];

  private _currency?: ConfigUpdateValues["currency"];

  private _timeZone?: ConfigUpdateValues["time_zone"];

  private _country?: ConfigUpdateValues["country"];

  @query("ha-locations-editor", true) private map!: HaLocationsEditor;

  @query("ha-input") private _input?: HTMLElement;

  protected render(): TemplateResult {
    const addressAttribution = this.onboardingLocalize(
      "ui.panel.page-onboarding.core-config.location_address",
      {
        openstreetmap: html`<a
          href="https://www.openstreetmap.org/"
          target="_blank"
          rel="noopener noreferrer"
          >OpenStreetMap</a
        >`,
        osm_privacy_policy: html`<a
          href="https://wiki.osmfoundation.org/wiki/Privacy_Policy"
          target="_blank"
          rel="noopener noreferrer"
          >${this.onboardingLocalize(
            "ui.panel.page-onboarding.core-config.osm_privacy_policy"
          )}</a
        >`,
      }
    );

    return html`
      <h1>
        ${this.onboardingLocalize(
          "ui.panel.page-onboarding.core-config.location_header"
        )}
      </h1>
      ${
        this._error
          ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
          : nothing
      }

      <p>
        ${this.onboardingLocalize(
          "ui.panel.page-onboarding.core-config.intro_location"
        )}
      </p>

      <div class="location-search">
        <ha-input
          label=${this.onboardingLocalize(
            "ui.panel.page-onboarding.core-config.address_label"
          )}
          .disabled=${this._working}
          @keyup=${this._addressSearch}
        >
          <ha-svg-icon slot="start" .path=${mdiMagnify}></ha-svg-icon>
          ${
            this._working
              ? html`<ha-spinner slot="end" size="small"></ha-spinner>`
              : html`
                  <ha-icon-button
                    @click=${this._handleButtonClick}
                    slot="end"
                    .disabled=${this._working}
                    .label=${this.onboardingLocalize(
                      this._search
                        ? "ui.common.search"
                        : "ui.panel.page-onboarding.core-config.button_detect"
                    )}
                    .path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
                  ></ha-icon-button>
                `
          }
        </ha-input>
        ${
          this._places !== undefined
            ? html`
                <ha-list activatable>
                  ${
                    this._places?.length
                      ? this._places.map((place) => {
                          const primary = [
                            place.name || place.address[place.category],
                            place.address.house_number,
                            place.address.road || place.address.waterway,
                            place.address.village || place.address.town,
                            place.address.suburb || place.address.subdivision,
                            place.address.city || place.address.municipality,
                          ]
                            .filter(Boolean)
                            .join(", ");
                          const secondary = [
                            place.address.county ||
                              place.address.state_district ||
                              place.address.region,
                            place.address.state,
                            place.address.country,
                          ]
                            .filter(Boolean)
                            .join(", ");
                          return html`<ha-list-item
                            @click=${this._itemClicked}
                            .placeId=${place.place_id}
                            .selected=${this._highlightedMarker === place.place_id}
                            .activated=${this._highlightedMarker === place.place_id}
                            .twoline=${primary && secondary}
                          >
                            ${primary || secondary}
                            <span slot="secondary"
                              >${primary ? secondary : ""}</span
                            >
                          </ha-list-item>`;
                        })
                      : html`<ha-list-item noninteractive
                          >${
                            this._places === null ? "" : "No results"
                          }</ha-list-item
                        >`
                  }
                </ha-list>
              `
            : nothing
        }
      </div>
      <ha-locations-editor
        class="flex"
        .locations=${this._markerLocations(
          this._location,
          this._places,
          this._highlightedMarker
        )}
        zoom="14"
        .themeMode=${darkMql.matches ? "dark" : "light"}
        .disabled=${this._working}
        @location-updated=${this._locationChanged}
        @marker-clicked=${this._markerClicked}
      ></ha-locations-editor>

      <p class="attribution">${addressAttribution}</p>

      <div class="footer">
        <ha-button @click=${this._save} .disabled=${this._working}>
          ${this.onboardingLocalize(
            "ui.panel.page-onboarding.core-config.finish"
          )}
        </ha-button>
      </div>
    `;
  }

  protected firstUpdated(changedProps: PropertyValues<this>) {
    super.firstUpdated(changedProps);
    setTimeout(() => this._input!.focus(), 100);
    this.addEventListener("keyup", (ev) => {
      if (ev.key === "Enter") {
        this._save(ev);
      }
    });
  }

  protected updated(changedProps: PropertyValues) {
    if (changedProps.has("_highlightedMarker") && this._highlightedMarker) {
      const place = this._places?.find(
        (plc) => plc.place_id === this._highlightedMarker
      );
      if (place?.boundingbox?.length === 4) {
        this.map.fitBounds(
          [
            [place.boundingbox[0], place.boundingbox[2]],
            [place.boundingbox[1], place.boundingbox[3]],
          ],
          { zoom: 16, pad: 0 }
        );
      } else {
        this.map.fitMarker(String(this._highlightedMarker), { zoom: 16 });
      }
    }
  }

  private _markerLocations = memoizeOne(
    (
      location?: [number, number],
      places?: OpenStreetMapPlace[] | null,
      highlightedMarker?: number
    ): MarkerLocation[] => {
      if (!places) {
        return [
          {
            id: LOCATION_MARKER_ID,
            latitude: (location || AMSTERDAM)[0],
            longitude: (location || AMSTERDAM)[1],
            location_editable: true,
          },
        ];
      }
      return places?.length
        ? places.map((place) => ({
            id: String(place.place_id),
            iconPath:
              place.place_id === highlightedMarker ? undefined : mdiMapMarker,
            latitude:
              location && place.place_id === highlightedMarker
                ? location[0]
                : Number(place.lat),
            longitude:
              location && place.place_id === highlightedMarker
                ? location[1]
                : Number(place.lon),
            location_editable: place.place_id === highlightedMarker,
          }))
        : [];
    }
  );

  private _locationChanged(ev) {
    this._location = ev.detail.location;
    if (ev.detail.id !== LOCATION_MARKER_ID) {
      this._reverseGeocode();
    }
  }

  private _markerClicked(ev) {
    if (ev.detail.id === LOCATION_MARKER_ID) {
      return;
    }
    this._highlightedMarker = Number(ev.detail.id);
    const place = this._places!.find(
      (plc) => plc.place_id === Number(ev.detail.id)
    )!;
    this._location = [Number(place.lat), Number(place.lon)];
    this._country = place.address.country_code.toUpperCase();
  }

  private _itemClicked(ev) {
    this._highlightedMarker = ev.currentTarget.placeId;
    const place = this._places!.find(
      (plc) => plc.place_id === ev.currentTarget.placeId
    )!;
    this._location = [Number(place.lat), Number(place.lon)];
    this._country = place.address.country_code.toUpperCase();
  }

  private async _addressSearch(ev: KeyboardEvent) {
    ev.stopPropagation();
    this._search = ((ev.currentTarget as HaInput).value ?? "").length > 0;
    if (ev.key !== "Enter") {
      return;
    }
    this._searchAddress((ev.currentTarget as HaInput).value ?? "");
  }

  private async _searchAddress(address: string) {
    this._working = true;
    this._highlightedMarker = undefined;
    this._error = undefined;
    this._places = null;
    this.map.addEventListener(
      "markers-updated",
      () => {
        setTimeout(() => {
          if ((this._places?.length || 0) > 2) {
            this.map.fitMap({ pad: 0.5 });
          }
        }, 500);
      },
      {
        once: true,
      }
    );
    try {
      this._places = await searchPlaces(address, this.hass, true, 3);
      if (this._places?.length) {
        this._highlightedMarker = this._places[0].place_id;
        this._location = [
          Number(this._places[0].lat),
          Number(this._places[0].lon),
        ];
        this._country = this._places[0].address.country_code.toUpperCase();
      }
    } catch (e: any) {
      this._places = undefined;
      this._error = e.message;
    } finally {
      this._working = false;
    }
  }

  private async _reverseGeocode() {
    if (!this._location) {
      return;
    }
    this._places = null;
    const reverse = await reverseGeocode(this._location, this.hass);
    this._country = reverse.address.country_code.toUpperCase();
    this._places = [reverse];
    this._highlightedMarker = reverse.place_id;
  }

  private async _handleButtonClick(ev) {
    if (this._search) {
      this._searchAddress(ev.target.parentElement.value);
      return;
    }
    this._detectLocation();
  }

  private _detectLocation() {
    if (window.isSecureContext && navigator.geolocation) {
      this._working = true;
      const options = {
        enableHighAccuracy: true,
        timeout: 5000,
        maximumAge: 0,
      };
      navigator.geolocation.getCurrentPosition(
        async (result) => {
          this.map.addEventListener(
            "markers-updated",
            () => {
              this.map.fitMarker(LOCATION_MARKER_ID);
            },
            {
              once: true,
            }
          );
          this._location = [result.coords.latitude, result.coords.longitude];
          if (result.coords.altitude) {
            this._elevation = String(Math.round(result.coords.altitude));
          }
          try {
            await this._reverseGeocode();
          } finally {
            this._working = false;
          }
        },
        () => {
          // GPS is not available, get location based on IP
          this._working = false;
          this._whoAmI();
        },
        options
      );
    } else {
      this._whoAmI();
    }
  }

  private async _whoAmI() {
    const confirm = await showConfirmationDialog(this, {
      title: this.onboardingLocalize(
        "ui.panel.page-onboarding.core-config.title_location_detect"
      ),
      text: this.onboardingLocalize(
        "ui.panel.page-onboarding.core-config.intro_location_detect"
      ),
    });
    if (!confirm) {
      return;
    }
    this._working = true;
    try {
      const values = await detectCoreConfig(this.hass);

      if (values.latitude && values.longitude) {
        this.map.addEventListener(
          "markers-updated",
          () => {
            this.map.fitMarker(LOCATION_MARKER_ID);
          },
          {
            once: true,
          }
        );
        this._location = [Number(values.latitude), Number(values.longitude)];
      }
      if (values.elevation) {
        this._elevation = String(values.elevation);
      }
      if (values.unit_system) {
        this._unitSystem = values.unit_system;
      }
      if (values.time_zone) {
        this._timeZone = values.time_zone;
      }
      if (values.currency) {
        this._currency = values.currency;
      }
      if (values.country) {
        this._country = values.country;
      }
    } catch (err: any) {
      this._error = `Failed to detect location information: ${err.message}`;
    } finally {
      this._working = false;
    }
  }

  private async _save(ev) {
    if (!this._location) {
      return;
    }
    ev.preventDefault();
    fireEvent(this, "value-changed", {
      value: {
        location: this._location!,
        country: this._country,
        elevation: this._elevation,
        unit_system: this._unitSystem,
        time_zone: this._timeZone,
        currency: this._currency,
      },
    });
  }

  static get styles(): CSSResultGroup {
    return [
      onBoardingStyles,
      css`
        .location-search {
          margin-top: 32px;
          margin-bottom: 32px;
        }
        ha-locations-editor {
          display: block;
          height: 300px;
          margin-top: 8px;
          border-radius: var(--mdc-shape-large, 16px);
          overflow: hidden;
        }
        ha-list {
          width: 100%;
          border: 1px solid var(--divider-color);
          box-sizing: border-box;
          border-top-width: 0;
          border-bottom-left-radius: var(--mdc-shape-small, 4px);
          border-bottom-right-radius: var(--mdc-shape-small, 4px);
          --mdc-list-vertical-padding: 0;
        }
        ha-list-item {
          height: 72px;
        }
        .attribution {
          /* input helper style */
          margin: 0;
          padding: 4px 16px 12px 16px;
          color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
          font-family: var(
            --mdc-typography-caption-font-family,
            var(--mdc-typography-font-family, var(--ha-font-family-body))
          );
          font-size: var(
            --mdc-typography-caption-font-size,
            var(--ha-font-size-xs)
          );
          font-weight: var(
            --mdc-typography-caption-font-weight,
            var(--ha-font-weight-normal)
          );
          letter-spacing: var(
            --mdc-typography-caption-letter-spacing,
            0.0333333333em
          );
          text-decoration: var(
            --mdc-typography-caption-text-decoration,
            inherit
          );
          text-transform: var(--mdc-typography-caption-text-transform, inherit);
        }
        .attribution a {
          color: inherit;
        }
      `,
    ];
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "onboarding-location": OnboardingLocation;
  }
}
