import LayerManager from './layer-manager';
import ViewManager from './view-manager';
import MapView from '../views/map-view';
import EffectManager from './effect-manager';
import DeckRenderer from './deck-renderer';
import DeckPicker from './deck-picker';
import { WidgetManager } from './widget-manager';
import Tooltip from './tooltip';
import log from '../utils/log';
import { deepEqual } from '../utils/deep-equal';
import typedArrayManager from '../utils/typed-array-manager';
import { VERSION } from './init';
import { getBrowser } from '@probe.gl/env';
import { luma } from '@luma.gl/core';
import { WebGLDevice } from '@luma.gl/webgl';
import { Timeline } from '@luma.gl/engine';
import { setGLParameters } from '@luma.gl/webgl';
import { AnimationLoop } from '@luma.gl/engine';
import { GL } from '@luma.gl/constants';
import { Stats } from '@probe.gl/stats';
import { EventManager } from 'mjolnir.js';
import assert from '../utils/assert';
import { EVENTS } from './constants';

function noop() {}

const getCursor = ({
  isDragging
}) => isDragging ? 'grabbing' : 'grab';

const defaultProps = {
  id: '',
  width: '100%',
  height: '100%',
  style: null,
  viewState: null,
  initialViewState: null,
  pickingRadius: 0,
  layerFilter: null,
  parameters: {},
  parent: null,
  device: null,
  deviceProps: {
    type: 'webgl'
  },
  gl: null,
  glOptions: {},
  canvas: null,
  layers: [],
  effects: [],
  views: null,
  controller: null,
  useDevicePixels: true,
  touchAction: 'none',
  eventRecognizerOptions: {},
  _framebuffer: null,
  _animate: false,
  _pickable: true,
  _typedArrayManagerProps: {},
  _customRender: null,
  widgets: [],
  onDeviceInitialized: noop,
  onWebGLInitialized: noop,
  onResize: noop,
  onViewStateChange: noop,
  onInteractionStateChange: noop,
  onBeforeRender: noop,
  onAfterRender: noop,
  onLoad: noop,
  onError: error => log.error(error.message, error.cause)(),
  onHover: null,
  onClick: null,
  onDragStart: null,
  onDrag: null,
  onDragEnd: null,
  _onMetrics: null,
  getCursor,
  getTooltip: null,
  debug: false,
  drawPickingColors: false
};
export default class Deck {
  constructor(props) {
    this.props = void 0;
    this.width = 0;
    this.height = 0;
    this.userData = {};
    this.device = null;
    this.canvas = null;
    this.viewManager = null;
    this.layerManager = null;
    this.effectManager = null;
    this.deckRenderer = null;
    this.deckPicker = null;
    this.eventManager = null;
    this.widgetManager = null;
    this.tooltip = null;
    this.animationLoop = null;
    this.viewState = void 0;
    this.cursorState = {
      isHovering: false,
      isDragging: false
    };
    this.stats = new Stats({
      id: 'deck.gl'
    });
    this.metrics = {
      fps: 0,
      setPropsTime: 0,
      updateAttributesTime: 0,
      framesRedrawn: 0,
      pickTime: 0,
      pickCount: 0,
      gpuTime: 0,
      gpuTimePerFrame: 0,
      cpuTime: 0,
      cpuTimePerFrame: 0,
      bufferMemory: 0,
      textureMemory: 0,
      renderbufferMemory: 0,
      gpuMemory: 0
    };
    this._metricsCounter = 0;
    this._needsRedraw = 'Initial render';
    this._pickRequest = {
      mode: 'hover',
      x: -1,
      y: -1,
      radius: 0,
      event: null
    };
    this._lastPointerDownInfo = null;

    this._onPointerMove = event => {
      const {
        _pickRequest
      } = this;

      if (event.type === 'pointerleave') {
        _pickRequest.x = -1;
        _pickRequest.y = -1;
        _pickRequest.radius = 0;
      } else if (event.leftButton || event.rightButton) {
        return;
      } else {
        const pos = event.offsetCenter;

        if (!pos) {
          return;
        }

        _pickRequest.x = pos.x;
        _pickRequest.y = pos.y;
        _pickRequest.radius = this.props.pickingRadius;
      }

      if (this.layerManager) {
        this.layerManager.context.mousePosition = {
          x: _pickRequest.x,
          y: _pickRequest.y
        };
      }

      _pickRequest.event = event;
    };

    this._onEvent = event => {
      const eventOptions = EVENTS[event.type];
      const pos = event.offsetCenter;

      if (!eventOptions || !pos || !this.layerManager) {
        return;
      }

      const layers = this.layerManager.getLayers();
      const info = this.deckPicker.getLastPickedObject({
        x: pos.x,
        y: pos.y,
        layers,
        viewports: this.getViewports(pos)
      }, this._lastPointerDownInfo);
      const {
        layer
      } = info;
      const layerHandler = layer && (layer[eventOptions.handler] || layer.props[eventOptions.handler]);
      const rootHandler = this.props[eventOptions.handler];
      let handled = false;

      if (layerHandler) {
        handled = layerHandler.call(layer, info, event);
      }

      if (!handled) {
        rootHandler === null || rootHandler === void 0 ? void 0 : rootHandler(info, event);
        this.widgetManager.onEvent(info, event);
      }
    };

    this._onPointerDown = event => {
      const pos = event.offsetCenter;

      const pickedInfo = this._pick('pickObject', 'pickObject Time', {
        x: pos.x,
        y: pos.y,
        radius: this.props.pickingRadius
      });

      this._lastPointerDownInfo = pickedInfo.result[0] || pickedInfo.emptyInfo;
    };

    this.props = { ...defaultProps,
      ...props
    };
    props = this.props;

    if (props.viewState && props.initialViewState) {
      log.warn('View state tracking is disabled. Use either `initialViewState` for auto update or `viewState` for manual update.')();
    }

    if (getBrowser() === 'IE') {
      log.warn('IE 11 is not supported')();
    }

    this.viewState = props.initialViewState;

    if (props.device) {
      this.device = props.device;
    } else if (props.gl) {
      this.device = WebGLDevice.attach(props.gl);
    }

    let deviceOrPromise = this.device;

    if (!deviceOrPromise) {
      luma.registerDevices([WebGLDevice]);
      deviceOrPromise = luma.createDevice({ ...props.deviceProps,
        canvas: this._createCanvas(props)
      });
      deviceOrPromise.then(device => {
        this.device = device;
      });
    }

    this.animationLoop = this._createAnimationLoop(deviceOrPromise, props);
    this.setProps(props);

    if (props._typedArrayManagerProps) {
      typedArrayManager.setOptions(props._typedArrayManagerProps);
    }

    this.animationLoop.start();
  }

  finalize() {
    var _this$animationLoop, _this$animationLoop2, _this$layerManager, _this$viewManager, _this$effectManager, _this$deckRenderer, _this$deckPicker, _this$eventManager, _this$widgetManager;

    (_this$animationLoop = this.animationLoop) === null || _this$animationLoop === void 0 ? void 0 : _this$animationLoop.stop();
    (_this$animationLoop2 = this.animationLoop) === null || _this$animationLoop2 === void 0 ? void 0 : _this$animationLoop2.destroy();
    this.animationLoop = null;
    this._lastPointerDownInfo = null;
    (_this$layerManager = this.layerManager) === null || _this$layerManager === void 0 ? void 0 : _this$layerManager.finalize();
    this.layerManager = null;
    (_this$viewManager = this.viewManager) === null || _this$viewManager === void 0 ? void 0 : _this$viewManager.finalize();
    this.viewManager = null;
    (_this$effectManager = this.effectManager) === null || _this$effectManager === void 0 ? void 0 : _this$effectManager.finalize();
    this.effectManager = null;
    (_this$deckRenderer = this.deckRenderer) === null || _this$deckRenderer === void 0 ? void 0 : _this$deckRenderer.finalize();
    this.deckRenderer = null;
    (_this$deckPicker = this.deckPicker) === null || _this$deckPicker === void 0 ? void 0 : _this$deckPicker.finalize();
    this.deckPicker = null;
    (_this$eventManager = this.eventManager) === null || _this$eventManager === void 0 ? void 0 : _this$eventManager.destroy();
    this.eventManager = null;
    (_this$widgetManager = this.widgetManager) === null || _this$widgetManager === void 0 ? void 0 : _this$widgetManager.finalize();
    this.widgetManager = null;

    if (!this.props.canvas && !this.props.device && this.canvas) {
      var _this$canvas$parentEl;

      (_this$canvas$parentEl = this.canvas.parentElement) === null || _this$canvas$parentEl === void 0 ? void 0 : _this$canvas$parentEl.removeChild(this.canvas);
      this.canvas = null;
    }
  }

  setProps(props) {
    var _this$animationLoop3;

    this.stats.get('setProps Time').timeStart();

    if ('onLayerHover' in props) {
      log.removed('onLayerHover', 'onHover')();
    }

    if ('onLayerClick' in props) {
      log.removed('onLayerClick', 'onClick')();
    }

    if (props.initialViewState && !deepEqual(this.props.initialViewState, props.initialViewState, 3)) {
      this.viewState = props.initialViewState;
    }

    Object.assign(this.props, props);

    this._setCanvasSize(this.props);

    const resolvedProps = Object.create(this.props);
    Object.assign(resolvedProps, {
      views: this._getViews(),
      width: this.width,
      height: this.height,
      viewState: this._getViewState()
    });
    (_this$animationLoop3 = this.animationLoop) === null || _this$animationLoop3 === void 0 ? void 0 : _this$animationLoop3.setProps(resolvedProps);

    if (this.layerManager) {
      this.viewManager.setProps(resolvedProps);
      this.layerManager.activateViewport(this.getViewports()[0]);
      this.layerManager.setProps(resolvedProps);
      this.effectManager.setProps(resolvedProps);
      this.deckRenderer.setProps(resolvedProps);
      this.deckPicker.setProps(resolvedProps);
      this.widgetManager.setProps(resolvedProps);
    }

    this.stats.get('setProps Time').timeEnd();
  }

  needsRedraw(opts = {
    clearRedrawFlags: false
  }) {
    if (!this.layerManager) {
      return false;
    }

    if (this.props._animate) {
      return 'Deck._animate';
    }

    let redraw = this._needsRedraw;

    if (opts.clearRedrawFlags) {
      this._needsRedraw = false;
    }

    const viewManagerNeedsRedraw = this.viewManager.needsRedraw(opts);
    const layerManagerNeedsRedraw = this.layerManager.needsRedraw(opts);
    const effectManagerNeedsRedraw = this.effectManager.needsRedraw(opts);
    const deckRendererNeedsRedraw = this.deckRenderer.needsRedraw(opts);
    redraw = redraw || viewManagerNeedsRedraw || layerManagerNeedsRedraw || effectManagerNeedsRedraw || deckRendererNeedsRedraw;
    return redraw;
  }

  redraw(reason) {
    if (!this.layerManager) {
      return;
    }

    let redrawReason = this.needsRedraw({
      clearRedrawFlags: true
    });
    redrawReason = reason || redrawReason;

    if (!redrawReason) {
      return;
    }

    this.stats.get('Redraw Count').incrementCount();

    if (this.props._customRender) {
      this.props._customRender(redrawReason);
    } else {
      this._drawLayers(redrawReason);
    }
  }

  get isInitialized() {
    return this.viewManager !== null;
  }

  getViews() {
    assert(this.viewManager);
    return this.viewManager.views;
  }

  getViewports(rect) {
    assert(this.viewManager);
    return this.viewManager.getViewports(rect);
  }

  getCanvas() {
    return this.canvas;
  }

  pickObject(opts) {
    const infos = this._pick('pickObject', 'pickObject Time', opts).result;

    return infos.length ? infos[0] : null;
  }

  pickMultipleObjects(opts) {
    opts.depth = opts.depth || 10;
    return this._pick('pickObject', 'pickMultipleObjects Time', opts).result;
  }

  pickObjects(opts) {
    return this._pick('pickObjects', 'pickObjects Time', opts);
  }

  _addResources(resources, forceUpdate = false) {
    for (const id in resources) {
      this.layerManager.resourceManager.add({
        resourceId: id,
        data: resources[id],
        forceUpdate
      });
    }
  }

  _removeResources(resourceIds) {
    for (const id of resourceIds) {
      this.layerManager.resourceManager.remove(id);
    }
  }

  _addDefaultEffect(effect) {
    this.effectManager.addDefaultEffect(effect);
  }

  _pick(method, statKey, opts) {
    assert(this.deckPicker);
    const {
      stats
    } = this;
    stats.get('Pick Count').incrementCount();
    stats.get(statKey).timeStart();
    const infos = this.deckPicker[method]({
      layers: this.layerManager.getLayers(opts),
      views: this.viewManager.getViews(),
      viewports: this.getViewports(opts),
      onViewportActive: this.layerManager.activateViewport,
      effects: this.effectManager.getEffects(),
      ...opts
    });
    stats.get(statKey).timeEnd();
    return infos;
  }

  _createCanvas(props) {
    let canvas = props.canvas;

    if (typeof canvas === 'string') {
      canvas = document.getElementById(canvas);
      assert(canvas);
    }

    if (!canvas) {
      canvas = document.createElement('canvas');
      canvas.id = props.id || 'deckgl-overlay';
      const parent = props.parent || document.body;
      parent.appendChild(canvas);
    }

    Object.assign(canvas.style, props.style);
    return canvas;
  }

  _setCanvasSize(props) {
    if (!this.canvas) {
      return;
    }

    const {
      width,
      height
    } = props;

    if (width || width === 0) {
      const cssWidth = Number.isFinite(width) ? `${width}px` : width;
      this.canvas.style.width = cssWidth;
    }

    if (height || height === 0) {
      var _props$style;

      const cssHeight = Number.isFinite(height) ? `${height}px` : height;
      this.canvas.style.position = ((_props$style = props.style) === null || _props$style === void 0 ? void 0 : _props$style.position) || 'absolute';
      this.canvas.style.height = cssHeight;
    }
  }

  _updateCanvasSize() {
    var _canvas$clientWidth, _canvas$clientHeight;

    const {
      canvas
    } = this;

    if (!canvas) {
      return;
    }

    const newWidth = (_canvas$clientWidth = canvas.clientWidth) !== null && _canvas$clientWidth !== void 0 ? _canvas$clientWidth : canvas.width;
    const newHeight = (_canvas$clientHeight = canvas.clientHeight) !== null && _canvas$clientHeight !== void 0 ? _canvas$clientHeight : canvas.height;

    if (newWidth !== this.width || newHeight !== this.height) {
      var _this$viewManager2, _this$layerManager2;

      this.width = newWidth;
      this.height = newHeight;
      (_this$viewManager2 = this.viewManager) === null || _this$viewManager2 === void 0 ? void 0 : _this$viewManager2.setProps({
        width: newWidth,
        height: newHeight
      });
      (_this$layerManager2 = this.layerManager) === null || _this$layerManager2 === void 0 ? void 0 : _this$layerManager2.activateViewport(this.getViewports()[0]);
      this.props.onResize({
        width: newWidth,
        height: newHeight
      });
    }
  }

  _createAnimationLoop(deviceOrPromise, props) {
    const {
      gl,
      onError,
      useDevicePixels
    } = props;
    return new AnimationLoop({
      device: deviceOrPromise,
      useDevicePixels,
      autoResizeDrawingBuffer: !gl,
      autoResizeViewport: false,
      onInitialize: context => this._setDevice(context.device),
      onRender: this._onRenderFrame.bind(this),
      onError
    });
  }

  _getViewState() {
    return this.props.viewState || this.viewState;
  }

  _getViews() {
    let views = this.props.views || [new MapView({
      id: 'default-view'
    })];
    views = Array.isArray(views) ? views : [views];

    if (views.length && this.props.controller) {
      views[0].props.controller = this.props.controller;
    }

    return views;
  }

  _onContextLost() {
    const {
      onError
    } = this.props;

    if (this.animationLoop && onError) {
      onError(new Error('WebGL context is lost'));
    }
  }

  _pickAndCallback() {
    const {
      _pickRequest
    } = this;

    if (_pickRequest.event) {
      const {
        result,
        emptyInfo
      } = this._pick('pickObject', 'pickObject Time', _pickRequest);

      this.cursorState.isHovering = result.length > 0;
      let pickedInfo = emptyInfo;
      let handled = false;

      for (const info of result) {
        var _info$layer;

        pickedInfo = info;
        handled = ((_info$layer = info.layer) === null || _info$layer === void 0 ? void 0 : _info$layer.onHover(info, _pickRequest.event)) || handled;
      }

      if (!handled) {
        var _this$props$onHover, _this$props;

        (_this$props$onHover = (_this$props = this.props).onHover) === null || _this$props$onHover === void 0 ? void 0 : _this$props$onHover.call(_this$props, pickedInfo, _pickRequest.event);
        this.widgetManager.onHover(pickedInfo, _pickRequest.event);
      }

      _pickRequest.event = null;
    }
  }

  _updateCursor() {
    const container = this.props.parent || this.canvas;

    if (container) {
      container.style.cursor = this.props.getCursor(this.cursorState);
    }
  }

  _setDevice(device) {
    var _this$canvas;

    this.device = device;

    if (this.layerManager) {
      return;
    }

    if (!this.canvas) {
      var _this$device$canvasCo;

      this.canvas = (_this$device$canvasCo = this.device.canvasContext) === null || _this$device$canvasCo === void 0 ? void 0 : _this$device$canvasCo.canvas;
    }

    setGLParameters(this.device, {
      blend: true,
      blendFunc: [770, 771, 1, 771],
      polygonOffsetFill: true,
      depthTest: true,
      depthFunc: 515
    });
    this.props.onDeviceInitialized(this.device);

    if (this.device instanceof WebGLDevice) {
      this.props.onWebGLInitialized(this.device.gl);
    }

    const timeline = new Timeline();
    timeline.play();
    this.animationLoop.attachTimeline(timeline);
    this.eventManager = new EventManager(this.props.parent || this.canvas, {
      touchAction: this.props.touchAction,
      recognizerOptions: this.props.eventRecognizerOptions,
      events: {
        pointerdown: this._onPointerDown,
        pointermove: this._onPointerMove,
        pointerleave: this._onPointerMove
      }
    });

    for (const eventType in EVENTS) {
      this.eventManager.on(eventType, this._onEvent);
    }

    this.viewManager = new ViewManager({
      timeline,
      eventManager: this.eventManager,
      onViewStateChange: this._onViewStateChange.bind(this),
      onInteractionStateChange: this._onInteractionStateChange.bind(this),
      views: this._getViews(),
      viewState: this._getViewState(),
      width: this.width,
      height: this.height
    });
    const viewport = this.viewManager.getViewports()[0];
    this.layerManager = new LayerManager(this.device, {
      deck: this,
      stats: this.stats,
      viewport,
      timeline
    });
    this.effectManager = new EffectManager();
    this.deckRenderer = new DeckRenderer(this.device);
    this.deckPicker = new DeckPicker(this.device);
    this.widgetManager = new WidgetManager({
      deck: this,
      parentElement: (_this$canvas = this.canvas) === null || _this$canvas === void 0 ? void 0 : _this$canvas.parentElement
    });
    this.widgetManager.addDefault(new Tooltip());
    this.setProps(this.props);

    this._updateCanvasSize();

    this.props.onLoad();
  }

  _drawLayers(redrawReason, renderOptions) {
    var _this$deckRenderer2;

    const {
      device,
      gl
    } = this.layerManager.context;
    setGLParameters(device, this.props.parameters);
    this.props.onBeforeRender({
      device,
      gl
    });
    const opts = {
      target: this.props._framebuffer,
      layers: this.layerManager.getLayers(),
      viewports: this.viewManager.getViewports(),
      onViewportActive: this.layerManager.activateViewport,
      views: this.viewManager.getViews(),
      pass: 'screen',
      effects: this.effectManager.getEffects(),
      ...renderOptions
    };
    (_this$deckRenderer2 = this.deckRenderer) === null || _this$deckRenderer2 === void 0 ? void 0 : _this$deckRenderer2.renderLayers(opts);

    if (opts.pass === 'screen') {
      this.widgetManager.onRedraw({
        viewports: opts.viewports,
        layers: opts.layers
      });
    }

    this.props.onAfterRender({
      device,
      gl
    });
  }

  _onRenderFrame(animationProps) {
    this._getFrameStats();

    if (this._metricsCounter++ % 60 === 0) {
      this._getMetrics();

      this.stats.reset();
      log.table(4, this.metrics)();

      if (this.props._onMetrics) {
        this.props._onMetrics(this.metrics);
      }
    }

    this._updateCanvasSize();

    this._updateCursor();

    this.layerManager.updateLayers();

    this._pickAndCallback();

    this.redraw();

    if (this.viewManager) {
      this.viewManager.updateViewStates();
    }
  }

  _onViewStateChange(params) {
    const viewState = this.props.onViewStateChange(params) || params.viewState;

    if (this.viewState) {
      this.viewState = { ...this.viewState,
        [params.viewId]: viewState
      };

      if (!this.props.viewState) {
        if (this.viewManager) {
          this.viewManager.setProps({
            viewState: this.viewState
          });
        }
      }
    }
  }

  _onInteractionStateChange(interactionState) {
    this.cursorState.isDragging = interactionState.isDragging || false;
    this.props.onInteractionStateChange(interactionState);
  }

  _getFrameStats() {
    const {
      stats
    } = this;
    stats.get('frameRate').timeEnd();
    stats.get('frameRate').timeStart();
    const animationLoopStats = this.animationLoop.stats;
    stats.get('GPU Time').addTime(animationLoopStats.get('GPU Time').lastTiming);
    stats.get('CPU Time').addTime(animationLoopStats.get('CPU Time').lastTiming);
  }

  _getMetrics() {
    const {
      metrics,
      stats
    } = this;
    metrics.fps = stats.get('frameRate').getHz();
    metrics.setPropsTime = stats.get('setProps Time').time;
    metrics.updateAttributesTime = stats.get('Update Attributes').time;
    metrics.framesRedrawn = stats.get('Redraw Count').count;
    metrics.pickTime = stats.get('pickObject Time').time + stats.get('pickMultipleObjects Time').time + stats.get('pickObjects Time').time;
    metrics.pickCount = stats.get('Pick Count').count;
    metrics.gpuTime = stats.get('GPU Time').time;
    metrics.cpuTime = stats.get('CPU Time').time;
    metrics.gpuTimePerFrame = stats.get('GPU Time').getAverageTime();
    metrics.cpuTimePerFrame = stats.get('CPU Time').getAverageTime();
    const memoryStats = luma.stats.get('Memory Usage');
    metrics.bufferMemory = memoryStats.get('Buffer Memory').count;
    metrics.textureMemory = memoryStats.get('Texture Memory').count;
    metrics.renderbufferMemory = memoryStats.get('Renderbuffer Memory').count;
    metrics.gpuMemory = memoryStats.get('GPU Memory').count;
  }

}
Deck.defaultProps = defaultProps;
Deck.VERSION = VERSION;
//# sourceMappingURL=deck.js.map