import {
  Circle,
  DivIcon,
  DragEndEvent,
  LatLng,
  Marker,
  MarkerOptions,
} from "leaflet";
import {
  css,
  CSSResultGroup,
  html,
  LitElement,
  PropertyValues,
  TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import type { HomeAssistant } from "../../types";
import "./ha-map";
import type { HaMap } from "./ha-map";

declare global {
  // for fire event
  interface HASSDomEvents {
    "location-updated": { id: string; location: [number, number] };
    "markers-updated": undefined;
    "radius-updated": { id: string; radius: number };
    "marker-clicked": { id: string };
  }
}

export interface MarkerLocation {
  latitude: number;
  longitude: number;
  radius?: number;
  name?: string;
  id: string;
  icon?: string;
  radius_color?: string;
  location_editable?: boolean;
  radius_editable?: boolean;
}

@customElement("ha-locations-editor")
export class HaLocationsEditor extends LitElement {
  @property({ attribute: false }) public hass!: HomeAssistant;

  @property({ attribute: false }) public locations?: MarkerLocation[];

  @property({ type: Boolean }) public autoFit = false;

  @property({ type: Number }) public zoom = 16;

  @property({ type: Boolean }) public darkMode?: boolean;

  @state() private _locationMarkers?: Record<string, Marker | Circle>;

  @state() private _circles: Record<string, Circle> = {};

  @query("ha-map", true) private map!: HaMap;

  private Leaflet?: LeafletModuleType;

  constructor() {
    super();

    import("leaflet").then((module) => {
      import("leaflet-draw").then(() => {
        this.Leaflet = module.default as LeafletModuleType;
        this._updateMarkers();
        this.updateComplete.then(() => this.fitMap());
      });
    });
  }

  public fitMap(): void {
    this.map.fitMap();
  }

  public fitMarker(id: string): void {
    if (!this.map.leafletMap || !this._locationMarkers) {
      return;
    }
    const marker = this._locationMarkers[id];
    if (!marker) {
      return;
    }
    if ("getBounds" in marker) {
      this.map.leafletMap.fitBounds(marker.getBounds());
      (marker as Circle).bringToFront();
    } else {
      const circle = this._circles[id];
      if (circle) {
        this.map.leafletMap.fitBounds(circle.getBounds());
      } else {
        this.map.leafletMap.setView(marker.getLatLng(), this.zoom);
      }
    }
  }

  protected render(): TemplateResult {
    return html`<ha-map
      .hass=${this.hass}
      .layers=${this._getLayers(this._circles, this._locationMarkers)}
      .zoom=${this.zoom}
      .autoFit=${this.autoFit}
      .darkMode=${this.darkMode}
    ></ha-map>`;
  }

  private _getLayers = memoizeOne(
    (
      circles: Record<string, Circle>,
      markers?: Record<string, Marker | Circle>
    ): Array<Marker | Circle> => {
      const layers: Array<Marker | Circle> = [];
      Array.prototype.push.apply(layers, Object.values(circles));
      if (markers) {
        Array.prototype.push.apply(layers, Object.values(markers));
      }
      return layers;
    }
  );

  public willUpdate(changedProps: PropertyValues): void {
    super.willUpdate(changedProps);

    // Still loading.
    if (!this.Leaflet) {
      return;
    }

    if (changedProps.has("locations")) {
      this._updateMarkers();
    }
  }

  private _updateLocation(ev: DragEndEvent) {
    const marker = ev.target;
    const latlng: LatLng = marker.getLatLng();
    let longitude: number = latlng.lng;
    if (Math.abs(longitude) > 180.0) {
      // Normalize longitude if map provides values beyond -180 to +180 degrees.
      longitude = (((longitude % 360.0) + 540.0) % 360.0) - 180.0;
    }
    const location: [number, number] = [latlng.lat, longitude];
    fireEvent(
      this,
      "location-updated",
      { id: marker.id, location },
      { bubbles: false }
    );
  }

  private _updateRadius(ev: DragEndEvent) {
    const marker = ev.target;
    const circle = this._locationMarkers![marker.id] as Circle;
    fireEvent(
      this,
      "radius-updated",
      { id: marker.id, radius: circle.getRadius() },
      { bubbles: false }
    );
  }

  private _markerClicked(ev: DragEndEvent) {
    const marker = ev.target;
    fireEvent(this, "marker-clicked", { id: marker.id }, { bubbles: false });
  }

  private _updateMarkers(): void {
    if (!this.locations || !this.locations.length) {
      this._circles = {};
      this._locationMarkers = undefined;
      return;
    }

    const locationMarkers = {};
    const circles = {};

    const defaultZoneRadiusColor =
      getComputedStyle(this).getPropertyValue("--accent-color");

    this.locations.forEach((location: MarkerLocation) => {
      let icon: DivIcon | undefined;
      if (location.icon) {
        // create icon
        const el = document.createElement("div");
        el.className = "named-icon";
        if (location.name) {
          el.innerText = location.name;
        }
        const iconEl = document.createElement("ha-icon");
        iconEl.setAttribute("icon", location.icon);
        el.prepend(iconEl);

        icon = this.Leaflet!.divIcon({
          html: el.outerHTML,
          iconSize: [24, 24],
          className: "light",
        });
      }
      if (location.radius) {
        const circle = this.Leaflet!.circle(
          [location.latitude, location.longitude],
          {
            color: location.radius_color || defaultZoneRadiusColor,
            radius: location.radius,
          }
        );
        if (location.radius_editable || location.location_editable) {
          // @ts-ignore
          circle.editing.enable();
          circle.addEventListener("add", () => {
            // @ts-ignore
            const moveMarker = circle.editing._moveMarker;
            // @ts-ignore
            const resizeMarker = circle.editing._resizeMarkers[0];
            if (icon) {
              moveMarker.setIcon(icon);
            }
            resizeMarker.id = moveMarker.id = location.id;
            moveMarker
              .addEventListener(
                "dragend",
                // @ts-ignore
                (ev: DragEndEvent) => this._updateLocation(ev)
              )
              .addEventListener(
                "click",
                // @ts-ignore
                (ev: MouseEvent) => this._markerClicked(ev)
              );
            if (location.radius_editable) {
              resizeMarker.addEventListener(
                "dragend",
                // @ts-ignore
                (ev: DragEndEvent) => this._updateRadius(ev)
              );
            } else {
              resizeMarker.remove();
            }
          });
          locationMarkers[location.id] = circle;
        } else {
          circles[location.id] = circle;
        }
      }
      if (
        !location.radius ||
        (!location.radius_editable && !location.location_editable)
      ) {
        const options: MarkerOptions = {
          title: location.name,
          draggable: location.location_editable,
        };

        if (icon) {
          options.icon = icon;
        }

        const marker = this.Leaflet!.marker(
          [location.latitude, location.longitude],
          options
        )
          .addEventListener("dragend", (ev: DragEndEvent) =>
            this._updateLocation(ev)
          )
          .addEventListener(
            // @ts-ignore
            "click",
            // @ts-ignore
            (ev: MouseEvent) => this._markerClicked(ev)
          );
        (marker as any).id = location.id;

        locationMarkers[location.id] = marker;
      }
    });
    this._circles = circles;
    this._locationMarkers = locationMarkers;
    fireEvent(this, "markers-updated");
  }

  static get styles(): CSSResultGroup {
    return css`
      :host {
        display: block;
        height: 300px;
      }
      ha-map {
        height: 100%;
      }
    `;
  }
}

declare global {
  interface HTMLElementTagNameMap {
    "ha-locations-editor": HaLocationsEditor;
  }
}
