import React from 'react';
import Deck from "deck.gl"
import { StaticMap } from "react-map-gl";
import {JSONConverter} from '@deck.gl/json';
import {CSVLoader} from "@loaders.gl/csv";
import {registerLoaders} from "@loaders.gl/core";
import * as core from '@deck.gl/core';
import * as layers from "@deck.gl/layers";
import * as aggregationLayers from "@deck.gl/aggregation-layers";
import * as geoLayers from "@deck.gl/geo-layers";
import * as meshLayers from "@deck.gl/mesh-layers";
import PropTypes from 'prop-types';
import GL from '@luma.gl/constants';

import * as LumaGL from '../lumagl';
import makeTooltip from '../tooltip';


// CSV loader is needed to download and read CSV Files
registerLoaders(CSVLoader);
// Configure the JSON converter to include all possible
// layers and views.
const configuration = {
  classes: Object.assign(
    {}, 
    layers, 
    aggregationLayers,
    geoLayers,
    meshLayers,
    // All the core elements of deck.gl
    core,
    // Cherry picked luma.gl exports relevant to deck
    LumaGL
  ),
  enumerations: {
    COORDINATE_SYSTEM: core.COORDINATE_SYSTEM,
    GL
  }
}
const jsonConverter = new JSONConverter({ configuration });


/**
 * This component lets you visualizes PyDeck and deck/json files
 * directly in Dash. It also exposes various events (such as click,
 * hover and drag) inside callbacks.
 */
export default class DeckGL extends React.Component {
  safeSetProps(events){
    // This method sanitizes the info and event objects that are
    // output by onClick, onHover, etc. Then, it proceeds to call setProps.
    const propsToClean = ["layer", "target", "rootElement"];

    Object.keys(events).map(key => {
      const e = events[key];
      // Cleaning starts here:
      propsToClean.map(prop => {
        if (prop in e && e[prop] !== null){
          e[prop] = e[prop].toString();
        }
      })
    })

    if ('setProps' in this.props){
      this.props.setProps(events);
    } else {
      console.warn(
        "setProps is not a function of this.props, as a result the following object was not updated:", 
        events,
      );
    }
  }

  componentDidMount() {
    const { disableContext } = this.props
    if (disableContext) {
        document
            .getElementById("deckgl-wrapper")
            .addEventListener("contextmenu", evt => evt.preventDefault());
    }
  }


  render() {
    let {enableEvents, data} = this.props;
    const {id, mapboxKey, tooltip, style} = this.props;
    const getTooltip = makeTooltip(tooltip);

    // ******* PARSE AND CONVERT JSON *******
    // If data is a string, we need to convert into JSON format
    if (typeof(data) === "string"){
      data = JSON.parse(data);
    }
    // Now, we can convert the JSON document to a deck object
    const deckProps = jsonConverter.convert(data);

    // ******** UPDATE DECK PROPS ********
    // Assign the ID to the deck object
    deckProps.id = id;
    // Extract the map style from JSON document, since the map style 
    // is sometimes located in data.views.length
    if (!("mapStyle" in deckProps) && "views" in data && data.views.length > 0){
      deckProps.mapStyle = data.views[0].mapStyle;
    }

    // ******** STATIC MAP ******** 
    // Only render static map if a mapbox token was given
    let staticMap;
    if (mapboxKey !== null){
      staticMap = <StaticMap
        mapboxApiAccessToken={mapboxKey}
        mapStyle={deckProps.mapStyle}
      />
    } else {
      staticMap = null;
    }

    // ******** EVENT CALLBACKS ********
    // First, convert enableEvents to list if it was a boolean
    if (enableEvents === true){
      enableEvents = ['click', 'dragStart', 'dragEnd', 'hover'];
    }
    else if (enableEvents === false){
      enableEvents = [];
    }
    // Now, construct the respective functions
    const clickFn = (info, e) => this.safeSetProps({clickInfo: info, clickEvent: e});
    const dragStartFn = (info, e) => this.safeSetProps({dragStartInfo: info, dragStartEvent: e});
    const dragEndFn = (info, e) => this.safeSetProps({dragEndInfo: info, dragEndEvent: e});
    const hoverFn = (info, e) => this.safeSetProps({hoverInfo: info, hoverEvent: e});

    // Finally, assign them as prop to deckProps
    deckProps.onClick = enableEvents.includes("click") ? clickFn: null;
    deckProps.onDragStart = enableEvents.includes("dragStart") ? dragStartFn: null;
    deckProps.onDragEnd = enableEvents.includes("dragEnd") ? dragEndFn: null;
    deckProps.onHover = enableEvents.includes("hover") ? hoverFn: null;

    return (
        <Deck
            getTooltip={getTooltip}
            style={style}
            {...deckProps}
        >
          {staticMap}
        </Deck>
    );
  }
}

DeckGL.defaultProps = {
    data: {},
    mapboxKey: null,
    tooltip: false,
    enableEvents: false,
    disableContext: false,
    style: {}
};

DeckGL.propTypes = {
    /**
     * Your map using the Deck.gl JSON format. This can be generated by calling
     * `pdk.Deck(...).to_json()`. Both a Python dictionary and a JSON-string your map is accepted.
     */
    data: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),

    /**
     * The ID used to identify this component in Dash callbacks.
     */
    id: PropTypes.string,

    /**
     * Custom CSS for your map. This is useful for changing the height, width, and sometimes the background color.
     */
    style: PropTypes.object,

    /**
     * Either a boolean indicating if all event callbacks should be enabled, or a list of strings
     * indicating which ones should be used. If it's a list, you will need to specify one of the
     * following gestures: `click`, `dragStart`, `dragEnd`, `hover`.
     */
    enableEvents: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.string), PropTypes.bool
    ]),

    /**
     * This can be a boolean value (e.g. `True`, `False`) to display the default tooltip.
     * You can also give a dictionary specifying an `html` template and custom style using `css`. For more
     * information about templating, see: https://pydeck.gl/tooltip.html
     */
    tooltip: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),


    /**
     * You will need a mapbox token to use deck.gl. Please create a mapbox
     * and follow the instructions here: https://docs.mapbox.com/help/how-mapbox-works/access-tokens/
     */
    mapboxKey: PropTypes.string,

    /**
     * This is a boolean value (e.g. `True`, `False`)  indicating whether or not to disable the default context menu
     * that shows up when right clicking on the map. If set to `True`, right clicking to rotate
     * a map or adjust its pitch will not trigger the default context menu.
     */
    disableContext: PropTypes.bool,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when an element in the map is clicked. This contains
     * the original gesture event (in JSON).
     */
    clickEvent: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when an element in the map is clicked. This contains
     * the picking info describing the object being clicked.
     * Complete description here:
     * https://deck.gl/docs/developer-guide/interactivity#the-picking-info-object
     */
    clickInfo: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when an element in the map is hovered. This contains
     * the original gesture event (in JSON).
     */
    hoverEvent: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when an element in the map is hovered. This contains
     * the picking info describing the object being hovered.
     * Complete description here:
     * https://deck.gl/docs/developer-guide/interactivity#the-picking-info-object
     */
    hoverInfo: PropTypes.object,

    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type. 
     * This prop is updated when the user starts dragging on the canvas. This contains
     * the original gesture event (in JSON).
     */
    dragStartEvent: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when the user starts dragging on the canvas. This contains
     * the picking info describing the object being dragged.
     * Complete description here:
     * https://deck.gl/docs/developer-guide/interactivity#the-picking-info-object
     */
    dragStartInfo: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when the user releases from dragging the canvas. This contains
     * the original gesture event (in JSON).
     */
    dragEndEvent: PropTypes.object,


    /**
     * Read-only prop. To use this, make sure that `enableEvents` is set to `True`, or that `enableEvents` is a list that contains this event type.
     * This prop is updated when the user releases from dragging the canvas. This contains
     * the picking info describing the object being dragged.
     * Complete description here:
     * https://deck.gl/docs/developer-guide/interactivity#the-picking-info-object
     */
    dragEndInfo: PropTypes.object,


    /**
     * Dash-assigned callback that should be called to report property changes
     * to Dash, to make them available for callbacks.
     */
    setProps: PropTypes.func
};