import React, { Component } from 'react';
import { PanelProps, createTheme, getValueFormat, DataFrameView, sortDataFrame, getTimeField } from '@grafana/data';
import { MapOptions } from './types';
import { sanitizeTopology } from './components/lib/topologyTools';
import './components/MapCanvas.component.js';
import { PubSub } from './components/lib/pubsub.js';
import { locationService } from '@grafana/runtime';
import { setPath } from "./components/lib/utils.js"
import { signals } from "./signals.js"
import * as constants from "./constants.js"

export interface MapPanelProps extends PanelProps<MapOptions> {
  fieldConfig: any;
  options: MapOptions;
}

export function toDataFrames(data){
  let dataFrames = [] as any[];

  data.series.forEach(function (series) {
    // get frame and ensure it's sorted by timestamp
    const { timeIndex } = getTimeField(series);
    const sortedFrame = sortDataFrame(series, timeIndex, true);
    let frame = new DataFrameView(sortedFrame);
    dataFrames.push(frame);
  });

  return dataFrames;
}

export class MapPanel extends Component<MapPanelProps> {
  mapCanvas: any;
  lastOptions: any;
  lastTopology: any;
  theme: any;
  mapjsonCache: any;
  subscriptionHandle: any;
  variableChangeHandle: any;
  _configurationUrl: any;
  updateListener: any;

  constructor(props: MapPanelProps) {
    super(props);
    this.mapCanvas = React.createRef();
    this.mapjsonCache = {
      L1: {},
      L2: {},
      L3: {},
    };
    this.lastOptions = {...this.props.options};
    this.theme = createTheme();
    PubSub.global.subscribe(signals.RETURN_MAP_CENTER_AND_ZOOM, this.updateCenter);
    PubSub.global.subscribe(signals.RETURN_VIEWPORT, this.updateMapViewport);
  }

  setDashboardVariables() {
    let self = this;
    return function (event) {
      let setLocation = {};
      for (let i = 0; i < (self?.props?.options?.layerLimit || constants.DEFAULT_LAYER_LIMIT); i++) {
        let layer = self.props.options.layers[i];
        const keys = [
          'dashboardEdgeSrcVar',
          'dashboardEdgeDstVar',
          'dashboardNodeVar'
        ];
        keys.forEach((k)=>{
          if(layer?.[k]){
            setLocation[`var-${layer[k]}`] = null;
          }
        })
      }
      if (event && event.type === 'edge' && event.selection?.nodeA && event.selection?.nodeZ) {
        const srcVariable = 'var-' + self.props.options.layers[event.layer]['dashboardEdgeSrcVar'];
        setLocation[srcVariable] = event.selection.nodeA;
        const dstVariable = 'var-' + self.props.options.layers[event.layer]['dashboardEdgeDstVar'];
        setLocation[dstVariable] = event.selection.nodeZ;
      }
      if (event && event.type === 'node') {
        const dashboardVariable = 'var-' + self.props.options.layers[event.layer]['dashboardNodeVar'];
        setLocation[dashboardVariable] = event.selection.name;
      }
      locationService.partial(setLocation, false);
    }
  }

  updateCenter = (centerData) => {
    let viewport = {
      "zoom": centerData.zoom,
      "center": {
        "lat": centerData.center.lat,
        "lng": centerData.center.lng,
      }
    }
    this.props.onOptionsChange({ ...this.props.options, viewport });
  };

  updateMapViewport = (viewportData) => {
    const coordinates = viewportData.coordinates;
    this.props.onOptionsChange({
      ...this.props.options,
      "viewport": {
        "top": coordinates.getNorth().toFixed(2),
        "left": coordinates.getWest().toFixed(2),
        "bottom": coordinates.getSouth().toFixed(2),
        "right": coordinates.getEast().toFixed(2),
      }
    });
  };

  // A function to update the map jsons in the Edit panel based on the current map state
  // Used in esmap.js
  updateTopologyEditor = (mapData) => {
    const { options } = this.props;
    let update = { ...options }
    if(!update.layers){
      update.layers = [];
    }
    let layerUpdates = [
        options.layers[0].mapjson,
        options.layers[1].mapjson,
        options.layers[2].mapjson,
    ];
    for(let i=0; i < (this.props?.options?.layerLimit || constants.DEFAULT_LAYER_LIMIT); i++){
      if (mapData[i] != null) {
        layerUpdates[i] = JSON.stringify(sanitizeTopology(mapData[i]));
      }
      if(update.layers[i]){
        update.layers[i].mapjson = layerUpdates[i];
      }
    }
    this.props.onOptionsChange(update);
  };
  // A function to turn layers on or off. Takes in the layer and boolean value
  // Used in SideBar.tsx
  toggleLayer = (layer, value) => {
    let update = { ...this.props.options };
    update.layers[layer].visible = value;
    this.props.onOptionsChange(update);
  };

  resolveLatLngFromVars(options, data, replaceVariables) {
    // set a sensible default output: 0,0 in case we can't resolve.
    let output = {
      resolvedLat: 0,
      resolvedLng: 0,
    };
    if (this.props.options.initialViewStrategy === 'variables') {
      let frames: any[];
      frames = toDataFrames(data);
      const toResolve = {
        latitudeVar: 'resolvedLat',
        longitudeVar: 'resolvedLng',
      };

      Object.keys(toResolve).forEach((variableName) => {
        const resolvedName = toResolve[variableName];
        let fieldName = options[variableName];
        // if the latitudeVar contains the string "__data.fields"
        if (options[variableName].indexOf('__data.fields') >= 0) {
          // we're trying to get the field name from the interior of the string,
          // surrounded by quotation marks
          fieldName = this.props.options[variableName].split('"')[1];
        }
        // this block attempts to resolve "template level" i.e. dashboard vars
        let candidateVal = parseFloat(replaceVariables(fieldName));
        if (!isNaN(candidateVal)) {
          output[resolvedName] = candidateVal;
        } else {
          // in the case that we can't resolve the dashboard var,
          // loop through the dataframeview searching for our field,
          // and once the field is found, loop through until we find the
          // first value. Then set the var and return
          frames.forEach((frame) => {
            frame.forEach((row) => {
              if (!!row[fieldName]) {
                output[resolvedName] = row[fieldName];
              }
            });
          });
        }
      });
    }
    return output;
  }

  resolveNodeThresholds(options){
    let thresholds: any[] = [];
    for (let layer=0; layer < (this.props?.options?.layerLimit || constants.DEFAULT_LAYER_LIMIT); layer++) {
      if (!options.layers[layer]) {
        continue;
      }
      let layerThresholds: any[] = [];
      if (Array.isArray(options.layers[layer]?.nodeThresholds?.steps)) {
        options.layers[layer]?.nodeThresholds?.steps.forEach((step) => {
          layerThresholds.push({
            color: this.theme.visualization.getColorByName(step.color),
            value: step.value || 0,
          });
        });
      }
      thresholds.push(layerThresholds);
    }
    // snapshot the current options. If they're not the same as the last options, update them.
    let currOptions = JSON.parse(JSON.stringify(options));

    if (Array.isArray(thresholds)) {
      thresholds.forEach((layerThresholds, layerIdx)=>{
        const currLayerNodeThresholds = options?.layers?.[layerIdx].nodeThresholds;
        if(JSON.stringify(layerThresholds) !== JSON.stringify(currLayerNodeThresholds)){
          setPath(currOptions, `layers[${layerIdx}].nodeThresholds`, layerThresholds);
        }
      });
    }    return currOptions;
  }

  updateMap(forceRefresh?) {
    const { options, data, replaceVariables, fieldConfig } = this.props;

    const latLng = this.resolveLatLngFromVars(options, data, replaceVariables);

    if (fieldConfig.defaults?.unit) {
      this.mapCanvas.current.valueFormat = getValueFormat(fieldConfig.defaults?.unit);
    }

    this.mapCanvas.current.setAttribute('startlat', latLng['resolvedLat']);
    this.mapCanvas.current.setAttribute('startlng', latLng['resolvedLng']);
    if(data.state === "Done"){
      let frames = toDataFrames(data);
      let output = [] as any[];
      frames.forEach((frame)=>{
        frame.forEach((row) => {
          output.push(JSON.parse(JSON.stringify(row)));
        })
      })
      this.mapCanvas.current.data = output
      this.mapCanvas.current.autodetectTopology();
    }


    let topology = [
      { nodes: [], edges: [] },
      { nodes: [], edges: [] },
      { nodes: [], edges: [] },
    ]
    let topologyData = [
      "",
      "",
      "",
    ]

    for(let layer=0; layer < (this.props?.options?.layerLimit || constants.DEFAULT_LAYER_LIMIT); layer++){

      const memoryTopology = JSON.stringify(this.mapCanvas?.current?.topology?.[layer] || null)
      const editorTopology = options.layers?.[layer]?.mapjson;
      // if we have an existing in-memory copy, use it
      if (!forceRefresh && JSON.parse(memoryTopology) && editorTopology === memoryTopology){
        topologyData[layer] = memoryTopology;
      } else if (editorTopology !== memoryTopology) {
        topologyData[layer] = editorTopology as string;
      }

      // here, we parse the topology data (as strings in topologyData)
      // In the case that we have an error, we pass the topologyData along to
      // MapCanvas to figure out what went wrong.
      // @ts-ignore

      try {
        topology[layer] = JSON.parse(topologyData[layer]);
      } catch(e) {
        // ensure that the mapcanvas has jsonResults set up so it can collect errors.
        if(!this.mapCanvas.current.jsonResults){
          this.mapCanvas.current.jsonResults = [];
        }
        // @ts-ignore
        topology[layer] = topologyData[layer];
      }
    }

    // snapshot the current topology. If it's not the same as the last topology, update it.
    let currTopology = JSON.stringify(topology);
    if(currTopology !== this.lastTopology){
      this.mapCanvas.current.setTopology(topology);
      this.lastTopology = currTopology;
    }

    let dataFrames = toDataFrames(data);
    let trafficData = [] as any[];
    let formatter = null as any;
    dataFrames.forEach((frame)=>{
      frame.data.fields.forEach((field)=>{
        // all numerical fields use the same formatter.
        // find a numerical field and set up the formatter function here.
        if(field.type === "number"){
          let display = field.display;
          formatter = (value)=>{
            const formatted = display(value);
            return `${formatted.text}${formatted.suffix}`;
          }
        }
      })
      frame.forEach((row)=>{
        trafficData.push({...row});
      })
    });

    this.mapCanvas.current.setTrafficFormat(formatter);
    this.mapCanvas.current.setTraffic(trafficData);
  }
  componentDidMount() {
    let { eventBus, options, replaceVariables, fieldConfig } = this.props;
    options = JSON.parse(JSON.stringify(options));

    let thresholds: any[];
    thresholds = [];
    fieldConfig.defaults?.thresholds?.steps?.forEach((threshold) => {
      thresholds.push({
        color: this.theme.visualization.getColorByName(threshold.color),
        value: threshold.value,
      });
    });
    options.thresholds = thresholds;
    options = this.resolveNodeThresholds(options);
    options.zIndexBase = 200;
    this._configurationUrl = options.configurationUrl;
    options.configurationUrl = replaceVariables(options.configurationUrl);

    this.lastOptions.configurationUrl = options.configurationUrl;

    this.mapCanvas.current.setOptions(options);

    this.mapCanvas.current.listen(signals.TOPOLOGY_UPDATED,() => {
        this.updateTopologyEditor(this.mapCanvas.current['topology']);
    });

    this.mapCanvas.current.listen(signals.SELECTION_SET, this.setDashboardVariables() )
    this.mapCanvas.current.listen(signals.SELECTION_CLEARED, this.setDashboardVariables() )

    this.updateMap();
    // @ts-ignore
    this.subscriptionHandle = eventBus.getStream({type: "panel-edit-finished"}).subscribe((e)=>{
        if(this.mapCanvas.current){
          // do this async to ensure that the UI has time to update after conclusion of edit.
          setTimeout(()=>{
            if(options.topologySource !== "url"){
              this.updateMap(true);
            }
          }, 10);
        }
    })
    // @ts-ignore
    this.variableChangeHandle = eventBus.getStream({type: "refresh"}).subscribe((e)=>{
        if(this.mapCanvas.current){
          setTimeout(()=>{
              this.componentDidUpdate();
          }, 10);
        }
    })
  }

  componentWillUnmount() {
    if(this.subscriptionHandle){
      this.subscriptionHandle.unsubscribe();
    }
    if(this.variableChangeHandle){
      this.variableChangeHandle.unsubscribe();
    }
  }

  componentDidUpdate() {
    let { options, fieldConfig, replaceVariables } = this.props;

    options = JSON.parse(JSON.stringify(options));
    options.configurationUrl = replaceVariables(this._configurationUrl);

    let thresholds: any[];
    thresholds = [];
    fieldConfig.defaults?.thresholds?.steps?.forEach((threshold) => {
      thresholds.push({
        color: this.theme.visualization.getColorByName(threshold.color),
        value: threshold.value,
      });
    });
    options.thresholds = thresholds;
    options = this.resolveNodeThresholds(options);
    options.zIndexBase = 200;

    let changed = this.mapCanvas.current.calculateOptionsChanges(options);

    if(changed.indexOf("topologySource") >= 0){
      if(options.topologySource !== "url"){
        this.mapCanvas.current.setOptions(JSON.parse(JSON.stringify(options)));
        this.mapCanvas.current.refresh();
      }
      if(options.topologySource === "json"){
        this.mapCanvas.current.pubsub.clearTopicCallbacks(signals.TOPOLOGY_UPDATED);
        this.mapCanvas.current.setTopology(JSON.parse(this.lastTopology));
        this.mapCanvas.current.setOptions(options, true);
        this.mapCanvas.current.listen(signals.TOPOLOGY_UPDATED,() => {
          this.updateTopologyEditor(this.mapCanvas.current['topology']);
        });
      }
    }

    this.updateMap();

    if (changed.length > 0) {
      this.mapCanvas.current.setOptions(options);
    }
  }

  render() {
    let { options, width, height, data, replaceVariables } = this.props;

    const output = this.resolveLatLngFromVars(options, data, replaceVariables);
    const elem = React.createElement('esnet-map-canvas', {
      width: width,
      height: height,
      startlat: output['resolvedLat'],
      startlng: output['resolvedLng'],
      ref: this.mapCanvas,
    });
    return elem;
  }
}
