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

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

interface OverlayMap {
  [listName: string]: OverlayType;
}

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;
  }
  
  center(): Point {
    return this.points[0];
  }
  
  static centerAsString(pt: Point): string {
    let eastWest = "";
    let northSouth = "";
    const lat = pt.lat;
    const long = pt.lng;
    if (lat > 0) {
      northSouth = "N";
    } else if (lat < 0) {
      northSouth = "S";
    }
    if (long > 0) {
      eastWest = "E";
    } else if (long < 0) {
      eastWest = "W";
    }
    
    return `<span class="tiny">${String(long).substring(0, 7)}&deg;${eastWest}, ${String(lat).substring(0,7)}&deg;${northSouth}</span>`;
  }
  
  setPopupContent(content: string): void {
    this.self.bindPopup(content);
  }
  
  abstract add(map: L.Map): void;
  
  abstract remove(map: L.Map): void;
  
  abstract menuItem: Node | null;
  
  private static classSanitize(input: string): string {
    return input.replace(/\-/g, "_");
  }
  
  private static overlayTypeMap: OverlayMap = {
    markers_list: OverlayType.POINT,
    circles_list: OverlayType.CIRCLE,
    polygons_list: OverlayType.POLYGON,
  }
  
  static listAdd(self: OverlayBase, listName: string) {
    const list = document.getElementById(listName);
    if (list) {
      const li = document.createElement("li");
      const a = document.createElement("a");
      if (li && a) {
        a.innerText = self.name;
        a.href = "#";
        a.onclick = (e: any) => {
          MapHandler.editOverlay(self, OverlayBase.overlayTypeMap[OverlayBase.classSanitize(listName)]);
        };
        li.appendChild(a);
        list.appendChild(li);
        self.menuItem = li;
      }
    }
  }
  
  static listRemove(self: OverlayBase, listName: string) {
    const list = document.getElementById(listName);
    if (list && self.menuItem) {
      list.removeChild(self.menuItem);
    }
  }
}

class Marker extends OverlayBase {
  
  menuItem: Node | null = null;
  
  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>`);
  }
  
  add(map: L.Map) {
    this.self.addTo(map);
    OverlayBase.listAdd(this, "markers-list");
  }
  
  remove(map: L.Map) {
    this.self.removeFrom(map);
    OverlayBase.listRemove(this, "markers-list");
  }
}

class Circle extends OverlayBase {

  menuItem: Node | null = null;

  constructor(name: string, desc: string, point: Point, options: any) {
    super(name, desc, [ point ], options);
    this.self = L.circle(point, options);
    this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`);
  }
  
  add(map: L.Map) {
    this.self.addTo(map);
    OverlayBase.listAdd(this, "circles-list");
  }
  
  remove(map: L.Map) {
    this.self.removeFrom(map);
    OverlayBase.listRemove(this, "circles-list");
  }
}

class Polygon extends OverlayBase {

  menuItem: Node | null = null;

  constructor(name: string, desc: string, points: Point[], options: any) {
    super(name, desc, points, options);    
    this.self = L.polygon(points, options);
    this.self.bindPopup(`<h3>${name}</h3><p>${desc}</p>`);
  }
  
  center(): Point {
    return this.self.getCenter();
  }

  add(map: L.Map) {
    this.self.addTo(map);
    OverlayBase.listAdd(this, "polygons-list");
  }
  
  remove(map: L.Map) {
    this.self.removeFrom(map);
    OverlayBase.listRemove(this, "polygons-list");
  }
}

class Polyline extends OverlayBase {
  menuItem: Node | null = null;

  constructor() {
    super("", "", [ ], {});
    this.self = L.polyline([]);
  }
  
  insertPoint(pt: Point): void {
    this.self.addLatLng(pt);
    this.points.push(pt);
  }
  
  clearPoints(): void {
    this.points = [];
    this.self.setLatLngs([]);
  }
  
  numPoints(): number {
    return this.self.getLatLngs().length;
  }
  
  center(): Point {
    return this.self.getCenter();
  }
  
  add(map: L.Map) {
    this.self.addTo(map);
  }
  
  remove(map: L.Map) {
    this.self.removeFrom(map);
  }
}

class OverlayState {
  markers: Marker[];
  circles: Circle[];
  polygons: Polygon[];
  polyline: Polyline;
  
  constructor() {
    this.markers = [];
    this.circles = [];
    this.polygons = [];
    this.polyline = new Polyline();
  }
  
  static load(): OverlayState {
    const store = localStorage.getItem("overlay_state");
    return store ? OverlayState.import(store) : new OverlayState;
  }
  
  static import(overlayData: string): OverlayState {
    const model = JSON.parse(overlayData);
    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)),
      polyline: new Polyline(),
    } as OverlayState
  }
  
  static exportSingle(overlay: OverlayBase): string {
    return JSON.stringify(OverlayState.toData(overlay), null, 2);
  }
  
  static export(overlayState: OverlayState): string {
    return 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)),
    }, null, 2);
  }
  
  static save(overlayState: OverlayState): void {
    localStorage.setItem("overlay_state", OverlayState.export(overlayState));
  }
  
  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));
    
    const self = new OverlayState();
    self.polyline.add(map);
    return self;
  }
  
  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:
      default:
        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);
    }
  }
  
  static importWrapper(data: string, overlayState: OverlayState, map: L.Map): boolean {
    try {
      const singleData = <OverlayData>JSON.parse(data);
      const overlay = OverlayState.fromData(singleData);
      switch (singleData.type) {
        case OverlayType.POINT:
          overlayState.markers.push(overlay);
          break;
        case OverlayType.CIRCLE:
          overlayState.circles.push(overlay);
          break;
        case OverlayType.POLYGON:
          overlayState.polygons.push(overlay);
          break;
      }
      overlay.add(map);
      return true;
    } catch {}
    try {
      const self = OverlayState.import(data);
      self.markers.forEach((m: Marker) => {
        overlayState.markers.push(m);
        m.add(map);
      });
      self.circles.forEach((c: Circle) => {
        overlayState.circles.push(c);
        c.add(map);
      });
      self.polygons.forEach((p: Polygon) => {
        overlayState.polygons.push(p);
        p.add(map);
      });
      return true;
    } catch {}
    return false;
  }


}