import Attribute from './attribute';
import log from '../../utils/log';
import memoize from '../../utils/memoize';
import { mergeBounds } from '../../utils/math-utils';
import debug from '../../debug';
import AttributeTransitionManager from './attribute-transition-manager';
const TRACE_INVALIDATE = 'attributeManager.invalidate';
const TRACE_UPDATE_START = 'attributeManager.updateStart';
const TRACE_UPDATE_END = 'attributeManager.updateEnd';
const TRACE_ATTRIBUTE_UPDATE_START = 'attribute.updateStart';
const TRACE_ATTRIBUTE_ALLOCATE = 'attribute.allocate';
const TRACE_ATTRIBUTE_UPDATE_END = 'attribute.updateEnd';
export default class AttributeManager {
  constructor(device, {
    id = 'attribute-manager',
    stats,
    timeline
  } = {}) {
    this.id = void 0;
    this.device = void 0;
    this.attributes = void 0;
    this.updateTriggers = void 0;
    this.needsRedraw = void 0;
    this.userData = void 0;
    this.stats = void 0;
    this.attributeTransitionManager = void 0;
    this.mergeBoundsMemoized = memoize(mergeBounds);
    this.id = id;
    this.device = device;
    this.attributes = {};
    this.updateTriggers = {};
    this.needsRedraw = true;
    this.userData = {};
    this.stats = stats;
    this.attributeTransitionManager = new AttributeTransitionManager(device, {
      id: `${id}-transitions`,
      timeline
    });
    Object.seal(this);
  }

  finalize() {
    for (const attributeName in this.attributes) {
      this.attributes[attributeName].delete();
    }

    this.attributeTransitionManager.finalize();
  }

  getNeedsRedraw(opts = {
    clearRedrawFlags: false
  }) {
    const redraw = this.needsRedraw;
    this.needsRedraw = this.needsRedraw && !opts.clearRedrawFlags;
    return redraw && this.id;
  }

  setNeedsRedraw() {
    this.needsRedraw = true;
  }

  add(attributes) {
    this._add(attributes);
  }

  addInstanced(attributes) {
    this._add(attributes, {
      instanced: 1
    });
  }

  remove(attributeNameArray) {
    for (const name of attributeNameArray) {
      if (this.attributes[name] !== undefined) {
        this.attributes[name].delete();
        delete this.attributes[name];
      }
    }
  }

  invalidate(triggerName, dataRange) {
    const invalidatedAttributes = this._invalidateTrigger(triggerName, dataRange);

    debug(TRACE_INVALIDATE, this, triggerName, invalidatedAttributes);
  }

  invalidateAll(dataRange) {
    for (const attributeName in this.attributes) {
      this.attributes[attributeName].setNeedsUpdate(attributeName, dataRange);
    }

    debug(TRACE_INVALIDATE, this, 'all');
  }

  update({
    data,
    numInstances,
    startIndices = null,
    transitions,
    props = {},
    buffers = {},
    context = {}
  }) {
    let updated = false;
    debug(TRACE_UPDATE_START, this);

    if (this.stats) {
      this.stats.get('Update Attributes').timeStart();
    }

    for (const attributeName in this.attributes) {
      const attribute = this.attributes[attributeName];
      const accessorName = attribute.settings.accessor;
      attribute.startIndices = startIndices;
      attribute.numInstances = numInstances;

      if (props[attributeName]) {
        log.removed(`props.${attributeName}`, `data.attributes.${attributeName}`)();
      }

      if (attribute.setExternalBuffer(buffers[attributeName])) {} else if (attribute.setBinaryValue(typeof accessorName === 'string' ? buffers[accessorName] : undefined, data.startIndices)) {} else if (typeof accessorName === 'string' && !buffers[accessorName] && attribute.setConstantValue(props[accessorName])) {} else if (attribute.needsUpdate()) {
        updated = true;

        this._updateAttribute({
          attribute,
          numInstances,
          data,
          props,
          context
        });
      }

      this.needsRedraw = this.needsRedraw || attribute.needsRedraw();
    }

    if (updated) {
      debug(TRACE_UPDATE_END, this, numInstances);
    }

    if (this.stats) {
      this.stats.get('Update Attributes').timeEnd();
    }

    this.attributeTransitionManager.update({
      attributes: this.attributes,
      numInstances,
      transitions
    });
  }

  updateTransition() {
    const {
      attributeTransitionManager
    } = this;
    const transitionUpdated = attributeTransitionManager.run();
    this.needsRedraw = this.needsRedraw || transitionUpdated;
    return transitionUpdated;
  }

  getAttributes() {
    return this.attributes;
  }

  getBounds(attributeNames) {
    const bounds = attributeNames.map(attributeName => {
      var _this$attributes$attr;

      return (_this$attributes$attr = this.attributes[attributeName]) === null || _this$attributes$attr === void 0 ? void 0 : _this$attributes$attr.getBounds();
    });
    return this.mergeBoundsMemoized(bounds);
  }

  getChangedAttributes(opts = {
    clearChangedFlags: false
  }) {
    const {
      attributes,
      attributeTransitionManager
    } = this;
    const changedAttributes = { ...attributeTransitionManager.getAttributes()
    };

    for (const attributeName in attributes) {
      const attribute = attributes[attributeName];

      if (attribute.needsRedraw(opts) && !attributeTransitionManager.hasAttribute(attributeName)) {
        changedAttributes[attributeName] = attribute;
      }
    }

    return changedAttributes;
  }

  getBufferLayouts(attributes, excludeAttributes = {}) {
    if (!attributes) {
      attributes = this.getAttributes();
    }

    const bufferMaps = [];

    for (const attributeName in attributes) {
      if (!excludeAttributes[attributeName]) {
        bufferMaps.push(attributes[attributeName].getBufferLayout());
      }
    }

    return bufferMaps;
  }

  _add(attributes, extraProps = {}) {
    for (const attributeName in attributes) {
      const attribute = attributes[attributeName];
      this.attributes[attributeName] = this._createAttribute(attributeName, attribute, extraProps);
    }

    this._mapUpdateTriggersToAttributes();
  }

  _createAttribute(name, attribute, extraProps) {
    const props = { ...attribute,
      id: name,
      size: attribute.isIndexed && 1 || attribute.size || 1,
      divisor: extraProps.instanced ? 1 : attribute.divisor || 0
    };
    return new Attribute(this.device, props);
  }

  _mapUpdateTriggersToAttributes() {
    const triggers = {};

    for (const attributeName in this.attributes) {
      const attribute = this.attributes[attributeName];
      attribute.getUpdateTriggers().forEach(triggerName => {
        if (!triggers[triggerName]) {
          triggers[triggerName] = [];
        }

        triggers[triggerName].push(attributeName);
      });
    }

    this.updateTriggers = triggers;
  }

  _invalidateTrigger(triggerName, dataRange) {
    const {
      attributes,
      updateTriggers
    } = this;
    const invalidatedAttributes = updateTriggers[triggerName];

    if (invalidatedAttributes) {
      invalidatedAttributes.forEach(name => {
        const attribute = attributes[name];

        if (attribute) {
          attribute.setNeedsUpdate(attribute.id, dataRange);
        }
      });
    }

    return invalidatedAttributes;
  }

  _updateAttribute(opts) {
    const {
      attribute,
      numInstances
    } = opts;
    debug(TRACE_ATTRIBUTE_UPDATE_START, attribute);

    if (attribute.constant) {
      attribute.setConstantValue(attribute.value);
      return;
    }

    if (attribute.allocate(numInstances)) {
      debug(TRACE_ATTRIBUTE_ALLOCATE, attribute, numInstances);
    }

    const updated = attribute.updateBuffer(opts);

    if (updated) {
      this.needsRedraw = true;
      debug(TRACE_ATTRIBUTE_UPDATE_END, attribute, numInstances);
    }
  }

}
//# sourceMappingURL=attribute-manager.js.map