text: // @flow
  // Copyright (c) 2015 Uber Technologies, Inc.
  
  // Permission is hereby granted, free of charge, to any person obtaining a copy
  // of this software and associated documentation files (the "Software"), to deal
  // in the Software without restriction, including without limitation the rights
  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  // copies of the Software, and to permit persons to whom the Software is
  // furnished to do so, subject to the following conditions:
  
  // The above copyright notice and this permission notice shall be included in
  // all copies or substantial portions of the Software.
  
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  // THE SOFTWARE.
  
  /* global window, process, HTMLCanvasElement */
  import PropTypes from 'prop-types';
  import {document} from '../utils/globals';
  
  function noop() {}
  
  const propTypes = {
    // Creation parameters
    container: PropTypes.object, /** The container to have the map. */
    gl: PropTypes.object, /** External WebGLContext to use */
  
    mapboxApiAccessToken: PropTypes.string, /** Mapbox API access token for Mapbox tiles/styles. */
    attributionControl: PropTypes.bool, /** Show attribution control or not. */
    preserveDrawingBuffer: PropTypes.bool, /** Useful when you want to export the canvas as a PNG. */
    reuseMaps: PropTypes.bool,
    transformRequest: PropTypes.func, /** The transformRequest callback for the map */
    mapOptions: PropTypes.object, /** Extra options to pass to Mapbox constructor. See #545. **/
    mapStyle: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object
    ]), /** The Mapbox style. A string url to a MapboxGL style */
  
    visible: PropTypes.bool, /** Whether the map is visible */
  
    onLoad: PropTypes.func, /** The onLoad callback for the map */
    onError: PropTypes.func, /** The onError callback for the map */
  
    // Map view state
    width: PropTypes.number, /** The width of the map. */
    height: PropTypes.number, /** The height of the map. */
  
    viewState: PropTypes.object, /** object containing lng/lat/zoom/bearing/pitch */
  
    longitude: PropTypes.number, /** The longitude of the center of the map. */
    latitude: PropTypes.number, /** The latitude of the center of the map. */
    zoom: PropTypes.number, /** The tile zoom level of the map. */
    bearing: PropTypes.number, /** Specify the bearing of the viewport */
    pitch: PropTypes.number, /** Specify the pitch of the viewport */
    // Note: Non-public API, see https://github.com/mapbox/mapbox-gl-js/issues/1137
    altitude: PropTypes.number /** Altitude of the viewport camera. Default 1.5 "screen heights" */
  };
  
  const defaultProps = {
    container: document.body,
    mapboxApiAccessToken: getAccessToken(),
    preserveDrawingBuffer: false,
    attributionControl: true,
    reuseMaps: false,
    mapOptions: {},
    mapStyle: 'mapbox://styles/mapbox/light-v8',
  
    visible: true,
  
    onLoad: noop,
    onError: noop,
  
    width: 0,
    height: 0,
    longitude: 0,
    latitude: 0,
    zoom: 0,
    bearing: 0,
    pitch: 0
  };
  
  type MapboxGL = {
    version: string,
    accessToken: string,
    Map: Function
  };
  
  export type ViewState = {
    longitude: number,
    latitude: number,
    zoom: number,
    bearing: number,
    pitch: number,
    altitude?: number
  };
  
  type Props = {
    mapboxgl?: MapboxGL,
    container: any,
    gl?: any,
    mapboxApiAccessToken: string,
    attributionControl: boolean,
    preserveDrawingBuffer: boolean,
    onLoad: Function,
    onError: Function,
    reuseMaps: boolean,
    transformRequest?: Function,
    mapStyle: any,
    visible: boolean,
    width: number,
    height: number,
    viewState?: ViewState,
    longitude: number,
    latitude: number,
    zoom: number,
    bearing: number,
    pitch: number,
    altitude?: number,
    mapOptions: any
  };
  
  // Try to get access token from URL, env, local storage or config
  export function getAccessToken() : string {
    let accessToken = null;
  
    if (typeof window !== 'undefined' && window.location) {
      const match = window.location.search.match(/access_token=([^&\/]*)/);
      accessToken = match && match[1];
    }
  
    if (!accessToken && typeof process !== 'undefined') {
      // Note: This depends on bundler plugins (e.g. webpack) importing environment correctly
      accessToken = accessToken || process.env.MapboxAccessToken || process.env.REACT_APP_MAPBOX_ACCESS_TOKEN; // eslint-disable-line
    }
  
    // Prevents mapbox from throwing
    return accessToken || 'no-token';
  }
  
  // Helper function to merge defaultProps and check prop types
  function checkPropTypes(props, component = 'component') {
    // TODO - check for production (unless done by prop types package?)
    if (props.debug) {
      PropTypes.checkPropTypes(propTypes, props, 'prop', component);
    }
  }
  
  // A small wrapper class for mapbox-gl
  // - Provides a prop style interface (that can be trivially used by a React wrapper)
  // - Makes sure mapbox doesn't crash under Node
  // - Handles map reuse (to work around Mapbox resource leak issues)
  // - Provides support for specifying tokens during development
  
  export default class Mapbox {
  
    static initialized : boolean = false;
    static propTypes : any = propTypes;
    static defaultProps : any = defaultProps;
    static savedMap : any = null;
  
    constructor(props: Props) {
      if (!props.mapboxgl) {
        throw new Error('Mapbox not available');
      }
  
      this.mapboxgl = props.mapboxgl;
  
      if (!Mapbox.initialized) {
        Mapbox.initialized = true;
  
        // Version detection using babel plugin
        // $FlowFixMe
        // const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'untranspiled source';
        // TODO - expose version for debug
  
        this._checkStyleSheet(this.mapboxgl.version);
      }
  
      this._initialize(props);
    }
  
    mapboxgl : MapboxGL;
    props : Props = defaultProps;
    _map : any = null;
    width : number = 0;
    height : number = 0;
  
    finalize() {
      this._destroy();
      return this;
    }
  
    setProps(props: Props) {
      this._update(this.props, props);
      return this;
    }
  
    // Mapbox's map.resize() reads size from DOM, so DOM element must already be resized
    // In a system like React we must wait to read size until after render
    // (e.g. until "componentDidUpdate")
    resize() {
      this._map.resize();
      // map render will throw error if style is not loaded
      if (this._map.isStyleLoaded()) {
        this._map._render();
      }
      return this;
    }
  
    // External apps can access map this way
    getMap() {
      return this._map;
    }
  
    // PRIVATE API
  
    _create(props: Props) {
      // Reuse a saved map, if available
      if (props.reuseMaps && Mapbox.savedMap) {
        this._map = Mapbox.savedMap;
        // When reusing the saved map, we need to reparent the map(canvas) and other child nodes
        // intoto the new container from the props.
        // Step1: reparenting child nodes from old container to new container
        const oldContainer = this._map.getContainer();
        const newContainer = props.container;
        newContainer.classList.add('mapboxgl-map');
        while (oldContainer.childNodes.length > 0) {
          newContainer.appendChild(oldContainer.childNodes[0]);
        }
        // Step2: replace the internal container with new container from the react component
        this._map._container = newContainer;
        Mapbox.savedMap = null;
  
        // Update style
        if (props.mapStyle) {
          this._map.setStyle(props.mapStyle);
        }
  
        // TODO - need to call onload again, need to track with Promise?
        props.onLoad();
      } else {
        if (props.gl) {
          const getContext = HTMLCanvasElement.prototype.getContext;
          // Hijack canvas.getContext to return our own WebGLContext
          // This will be called inside the mapboxgl.Map constructor
          // $FlowFixMe
          HTMLCanvasElement.prototype.getContext = () => {
            // Unhijack immediately
            // $FlowFixMe
            HTMLCanvasElement.prototype.getContext = getContext;
            return props.gl;
          };
        }
  
        const mapOptions : any = {
          container: props.container,
          center: [0, 0],
          zoom: 8,
          pitch: 0,
          bearing: 0,
          maxZoom: 24,
          style: props.mapStyle,
          interactive: false,
          trackResize: false,
          attributionControl: props.attributionControl,
          preserveDrawingBuffer: props.preserveDrawingBuffer
        };
        // We don't want to pass a null or no-op transformRequest function.
        if (props.transformRequest) {
          mapOptions.transformRequest = props.transformRequest;
        }
        this._map = new this.mapboxgl.Map(
          Object.assign({}, mapOptions, props.mapOptions));
  
        // Attach optional onLoad function
        this._map.once('load', props.onLoad);
        this._map.on('error', props.onError);
      }
  
      return this;
    }
  
    _destroy() {
      if (!this._map) {
        return;
      }
  
      if (!Mapbox.savedMap) {
        Mapbox.savedMap = this._map;
      } else {
        this._map.remove();
      }
      this._map = null;
    }
  
    _initialize(props: Props) {
      props = Object.assign({}, defaultProps, props);
      checkPropTypes(props, 'Mapbox');
  
      // Creation only props
      this.mapboxgl.accessToken = props.mapboxApiAccessToken || defaultProps.mapboxApiAccessToken;
  
      this._create(props);
  
      // Hijack dimension properties
      // This eliminates the timing issue between calling resize() and DOM update
      /* eslint-disable accessor-pairs */
      const {container} = props;
      // $FlowFixMe
      Object.defineProperty(container, 'offsetWidth', {get: () => this.width});
      // $FlowFixMe
      Object.defineProperty(container, 'clientWidth', {get: () => this.width});
      // $FlowFixMe
      Object.defineProperty(container, 'offsetHeight', {get: () => this.height});
      // $FlowFixMe
      Object.defineProperty(container, 'clientHeight', {get: () => this.height});
  
      // Disable outline style
      const canvas = this._map.getCanvas();
      if (canvas) {
        canvas.style.outline = 'none';
      }
  
      this._updateMapViewport({}, props);
      this._updateMapSize({}, props);
  
      this.props = props;
    }
  
    _update(oldProps: Props, newProps: Props) {
      if (!this._map) {
        return;
      }
  
      newProps = Object.assign({}, this.props, newProps);
      checkPropTypes(newProps, 'Mapbox');
  
      this._updateMapViewport(oldProps, newProps);
      this._updateMapSize(oldProps, newProps);
  
      this.props = newProps;
    }
  
    // Note: needs to be called after render (e.g. in componentDidUpdate)
    _updateMapSize(oldProps: any, newProps: Props) {
      const sizeChanged = oldProps.width !== newProps.width || oldProps.height !== newProps.height;
      if (sizeChanged) {
        this.width = newProps.width;
        this.height = newProps.height;
        this.resize();
      }
    }
  
    _updateMapViewport(oldProps: any, newProps: Props) {
      const oldViewState = this._getViewState(oldProps);
      const newViewState = this._getViewState(newProps);
  
      const viewportChanged =
        newViewState.latitude !== oldViewState.latitude ||
        newViewState.longitude !== oldViewState.longitude ||
        newViewState.zoom !== oldViewState.zoom ||
        newViewState.pitch !== oldViewState.pitch ||
        newViewState.bearing !== oldViewState.bearing ||
        newViewState.altitude !== oldViewState.altitude;
  
      if (viewportChanged) {
        this._map.jumpTo(this._getMapboxViewStateProps(newProps));
  
        // TODO - jumpTo doesn't handle altitude
        if (newViewState.altitude !== oldViewState.altitude) {
          this._map.transform.altitude = newViewState.altitude;
        }
      }
    }
  
    _getViewState(props: Props) : ViewState {
      const {
        longitude,
        latitude,
        zoom,
        pitch = 0,
        bearing = 0,
        altitude = 1.5
      } = props.viewState || props;
      return {longitude, latitude, zoom, pitch, bearing, altitude};
    }
  
    _checkStyleSheet(mapboxVersion : string = '0.47.0') {
      if (typeof document === 'undefined') {
        return;
      }
  
      // check mapbox styles
      try {
        const testElement = document.createElement('div');
        testElement.className = 'mapboxgl-map';
        testElement.style.display = 'none';
        document.body.append(testElement);
        const isCssLoaded = window.getComputedStyle(testElement).position !== 'static';
  
        if (!isCssLoaded) {
          // attempt to insert mapbox stylesheet
          const link = document.createElement('link');
          link.setAttribute('rel', 'stylesheet');
          link.setAttribute('type', 'text/css');
          link.setAttribute('href',
            `https://api.tiles.mapbox.com/mapbox-gl-js/v${mapboxVersion}/mapbox-gl.css`);
          document.head.append(link);
        }
      } catch (error) {
        // do nothing
      }
    }
  
    _getMapboxViewStateProps(props: Props) {
      const viewState = this._getViewState(props);
      return {
        center: [viewState.longitude, viewState.latitude],
        zoom: viewState.zoom,
        bearing: viewState.bearing,
        pitch: viewState.pitch
      };
    }
  }
  