import DataColumn from './data-column';
import assert from '../../utils/assert';
import { createIterable, getAccessorFromBuffer } from '../../utils/iterable-utils';
import { fillArray } from '../../utils/flatten';
import * as range from '../../utils/range';
import { bufferLayoutEqual } from './gl-utils';
import { normalizeTransitionSettings } from './attribute-transition-utils';
export default class Attribute extends DataColumn {
  constructor(device, opts) {
    super(device, opts, {
      startIndices: null,
      lastExternalBuffer: null,
      binaryValue: null,
      binaryAccessor: null,
      needsUpdate: true,
      needsRedraw: false,
      layoutChanged: false,
      updateRanges: range.FULL
    });
    this.constant = false;
    this.settings.update = opts.update || (opts.accessor ? this._autoUpdater : undefined);
    Object.seal(this.settings);
    Object.seal(this.state);

    this._validateAttributeUpdaters();
  }

  get startIndices() {
    return this.state.startIndices;
  }

  set startIndices(layout) {
    this.state.startIndices = layout;
  }

  needsUpdate() {
    return this.state.needsUpdate;
  }

  needsRedraw({
    clearChangedFlags = false
  } = {}) {
    const needsRedraw = this.state.needsRedraw;
    this.state.needsRedraw = needsRedraw && !clearChangedFlags;
    return needsRedraw;
  }

  layoutChanged() {
    return this.state.layoutChanged;
  }

  setAccessor(accessor) {
    var _this$state;

    (_this$state = this.state).layoutChanged || (_this$state.layoutChanged = !bufferLayoutEqual(accessor, this.getAccessor()));
    super.setAccessor(accessor);
  }

  getUpdateTriggers() {
    const {
      accessor
    } = this.settings;
    return [this.id].concat(typeof accessor !== 'function' && accessor || []);
  }

  supportsTransition() {
    return Boolean(this.settings.transition);
  }

  getTransitionSetting(opts) {
    if (!opts || !this.supportsTransition()) {
      return null;
    }

    const {
      accessor
    } = this.settings;
    const layerSettings = this.settings.transition;
    const userSettings = Array.isArray(accessor) ? opts[accessor.find(a => opts[a])] : opts[accessor];
    return normalizeTransitionSettings(userSettings, layerSettings);
  }

  setNeedsUpdate(reason = this.id, dataRange) {
    this.state.needsUpdate = this.state.needsUpdate || reason;
    this.setNeedsRedraw(reason);

    if (dataRange) {
      const {
        startRow = 0,
        endRow = Infinity
      } = dataRange;
      this.state.updateRanges = range.add(this.state.updateRanges, [startRow, endRow]);
    } else {
      this.state.updateRanges = range.FULL;
    }
  }

  clearNeedsUpdate() {
    this.state.needsUpdate = false;
    this.state.updateRanges = range.EMPTY;
  }

  setNeedsRedraw(reason = this.id) {
    this.state.needsRedraw = this.state.needsRedraw || reason;
  }

  allocate(numInstances) {
    const {
      state,
      settings
    } = this;

    if (settings.noAlloc) {
      return false;
    }

    if (settings.update) {
      super.allocate(numInstances, state.updateRanges !== range.FULL);
      return true;
    }

    return false;
  }

  updateBuffer({
    numInstances,
    data,
    props,
    context
  }) {
    if (!this.needsUpdate()) {
      return false;
    }

    const {
      state: {
        updateRanges
      },
      settings: {
        update,
        noAlloc
      }
    } = this;
    let updated = true;

    if (update) {
      for (const [startRow, endRow] of updateRanges) {
        update.call(context, this, {
          data,
          startRow,
          endRow,
          props,
          numInstances
        });
      }

      if (!this.value) {} else if (this.constant || this.buffer.byteLength < this.value.byteLength + this.byteOffset) {
        this.setData({
          value: this.value,
          constant: this.constant
        });
        this.constant = false;
      } else {
        for (const [startRow, endRow] of updateRanges) {
          const startOffset = Number.isFinite(startRow) ? this.getVertexOffset(startRow) : 0;
          const endOffset = Number.isFinite(endRow) ? this.getVertexOffset(endRow) : noAlloc || !Number.isFinite(numInstances) ? this.value.length : numInstances * this.size;
          super.updateSubBuffer({
            startOffset,
            endOffset
          });
        }
      }

      this._checkAttributeArray();
    } else {
      updated = false;
    }

    this.clearNeedsUpdate();
    this.setNeedsRedraw();
    return updated;
  }

  setConstantValue(value) {
    if (value === undefined || typeof value === 'function') {
      return false;
    }

    const hasChanged = this.setData({
      constant: true,
      value
    });

    if (hasChanged) {
      this.setNeedsRedraw();
    }

    this.clearNeedsUpdate();
    return true;
  }

  setExternalBuffer(buffer) {
    const {
      state
    } = this;

    if (!buffer) {
      state.lastExternalBuffer = null;
      return false;
    }

    this.clearNeedsUpdate();

    if (state.lastExternalBuffer === buffer) {
      return true;
    }

    state.lastExternalBuffer = buffer;
    this.setNeedsRedraw();
    this.setData(buffer);
    return true;
  }

  setBinaryValue(buffer, startIndices = null) {
    const {
      state,
      settings
    } = this;

    if (!buffer) {
      state.binaryValue = null;
      state.binaryAccessor = null;
      return false;
    }

    if (settings.noAlloc) {
      return false;
    }

    if (state.binaryValue === buffer) {
      this.clearNeedsUpdate();
      return true;
    }

    state.binaryValue = buffer;
    this.setNeedsRedraw();
    const needsUpdate = settings.transform || startIndices !== this.startIndices;

    if (needsUpdate) {
      if (ArrayBuffer.isView(buffer)) {
        buffer = {
          value: buffer
        };
      }

      const binaryValue = buffer;
      assert(ArrayBuffer.isView(binaryValue.value), `invalid ${settings.accessor}`);
      const needsNormalize = Boolean(binaryValue.size) && binaryValue.size !== this.size;
      state.binaryAccessor = getAccessorFromBuffer(binaryValue.value, {
        size: binaryValue.size || this.size,
        stride: binaryValue.stride,
        offset: binaryValue.offset,
        startIndices: startIndices,
        nested: needsNormalize
      });
      return false;
    }

    this.clearNeedsUpdate();
    this.setData(buffer);
    return true;
  }

  getVertexOffset(row) {
    const {
      startIndices
    } = this;
    const vertexIndex = startIndices ? row < startIndices.length ? startIndices[row] : this.numInstances : row;
    return vertexIndex * this.size;
  }

  getValue() {
    const shaderAttributeDefs = this.settings.shaderAttributes;
    const result = super.getValue();

    if (!shaderAttributeDefs) {
      return result;
    }

    for (const shaderAttributeName in shaderAttributeDefs) {
      Object.assign(result, super.getValue(shaderAttributeName, shaderAttributeDefs[shaderAttributeName]));
    }

    return result;
  }

  getBufferLayout() {
    this.state.layoutChanged = false;
    const shaderAttributeDefs = this.settings.shaderAttributes;
    const result = super.getBufferLayout();

    if (!shaderAttributeDefs) {
      return result;
    }

    for (const shaderAttributeName in shaderAttributeDefs) {
      const map = super.getBufferLayout(shaderAttributeName, shaderAttributeDefs[shaderAttributeName]);
      result.attributes.push(...map.attributes);
    }

    return result;
  }

  _autoUpdater(attribute, {
    data,
    startRow,
    endRow,
    props,
    numInstances
  }) {
    if (attribute.constant) {
      return;
    }

    const {
      settings,
      state,
      value,
      size,
      startIndices
    } = attribute;
    const {
      accessor,
      transform
    } = settings;
    const accessorFunc = state.binaryAccessor || (typeof accessor === 'function' ? accessor : props[accessor]);
    assert(typeof accessorFunc === 'function', `accessor "${accessor}" is not a function`);
    let i = attribute.getVertexOffset(startRow);
    const {
      iterable,
      objectInfo
    } = createIterable(data, startRow, endRow);

    for (const object of iterable) {
      objectInfo.index++;
      let objectValue = accessorFunc(object, objectInfo);

      if (transform) {
        objectValue = transform.call(this, objectValue);
      }

      if (startIndices) {
        const numVertices = (objectInfo.index < startIndices.length - 1 ? startIndices[objectInfo.index + 1] : numInstances) - startIndices[objectInfo.index];

        if (objectValue && Array.isArray(objectValue[0])) {
          let startIndex = i;

          for (const item of objectValue) {
            attribute._normalizeValue(item, value, startIndex);

            startIndex += size;
          }
        } else if (objectValue && objectValue.length > size) {
          value.set(objectValue, i);
        } else {
          attribute._normalizeValue(objectValue, objectInfo.target, 0);

          fillArray({
            target: value,
            source: objectInfo.target,
            start: i,
            count: numVertices
          });
        }

        i += numVertices * size;
      } else {
        attribute._normalizeValue(objectValue, value, i);

        i += size;
      }
    }
  }

  _validateAttributeUpdaters() {
    const {
      settings
    } = this;
    const hasUpdater = settings.noAlloc || typeof settings.update === 'function';

    if (!hasUpdater) {
      throw new Error(`Attribute ${this.id} missing update or accessor`);
    }
  }

  _checkAttributeArray() {
    const {
      value
    } = this;
    const limit = Math.min(4, this.size);

    if (value && value.length >= limit) {
      let valid = true;

      switch (limit) {
        case 4:
          valid = valid && Number.isFinite(value[3]);

        case 3:
          valid = valid && Number.isFinite(value[2]);

        case 2:
          valid = valid && Number.isFinite(value[1]);

        case 1:
          valid = valid && Number.isFinite(value[0]);
          break;

        default:
          valid = false;
      }

      if (!valid) {
        throw new Error(`Illegal attribute generated for ${this.id}`);
      }
    }
  }

}
//# sourceMappingURL=attribute.js.map