import { DataFactory } from 'rdf-data-factory';
// L needs to be imported *before* leaflet-geoman-free
import L, { LatLng, Rectangle, PolylineOptions, Polygon, PM, TileLayer } from "leaflet";
import AddUserInputBtn from "../buttons/AddUserInputBtn";
import { AbstractWidget, ValueRepetition, WidgetValue } from "./AbstractWidget";
import {
  FilterPattern,
  FunctionCallExpression,
  LiteralTerm,
  Pattern,
} from "sparqljs";
import "leaflet/dist/leaflet.css";
import "@geoman-io/leaflet-geoman-free";
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
import { SelectedVal } from "../SelectedVal";
import { NamedNode } from '@rdfjs/types/data-model';
import { I18n } from '../../settings/I18n';
import HTMLComponent from '../HtmlComponent';
import CriteriaGroup from '../builder-section/groupwrapper/criteriagroup/CriteriaGroup';


const factory = new DataFactory();

const GEOFUNCTIONS_NAMESPACE = 'http://www.opengis.net/def/function/geosparql/'
export const GEOFUNCTIONS = {
  WITHIN: factory.namedNode(GEOFUNCTIONS_NAMESPACE + 'sfWithin') as NamedNode
}

const GEOSPARQL_NAMESPACE = "http://www.opengis.net/ont/geosparql#"
export const GEOSPARQL = {
  WKT_LITERAL: factory.namedNode(GEOSPARQL_NAMESPACE + 'wktLiteral') as NamedNode
}

export class MapValue implements WidgetValue {
  value: {
    label: string;
    type: string;
    coordinates: LatLng[][];
  };

  key():string {
    return this.value.coordinates.toString();
  }

  constructor(v:MapValue["value"]) {
    this.value = v;
  }

  exportToGeoJson():object {
    // TODO : recreate a GeoJSON value from the MapWidgetValue
    return {};
  }
}

// converts props of type Date to type string
type ObjectifyLatLng<T> = T extends LatLng[][]
  ? [[{lat:number,lng:number}]]
  : T extends object
  ? {
      [k in keyof T]: ObjectifyLatLng<T[k]>;
    }
  : T;

// stringified type of MapWidgetValue
// see: https://effectivetypescript.com/2020/04/09/jsonify/
type ObjectMapWidgetValue = ObjectifyLatLng<MapValue>


export interface MapConfiguration {
  zoom: number,
  center: {
    lat: number,
    long: number
  }
}

export interface CustomControlOptions {
  name: string,
  block: any,
  title: string,
  className: string,
  onClick: () => void
}

export default class MapWidget extends AbstractWidget {
  
  // The default implementation of MapConfiguration
  static defaultConfiguration: MapConfiguration = {
    zoom:5,
    center: {
      lat: 46.20222,
      long: 6.14569
    }
  }
  
  protected configuration: MapConfiguration;
  protected endClassWidgetGroup: any;
  protected widgetValues: MapValue[];
  //protected widgetValue: MapWidgetValue[];
  // protected blockObjectPropTriple: boolean = true
  renderMapValueBtn: AddUserInputBtn;
  map: L.Map;
  drawingLayer: L.Layer;
  constructor(
    configuration: MapConfiguration,
    parentComponent: HTMLComponent,
    startClassVal: SelectedVal,
    objectPropVal: SelectedVal,
    endClassVal: SelectedVal
  ) {
    super(
      "map-widget",
      parentComponent,
      null,
      startClassVal,
      objectPropVal,
      endClassVal,
      ValueRepetition.SINGLE
    );

    this.configuration = configuration;
    this.parentComponent = parentComponent;
    this.endClassWidgetGroup = (this.parentComponent.parentComponent.parentComponent.parentComponent as CriteriaGroup).endClassWidgetGroup ;
  }

  render(): this {
    console.log("rendering map widget...")
    console.log(this) ;
    super.render();
    this.renderMapValueBtn = new AddUserInputBtn(
      this,
      I18n.labels.MapWidgetOpenMap,
      this.#renderMap
    ).render();

    // opens the map if there is a value - in the case we are editing the value
    if(this.widgetValues?.length > 0 ) {
      this.#renderMap();
    }
    
    return this;
  }

  #redrawSelection = () => {
    
    if ((this.widgetValues !== undefined) && (this.widgetValues.length > 0)) {
      let options = {
        color: "#3388ff",
        weight: 3,
        opacity: 1,
        lineCap: 'round',
        lineJoin: "round" ,
        fillColor: "#3388ff",
        fillOpacity: 0.2,
        fillRule: "evenodd"
      }

      switch ((this.widgetValues[0].value.type as string)) {
        case 'Rectangle':
            console.log(this.widgetValues[0].value.coordinates) ;
            let bounds = L.latLngBounds(this.widgetValues[0].value.coordinates[0][0],this.widgetValues[0].value.coordinates[0][2]) ;
            L.rectangle(bounds, (options as PolylineOptions)).addTo(this.map);
          break;    
        default: 
          console.log(this.widgetValues[0].value.coordinates) ;
          let coordinates = this.widgetValues[0].value.coordinates[0][0] ;
          L.polygon(this.widgetValues[0].value.coordinates[0], (options as PolylineOptions)).addTo(this.map);
        break;
      }
    }
  }

  #renderMap = () => {    

    let d = new Date();
    let elementId = d.valueOf();

    this.html.append($('<div id="map-'+elementId+'" class="map-wrapper"></div>'));

    this.map = L.map("map-"+elementId);    
    let tl:TileLayer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19,
      attribution: "© OpenStreetMap",
    });    
    tl.addTo(this.map);

    // attempt to rebind geoman - does not work at the moment
    // see https://geoman.io/docs/lazy-loading
    // L.PM.reInitLayer(tl);

    this.map.setView([this.configuration.center.lat, this.configuration.center.long], this.configuration.zoom);

    this.map.pm.addControls({
      position: "topleft",
      cutPolygon: false,
      drawCircle: false,
      drawPolyline: false,
      drawCircleMarker: false,
      drawMarker: false,
      drawText: false,
      drawPolygon: true,
      editMode: true,
      dragMode: true,
      rotateMode: false,
      removalMode: true
    });

    this.#redrawSelection() ;

    if ((this.widgetValues !== undefined) && (this.widgetValues.length > 0)) {
      
      var layers = [];
      //layers = L.PM.Utils.findLayers(this.map)
      //console.log(layers) ;
      //this.drawingLayer = layers[0] as Layer;
      this.map.pm.getGeomanLayers(false).forEach(layer => {
        if(layer instanceof L.Polygon) {
            layer.pm.enable() ;
            this.drawingLayer = layer ;
        }
      });
      this.drawingLayer.on("pm:edit", (e) => {
        
      console.log('fireing pm:create')
        console.log(e);
        
        //let widgetValue = this.#setWidgetValue(e.layer) ;
        this.drawingLayer = e.layer;

      });
      this.drawingLayer.on("pm:update", (e) => {
        
      console.log('fireing pm:update')
        console.log(e);
        
        //let widgetValue = this.#setWidgetValue(e.layer) ;
        this.drawingLayer = e.layer;

      });
    }
    
    let submitMapOptions: CustomControlOptions = {
      name: "submitMap",
      block: "custom",
      title: I18n.labels.MapWidgetValidate,
      className: "submitMap icon-map-validate",
      onClick: () => {
        //this.widgetValue = [this.widgetValue]
        this.#setWidgetValue(this.drawingLayer) ;
        this.triggerRenderWidgetVal(this.widgetValues);
        $(this.parentComponent).trigger("change");
        console.log(this) ;
        console.log(this.endClassWidgetGroup) ;
        //this.html.hide() ;
        //this.endClassWidgetGroup
      },
    }
    this.map.pm.Toolbar.createCustomControl(submitMapOptions);

    this.map.on("pm:create", (e:any) => {

      console.log('fireing pm:create')
      //If there is already a drawing, then delete it
      // allows only for one drawing at a time
      if (this.drawingLayer) this.map.removeLayer(this.drawingLayer);

      this.drawingLayer = e.layer;

      this.map.addLayer(this.drawingLayer);

      //let widgetValue = this.#setWidgetValue(e.layer) ;

      console.log(this.endClassWidgetGroup) ;

      //this.endClassWidgetGroup.html[0].addEventListener("click", (evt:MouseEvent) => this.showWidgetMap(evt)) ;

      //this.renderWidgetVal(widgetValue);
      //add listener when the shape gets changed
        this.drawingLayer.on("pm:edit", (e) => {
          
        console.log('fireing pm:create pm:edit');
        this.drawingLayer = e.layer;
          //let widgetValue = this.#setWidgetValue(e.layer) ;
          //this.#setWidgetValue(e.layer) ;
          //this.renderWidgetVal(widgetValue);
      });
    });

    
    this.map.on("pm:update", (e) => {
      
      console.log('fireing pm:update');
      this.drawingLayer = e.layer;
      //let widgetValue = this.#setWidgetValue(e.layer) ;
      //this.#setWidgetValue(e.layer) ;
      //this.renderWidgetVal(widgetValue);
      //this.endClassWidgetGroup.html[0].addEventListener("click", (evt:MouseEvent) => this.showWidgetMap(evt)) ;
    });

    /*this.map.on("pm:drawend", (e) => {
      console.log(e);
      this.drawingLayer = e.layer;
    });*/

    this.#changeButton();
  };

  /*showWidgetMap(this: HTMLElement, ev: Event) {
    console.log(ev) ;

  }*/

  /*private showWidgetMap(e: MouseEvent): void {
    let objectVal = this.endClassWidgetGroup.ParentComponent.ObjectPropertyGroup.objectPropVal ;
    this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents = false ;
    this.endClassWidgetGroup.ParentComponent.EndClassGroup.onObjectPropertyGroupSelected(objectVal) ;
    this.endClassWidgetGroup.ParentComponent.endClassWidgetGroup.render() ;
    console.log(this) ;
    let _this = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent ;
    //this.html = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent.html ;
    //this.parentComponent = this.endClassWidgetGroup.ParentComponent.EndClassGroup.editComponents.widgetWrapper.widgetComponent.parentComponent ;
    //this.endClassWidgetGroup = this.parentComponent.ParentComponent.ParentComponent.ParentComponent.endClassWidgetGroup ;
    _this.widgetValue = this.widgetValue ;
    _this.#renderMap(true) ;
    //this.render() ;

    console.info(`After click event, 'this' refers to canvas and not to the instance of Foo:`);
    console.info(this);
    console.info(_this);
    console.warn(`Message is: "${_this.widgetValue}"`); // error
  }*/

  #getValueLabel(layer: any) {
    let area = this.#polygonArea((layer as any).getLatLngs() as LatLng[][]) ; 
    let coordinates = (layer as any).getLatLngs() as LatLng[][] ;
    return this.#getSvgSelection(coordinates) + '<span>' + area +' km²</span>' ;
  }

  #setWidgetValue = (layer:any) => {
    this.widgetValues = [] ;

    switch ((layer as any).pm._shape) {
      case 'Rectangle':
        this.widgetValues.push(new MapValue({
          label: this.#getValueLabel(layer as Rectangle),
          type: 'Rectangle',
          coordinates: (layer as Rectangle).getLatLngs() as LatLng[][],
        }))
        break;    
      default: 
        this.widgetValues.push(new MapValue({
          label: this.#getValueLabel(layer as Polygon),
          type: 'Polygon',
          coordinates: (layer as Polygon).getLatLngs() as LatLng[][],
        }));
      break;
    }

    return this.widgetValues ;
  }

  #closeMap = () => {
    if(this.widgetValues?.length > 0 ) {
      this.triggerRenderWidgetVal(this.widgetValues);
      $(this.parentComponent).trigger("change");
    }
  };

  parseInput(input:ObjectMapWidgetValue["value"]): MapValue {

    const parsedCoords = input.coordinates.map((c)=>{
      return c.map((latlng)=>{
        if(!("lat" in latlng) || !('lng' in LatLng) || isNaN(latlng.lat) || isNaN(latlng.lng))
        return new LatLng(latlng.lat,latlng.lng)
      })
    })
    if(parsedCoords.length === 0) throw Error(`Parsing of ${input.coordinates} failed`)
    return new MapValue({
        label: input.label,
        coordinates: parsedCoords,
        type: input.type
      }
    );
  }

  #changeButton() {
    this.renderMapValueBtn.html.remove();
    this.renderMapValueBtn = new AddUserInputBtn(
      this,
      I18n.labels.MapWidgetCloseMap,
      this.#closeMap
    ).render();
  }

  #getSvgSelection(coordinates: LatLng[][]) {
    
    let bounds = L.latLngBounds(coordinates[0]) ;

    let startLeft = (bounds.getWest() as number);
    let startBottom = (bounds.getSouth() as number);
    let width = ((bounds.getEast() as number) -  (bounds.getWest() as number));
    let height = ((bounds.getNorth() as number) -  (bounds.getSouth() as number));

    let svgCoordinates = '';
    let lat = 0;
    let lon = 0;
    coordinates[0].forEach((point:any) => {
      console.log(point) ;
      lat = point.lat - startBottom ;
      lon = point.lng - startLeft ;
      if(!(svgCoordinates == '')) {
        svgCoordinates += ' ';
      }

      svgCoordinates += lon+','+lat ;
    });

    let svg = `<svg id="svgelem" width="30" height="30" viewBox="0 0 `+width+` `+height+`" xmlns="http://www.w3.org/2000/svg" style=" transform: rotateX(180deg);" preserveAspectRatio="xMidYMid meet">   <g><polygon points="`+svgCoordinates+`" style="fill:#ffffff;" /></g></svg>` ;
    return svg ;
  }


  #polygonArea(coords: any) {
    let total = 0;
    let arrayCoords = new Array()  ;
    arrayCoords[0] = new Array()  ;
    let index = 0 ;
    coords[0].forEach((point:any) => {
      let newSet = [point.lat, point.lng];
      arrayCoords[0][index] = newSet ; 

      index++;
    });

    if (arrayCoords && arrayCoords.length > 0) {
      total += Math.abs(this.#ringArea(arrayCoords[0]));
      for (let i = 1; i < arrayCoords.length; i++) {
        total -= Math.abs(this.#ringArea(arrayCoords[i]));
      }
      
    }
    return Math.round(total) ;
  }

  #ringArea(coords: number[][]): number {
    const coordsLength = coords.length;
    const earthRadius = 6371008.8/1000; //km²
    const FACTOR = (earthRadius * earthRadius) / 2;
    const PI_OVER_180 = Math.PI / 180;
  
    if (coordsLength <= 2) return 0;
    let total = 0;
  
    let i = 0;
    while (i < coordsLength) {
      const lower = coords[i];
      const middle = coords[i + 1 === coordsLength ? 0 : i + 1];
      const upper =
        coords[i + 2 >= coordsLength ? (i + 2) % coordsLength : i + 2];
  
      const lowerX = lower[0] * PI_OVER_180;
      const middleY = middle[1] * PI_OVER_180;
      const upperX = upper[0] * PI_OVER_180;
  
      total += (upperX - lowerX) * Math.sin(middleY);
  
      i++;
    }
    return total * FACTOR;
  }
}
