class Point implements L.LatLngLiteral {
  lat: number = 0.00;
  lng: number = 0.00;
}

enum OverlayType {
  POINT = 0,
  CIRCLE = 1,
  POLYGON = 2,
}

interface Overlay {
  name: string;
  desc: string;
  points: Point[];
  options: any;
}

class OverlayData implements Overlay {
  name: string;
  desc: string;
  points: Point[];
  options: any;
  type: OverlayType;
  
  constructor(type: OverlayType, name: string, desc: string, points: Point[], options: any) {
    this.type = type;
    this.name = name;
    this.desc = desc;
    this.points = points;
    this.options = options;
  }
}

abstract class OverlayBase implements Overlay {
  name: string;
  desc: string;
  points: Point[];
  options: any;
  protected self: any;
  
  constructor(name: string, desc: string, points: Point[], options: any) {
    this.name = name;
    this.desc = desc;
    this.points = points;
    this.options = options;
  }
  
  add(map: L.Map): void {
    this.self.addTo(map);
  }
  
  remove(map: L.Map): void {
    this.self.removeFrom(map);
  }
}


class Marker extends OverlayBase {
  
  constructor(name: string, desc: string, point: Point, options: any) {
    super(name, desc, [ point ], options);
    this.self = L.marker(point);
    this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`);
  }
}

class Circle extends OverlayBase {

  constructor(name: string, desc: string, point: Point, options: any) {
    super(name, desc, [ point ], options);
    this.self = L.circle(point, options);
  }
}

class Polygon extends OverlayBase {

  constructor(name: string, desc: string, points: Point[], options: any) {
    super(name, desc, points, options);    
    this.self = L.polygon(points, options);
  }
}

class OverlayState {
  markers: Marker[];
  circles: Circle[];
  polygons: Polygon[];
  
  constructor() {
    this.markers = [];
    this.circles = [];
    this.polygons = [];
  }
  
  static load(): OverlayState {
    const store = localStorage.getItem("overlay_state");
    if (store) {  
      const model = JSON.parse(store);
      return {
        markers: model.markers.map((m: OverlayData) => OverlayState.fromData(m)),
        circles: model.circles.map((c: OverlayData) => OverlayState.fromData(c)),
        polygons: model.polygons.map((p: OverlayData) => OverlayState.fromData(p)),
      } as OverlayState
    } else {
      return new OverlayState();
    }
  }
  
  static save(overlayState: OverlayState): void {
    localStorage.setItem("overlay_state", JSON.stringify({
      markers: overlayState.markers.map((m: OverlayBase) => OverlayState.toData(m)),
      circles: overlayState.circles.map((c: OverlayBase) => OverlayState.toData(c)),
      polygons: overlayState.polygons.map((p: OverlayBase) => OverlayState.toData(p)),
    }));
  }
  
  static clear(overlayState: OverlayState, map: L.Map): OverlayState {
    overlayState.markers.forEach((m: Marker) => m.remove(map));
    overlayState.circles.forEach((c: Circle) => c.remove(map));
    overlayState.polygons.forEach((p: Polygon) => p.remove(map));
    
    return new OverlayState();
  }
  
  private static toData(source: OverlayBase): OverlayData {
    let type = OverlayType.POINT;
    if (source.points.length > 1) {
      type = OverlayType.POLYGON;
    } else if (source.options.radius) {
      type = OverlayType.CIRCLE;
    }
    return new OverlayData(type, source.name, source.desc, source.points, source.options);
  }
  
  private static fromData(data: OverlayData): OverlayBase {
    switch(data.type) {
      case OverlayType.POINT:
        return new Marker(data.name, data.desc, data.points[0], data.options);
      case OverlayType.CIRCLE:
        return new Circle(data.name, data.desc, data.points[0], data.options);
      case OverlayType.POLYGON:
        return new Polygon(data.name, data.desc, data.points, data.options);
    }
  }


}class TileLayerWrapper {
  self: L.TileLayer;
  name: string;
  visible: boolean = false;
  
  constructor(name: string, self: L.TileLayer) {
    this.self = self;
    this.name = name;
  }
  
  static constructLayer(name: string, self: L.TileLayer): TileLayerWrapper {
    const wrapper = new TileLayerWrapper(name, self);
    TileLayerWrapper.layers.push(wrapper);
    return wrapper;
  }
  
  static getActiveLayer(): TileLayerWrapper | null {
    for (const l of TileLayerWrapper.layers) {
      if (l.visible == true) {
        return l;
      }
    }
    return null;
  }
  static layers: TileLayerWrapper[] = new Array<TileLayerWrapper>();
  static enableOnly(self: TileLayerWrapper, map: L.Map): void {
    for (const l of TileLayerWrapper.layers) {
      if (l.visible) {
        l.self.removeFrom(map);
        l.visible = false;
      }
      if (l.name == self.name) {
        l.self.addTo(map);
        l.visible = true;
      }
    }
  }
}class Modal {

  constructor() {
    const _this = this;
    const closeBtn = document.getElementById("modalCloseBtn");
    if (closeBtn) {
      closeBtn.onclick = ()=>{_this.setVisible(false)};
    }
  }

  self(): HTMLElement | null {
    return document.getElementById("modal-container");
  }
  
  title(): HTMLElement | null{
    return document.getElementById("modal-title");
  }
  
  content(): HTMLElement | null {
    return document.getElementById("modal-content");
  }
  
  submitBtn(): HTMLElement | null {
    return document.getElementById("modal-submitBtn");
  }
  
  nameField(): string { 
    return (document.getElementById("modal-name") as HTMLInputElement)?.value ?? "";
  }
  
  descField(): string {
    return (document.getElementById("modal-desc") as HTMLInputElement)?.value ?? "";
  }

  visible(): boolean {
    return this.self()?.style.display != "none";
  }
  
  setVisible(v: boolean): void {
    const modal = this.self();
    if (modal) {
      modal.style.display = v ? "block" : "none";
    }
  }
  
  setState(state: OverlayType, args: any): void {
    const _this = this;
    switch (state) {
      case OverlayType.POINT:
        const title = this.title()
        if (title) {
          title.innerHTML = "Add Marker";
        }
        fetch("/static/pointModal.html")
          .then(r=>r.text())
          .then(t=> {
            const content = _this.content();
            if (content) { content.innerHTML = t; }
            const submitBtn = _this.submitBtn();
            if (submitBtn) {
              submitBtn.onclick = () => {
                const name = _this.nameField();
                const desc = _this.descField();
                const point = new Marker(name, desc, args.latlng, {title: name, alt: name});
                point.add(args.map);
                args.overlays.markers.push(point);
                _this.setVisible(false);
              }
            }
          });

        break;
      case OverlayType.CIRCLE:
        break;
      case OverlayType.POLYGON:
        break;
    }
  }
}class MapHandlers {

}
function init(): void {
  let overlays: OverlayState = OverlayState.load() ?? new OverlayState(); 
  const map = L.map('map').setView([35.6653, -105.9507], 13);

  const streetLayer = TileLayerWrapper.constructLayer(
    "streetLayer",
    L.tileLayer(
      'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
      {
        maxZoom: 19,
        attribution: '© OpenStreetMap'
      }));
      
  const satelliteLayer = TileLayerWrapper.constructLayer(
    "satelliteLayer",
    L.tileLayer(
      'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
      {
        maxZoom: 19,
        attribution: "&copy; Esri"
      }));
      
  TileLayerWrapper.enableOnly(streetLayer, map);
  
  overlays.markers.forEach(m=>m.add(map));
  overlays.circles.forEach(m=>m.add(map));
  overlays.polygons.forEach(m=>m.add(map));
  
  const modal = new Modal();
  
  const addMarkerHandler = (e: any) => {
    modal.setVisible(true);
    modal.setState(OverlayType.POINT, {
      latlng: e.latlng,
      map: map,
      overlays: overlays,
    });    
    map.off("click", addMarkerHandler);
  }
  
  const addMarkerBtn = document.getElementById("addPoint-btn");
  if (addMarkerBtn) {
    addMarkerBtn.onclick = (e: any)=>{
      try{
        map.off("click", addMarkerHandler);
      } finally {
        map.on("click", addMarkerHandler);
      }
    };
  }  
  
  const saveBtn = document.getElementById("save-btn");
  if (saveBtn) {
    saveBtn.onclick = (e: any) => {
      OverlayState.save(overlays);
    };
  }
  
  const clearBtn = document.getElementById("clear-btn");
  if (clearBtn) {
    clearBtn.onclick = (e: any) => {
      overlays = OverlayState.clear(overlays, map);
    }
  }
  
  const tilesBtn = document.getElementById("tiles-btn");
  if (tilesBtn) {
    tilesBtn.onclick = (e: any) => {
      if (TileLayerWrapper.getActiveLayer() == satelliteLayer) {
        TileLayerWrapper.enableOnly(streetLayer, map);
      } else {
        TileLayerWrapper.enableOnly(satelliteLayer, map);
      }
    };
  }
}

init();
