/* eslint-disable no-param-reassign */
/* eslint-disable react/no-multi-comp */
import d3 from 'd3';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import MapGL from 'react-map-gl';
import Immutable from 'immutable';
import supercluster from 'supercluster';
import ViewportMercator from 'viewport-mercator-project';

import {
  kmToPixels,
  rgbLuminance,
  isNumeric,
  MILES_PER_KM,
  DEFAULT_LONGITUDE,
  DEFAULT_LATITUDE,
  DEFAULT_ZOOM,
} from '../utils/common';
import './mapbox.css';

const NOOP = () => {};

class ScatterPlotGlowOverlay extends React.Component {
  componentDidMount() {
    this.redraw();
  }

  componentDidUpdate() {
    this.redraw();
  }
  drawText(ctx, pixel, options = {}) {
    const IS_DARK_THRESHOLD = 110;
    const { fontHeight = 0, label = '', radius = 0, rgb = [0, 0, 0], shadow = false } = options;
    const maxWidth = radius * 1.8;
    const luminance = rgbLuminance(rgb[1], rgb[2], rgb[3]);

    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black';
    ctx.font = fontHeight + 'px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    if (shadow) {
      ctx.shadowBlur = 15;
      ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : '';
    }

    const textWidth = ctx.measureText(label).width;
    if (textWidth > maxWidth) {
      const scale = fontHeight / textWidth;
      ctx.font = scale * maxWidth + 'px sans-serif';
    }

    ctx.fillText(label, pixel[0], pixel[1]);
    ctx.globalCompositeOperation = this.props.compositeOperation;
    ctx.shadowBlur = 0;
    ctx.shadowColor = '';
  }

  // Modified: https://github.com/uber/react-map-gl/blob/master/src/overlays/scatterplot.react.js
  redraw() {
    const props = this.props;
    const pixelRatio = window.devicePixelRatio || 1;
    const canvas = this.refs.overlay;
    const ctx = canvas.getContext('2d');
    const radius = props.dotRadius;
    const mercator = ViewportMercator(props);
    const rgb = props.rgb;
    const clusterLabelMap = [];
    let maxLabel = -1;

    props.locations.forEach(function (location, i) {
      if (location.get('properties').get('cluster')) {
        let clusterLabel = location.get('properties').get('metric')
          ? location.get('properties').get('metric')
          : location.get('properties').get('point_count');

        if (clusterLabel instanceof Immutable.List) {
          clusterLabel = clusterLabel.toArray();
          if (props.aggregatorName === 'mean') {
            clusterLabel = d3.mean(clusterLabel);
          } else if (props.aggregatorName === 'median') {
            clusterLabel = d3.median(clusterLabel);
          } else if (props.aggregatorName === 'stdev') {
            clusterLabel = d3.deviation(clusterLabel);
          } else {
            clusterLabel = d3.variance(clusterLabel);
          }
        }

        clusterLabel = isNumeric(clusterLabel)
          ? d3.round(clusterLabel, 2)
          : location.get('properties').get('point_count');
        maxLabel = Math.max(clusterLabel, maxLabel);
        clusterLabelMap[i] = clusterLabel;
      }
    }, this);

    ctx.save();
    ctx.scale(pixelRatio, pixelRatio);
    ctx.clearRect(0, 0, props.width, props.height);
    ctx.globalCompositeOperation = props.compositeOperation;

    if ((props.renderWhileDragging || !props.isDragging) && props.locations) {
      props.locations.forEach(function _forEach(location, i) {
        const pixel = mercator.project(props.lngLatAccessor(location));
        const pixelRounded = [d3.round(pixel[0], 1), d3.round(pixel[1], 1)];

        if (pixelRounded[0] + radius >= 0
              && pixelRounded[0] - radius < props.width
              && pixelRounded[1] + radius >= 0
              && pixelRounded[1] - radius < props.height) {
          ctx.beginPath();
          if (location.get('properties').get('cluster')) {
            let clusterLabel = clusterLabelMap[i];
            const scaledRadius = d3.round(Math.pow(clusterLabel / maxLabel, 0.5) * radius, 1);
            const fontHeight = d3.round(scaledRadius * 0.5, 1);
            const gradient = ctx.createRadialGradient(
              pixelRounded[0], pixelRounded[1], scaledRadius,
              pixelRounded[0], pixelRounded[1], 0,
            );

            gradient.addColorStop(1, 'rgba(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ', 0.8)');
            gradient.addColorStop(0, 'rgba(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ', 0)');
            ctx.arc(pixelRounded[0], pixelRounded[1], scaledRadius, 0, Math.PI * 2);
            ctx.fillStyle = gradient;
            ctx.fill();

            if (isNumeric(clusterLabel)) {
              if (clusterLabel >= 10000) {
                clusterLabel = Math.round(clusterLabel / 1000) + 'k';
              } else if (clusterLabel >= 1000) {
                clusterLabel = (Math.round(clusterLabel / 100) / 10) + 'k';
              }
              this.drawText(ctx, pixelRounded, {
                fontHeight,
                label: clusterLabel,
                radius: scaledRadius,
                rgb,
                shadow: true,
              });
            }
          } else {
            const defaultRadius = radius / 6;
            const radiusProperty = location.get('properties').get('radius');
            const pointMetric = location.get('properties').get('metric');
            let pointRadius = radiusProperty === null ? defaultRadius : radiusProperty;
            let pointLabel;

            if (radiusProperty !== null) {
              const pointLatitude = props.lngLatAccessor(location)[1];
              if (props.pointRadiusUnit === 'Kilometers') {
                pointLabel = d3.round(pointRadius, 2) + 'km';
                pointRadius = kmToPixels(pointRadius, pointLatitude, props.zoom);
              } else if (props.pointRadiusUnit === 'Miles') {
                pointLabel = d3.round(pointRadius, 2) + 'mi';
                pointRadius = kmToPixels(pointRadius * MILES_PER_KM, pointLatitude, props.zoom);
              }
            }

            if (pointMetric !== null) {
              pointLabel = isNumeric(pointMetric) ? d3.round(pointMetric, 2) : pointMetric;
            }

            // Fall back to default points if pointRadius wasn't a numerical column
            if (!pointRadius) {
              pointRadius = defaultRadius;
            }

            ctx.arc(pixelRounded[0], pixelRounded[1], d3.round(pointRadius, 1), 0, Math.PI * 2);
            ctx.fillStyle = 'rgb(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ')';
            ctx.fill();

            if (pointLabel !== undefined) {
              this.drawText(ctx, pixelRounded, {
                fontHeight: d3.round(pointRadius, 1),
                label: pointLabel,
                radius: pointRadius,
                rgb,
                shadow: false,
              });
            }
          }
        }
      }, this);
    }

    ctx.restore();
  }
  render() {
    let width = 0;
    let height = 0;
    if (this.context.viewport) {
      width = this.context.viewport.width;
      height = this.context.viewport.height;
    }
    const { globalOpacity } = this.props;
    const pixelRatio = window.devicePixelRatio || 1;
    return (
      React.createElement('canvas', {
        ref: 'overlay',
        width: width * pixelRatio,
        height: height * pixelRatio,
        style: {
          width: `${width}px`,
          height: `${height}px`,
          position: 'absolute',
          pointerEvents: 'none',
          opacity: globalOpacity,
          left: 0,
          top: 0,
        },
      })
    );
  }
}
ScatterPlotGlowOverlay.propTypes = {
  locations: PropTypes.instanceOf(Immutable.List).isRequired,
  lngLatAccessor: PropTypes.func,
  renderWhileDragging: PropTypes.bool,
  globalOpacity: PropTypes.number,
  dotRadius: PropTypes.number,
  dotFill: PropTypes.string,
  compositeOperation: PropTypes.string,
};

ScatterPlotGlowOverlay.defaultProps = {
  lngLatAccessor: location => [location.get(0), location.get(1)],
  renderWhileDragging: true,
  dotRadius: 4,
  dotFill: '#1FBAD6',
  globalOpacity: 1,
  // Same as browser default.
  compositeOperation: 'source-over',
};
ScatterPlotGlowOverlay.contextTypes = {
  viewport: PropTypes.object,
  isDragging: PropTypes.bool,
};

class MapboxViz extends React.Component {
  constructor(props) {
    super(props);
    const longitude = this.props.viewportLongitude || DEFAULT_LONGITUDE;
    const latitude = this.props.viewportLatitude || DEFAULT_LATITUDE;

    this.state = {
      viewport: {
        longitude,
        latitude,
        zoom: this.props.viewportZoom || DEFAULT_ZOOM,
        startDragLngLat: [longitude, latitude],
      },
    };
    this.onViewportChange = this.onViewportChange.bind(this);
  }

  onViewportChange(viewport) {
    this.setState({ viewport });
    this.props.setControlValue('viewport_longitude', viewport.longitude);
    this.props.setControlValue('viewport_latitude', viewport.latitude);
    this.props.setControlValue('viewport_zoom', viewport.zoom);
  }

  render() {
    const mercator = ViewportMercator({
      width: this.props.sliceWidth,
      height: this.props.sliceHeight,
      longitude: this.state.viewport.longitude,
      latitude: this.state.viewport.latitude,
      zoom: this.state.viewport.zoom,
    });
    const topLeft = mercator.unproject([0, 0]);
    const bottomRight = mercator.unproject([this.props.sliceWidth, this.props.sliceHeight]);
    const bbox = [topLeft[0], bottomRight[1], bottomRight[0], topLeft[1]];
    const clusters = this.props.clusterer.getClusters(bbox, Math.round(this.state.viewport.zoom));
    const isDragging = this.state.viewport.isDragging === undefined ? false :
                       this.state.viewport.isDragging;
    return (
      <MapGL
        {...this.state.viewport}
        mapStyle={this.props.mapStyle}
        width={this.props.sliceWidth}
        height={this.props.sliceHeight}
        mapboxApiAccessToken={this.props.mapboxApiKey}
        onViewportChange={this.onViewportChange}
      >
        <ScatterPlotGlowOverlay
          {...this.state.viewport}
          isDragging={isDragging}
          width={this.props.sliceWidth}
          height={this.props.sliceHeight}
          locations={Immutable.fromJS(clusters)}
          dotRadius={this.props.pointRadius}
          pointRadiusUnit={this.props.pointRadiusUnit}
          rgb={this.props.rgb}
          globalOpacity={this.props.globalOpacity}
          compositeOperation={'screen'}
          renderWhileDragging={this.props.renderWhileDragging}
          aggregatorName={this.props.aggregatorName}
          lngLatAccessor={function (location) {
            const coordinates = location.get('geometry').get('coordinates');
            return [coordinates.get(0), coordinates.get(1)];
          }}
        />
      </MapGL>
    );
  }
}
MapboxViz.propTypes = {
  aggregatorName: PropTypes.string,
  clusterer: PropTypes.object,
  setControlValue: PropTypes.func,
  globalOpacity: PropTypes.number,
  mapStyle: PropTypes.string,
  mapboxApiKey: PropTypes.string,
  pointRadius: PropTypes.number,
  pointRadiusUnit: PropTypes.string,
  renderWhileDragging: PropTypes.bool,
  rgb: PropTypes.array,
  sliceHeight: PropTypes.number,
  sliceWidth: PropTypes.number,
  viewportLatitude: PropTypes.number,
  viewportLongitude: PropTypes.number,
  viewportZoom: PropTypes.number,
};

function mapbox(slice, json, setControlValue) {
  const div = d3.select(slice.selector);
  const DEFAULT_POINT_RADIUS = 60;
  const DEFAULT_MAX_ZOOM = 16;

  // Validate mapbox color
  const rgb = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/.exec(json.data.color);
  if (rgb === null) {
    slice.error('Color field must be of form \'rgb(%d, %d, %d)\'');
    return;
  }

  const aggName = json.data.aggregatorName;
  let reducer;

  if (aggName === 'sum' || !json.data.customMetric) {
    reducer = function (a, b) {
      return a + b;
    };
  } else if (aggName === 'min') {
    reducer = Math.min;
  } else if (aggName === 'max') {
    reducer = Math.max;
  } else {
    reducer = function (a, b) {
      if (a instanceof Array) {
        if (b instanceof Array) {
          return a.concat(b);
        }
        a.push(b);
        return a;
      }
      if (b instanceof Array) {
        b.push(a);
        return b;
      }
      return [a, b];
    };
  }

  const clusterer = supercluster({
    radius: json.data.clusteringRadius,
    maxZoom: DEFAULT_MAX_ZOOM,
    metricKey: 'metric',
    metricReducer: reducer,
  });
  clusterer.load(json.data.geoJSON.features);

  div.selectAll('*').remove();
  ReactDOM.render(
    <MapboxViz
      {...json.data}
      rgb={rgb}
      sliceHeight={slice.height()}
      sliceWidth={slice.width()}
      clusterer={clusterer}
      pointRadius={DEFAULT_POINT_RADIUS}
      aggregatorName={aggName}
      setControlValue={setControlValue || NOOP}
    />,
    div.node(),
  );
}

module.exports = mapbox;
