import { Buffer } from '@luma.gl/core';
import { GL } from '@luma.gl/constants';
import { glArrayFromType, getBufferAttributeLayout, getStride, getGLTypeFromTypedArray } from './gl-utils';
import typedArrayManager from '../../utils/typed-array-manager';
import { toDoublePrecisionArray } from '../../utils/math-utils';
import log from '../../utils/log';

function resolveShaderAttribute(baseAccessor, shaderAttributeOptions) {
  if (shaderAttributeOptions.offset) {
    log.removed('shaderAttribute.offset', 'vertexOffset, elementOffset')();
  }

  const stride = getStride(baseAccessor);
  const vertexOffset = shaderAttributeOptions.vertexOffset !== undefined ? shaderAttributeOptions.vertexOffset : baseAccessor.vertexOffset || 0;
  const elementOffset = shaderAttributeOptions.elementOffset || 0;
  const offset = vertexOffset * stride + elementOffset * baseAccessor.bytesPerElement + (baseAccessor.offset || 0);
  return { ...shaderAttributeOptions,
    offset,
    stride
  };
}

function resolveDoublePrecisionShaderAttributes(baseAccessor, shaderAttributeOptions) {
  const resolvedOptions = resolveShaderAttribute(baseAccessor, shaderAttributeOptions);
  return {
    high: resolvedOptions,
    low: { ...resolvedOptions,
      offset: resolvedOptions.offset + baseAccessor.size * 4
    }
  };
}

export default class DataColumn {
  constructor(device, opts, state) {
    this.device = void 0;
    this.id = void 0;
    this.size = void 0;
    this.settings = void 0;
    this.value = void 0;
    this.doublePrecision = void 0;
    this._buffer = void 0;
    this.state = void 0;
    this.device = device;
    this.id = opts.id || '';
    this.size = opts.size || 1;
    const logicalType = opts.logicalType || opts.type;
    const doublePrecision = logicalType === 5130;
    let {
      defaultValue
    } = opts;
    defaultValue = Number.isFinite(defaultValue) ? [defaultValue] : defaultValue || new Array(this.size).fill(0);
    let bufferType;

    if (doublePrecision) {
      bufferType = 5126;
    } else if (!logicalType && opts.isIndexed) {
      bufferType = device.features.has('index-uint32-webgl1') ? 5125 : 5123;
    } else {
      bufferType = logicalType || 5126;
    }

    let defaultType = glArrayFromType(logicalType || bufferType || 5126);
    this.doublePrecision = doublePrecision;

    if (doublePrecision && opts.fp64 === false) {
      defaultType = Float32Array;
    }

    this.value = null;
    this.settings = { ...opts,
      defaultType,
      defaultValue: defaultValue,
      logicalType,
      type: bufferType,
      size: this.size,
      bytesPerElement: defaultType.BYTES_PER_ELEMENT
    };
    this.state = { ...state,
      externalBuffer: null,
      bufferAccessor: this.settings,
      allocatedValue: null,
      numInstances: 0,
      bounds: null,
      constant: false
    };
    this._buffer = this._createBuffer(0);
  }

  get isConstant() {
    return this.state.constant;
  }

  get buffer() {
    return this._buffer;
  }

  get byteOffset() {
    const accessor = this.getAccessor();

    if (accessor.vertexOffset) {
      return accessor.vertexOffset * getStride(accessor);
    }

    return 0;
  }

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

  set numInstances(n) {
    this.state.numInstances = n;
  }

  delete() {
    if (this._buffer) {
      this._buffer.delete();

      this._buffer = null;
    }

    typedArrayManager.release(this.state.allocatedValue);
  }

  getBuffer() {
    if (this.state.constant) {
      return null;
    }

    return this.state.externalBuffer || this._buffer;
  }

  getValue(attributeName = this.id, options = null) {
    const result = {};

    if (this.state.constant) {
      const value = this.value;

      if (options) {
        const shaderAttributeDef = resolveShaderAttribute(this.getAccessor(), options);
        const offset = shaderAttributeDef.offset / value.BYTES_PER_ELEMENT;
        const size = shaderAttributeDef.size || this.size;
        result[attributeName] = value.subarray(offset, offset + size);
      } else {
        result[attributeName] = value;
      }
    } else {
      result[attributeName] = this.getBuffer();
    }

    if (this.doublePrecision) {
      if (this.value instanceof Float64Array) {
        result[`${attributeName}64Low`] = result[attributeName];
      } else {
        result[`${attributeName}64Low`] = new Float32Array(this.size);
      }
    }

    return result;
  }

  getBufferLayout(attributeName = this.id, options = null) {
    const accessor = this.getAccessor();
    const attributes = [];
    const result = {
      name: this.id,
      byteStride: getStride(accessor),
      attributes
    };

    if (this.doublePrecision) {
      const doubleShaderAttributeDefs = resolveDoublePrecisionShaderAttributes(accessor, options || {});
      attributes.push(getBufferAttributeLayout(attributeName, { ...accessor,
        ...doubleShaderAttributeDefs.high
      }), getBufferAttributeLayout(`${attributeName}64Low`, { ...accessor,
        ...doubleShaderAttributeDefs.low
      }));
    } else if (options) {
      const shaderAttributeDef = resolveShaderAttribute(accessor, options);
      attributes.push(getBufferAttributeLayout(attributeName, { ...accessor,
        ...shaderAttributeDef
      }));
    } else {
      attributes.push(getBufferAttributeLayout(attributeName, accessor));
    }

    return result;
  }

  setAccessor(accessor) {
    this.state.bufferAccessor = accessor;
  }

  getAccessor() {
    return this.state.bufferAccessor;
  }

  getBounds() {
    if (this.state.bounds) {
      return this.state.bounds;
    }

    let result = null;

    if (this.state.constant && this.value) {
      const min = Array.from(this.value);
      result = [min, min];
    } else {
      const {
        value,
        numInstances,
        size
      } = this;
      const len = numInstances * size;

      if (value && len && value.length >= len) {
        const min = new Array(size).fill(Infinity);
        const max = new Array(size).fill(-Infinity);

        for (let i = 0; i < len;) {
          for (let j = 0; j < size; j++) {
            const v = value[i++];
            if (v < min[j]) min[j] = v;
            if (v > max[j]) max[j] = v;
          }
        }

        result = [min, max];
      }
    }

    this.state.bounds = result;
    return result;
  }

  setData(data) {
    const {
      state
    } = this;
    let opts;

    if (ArrayBuffer.isView(data)) {
      opts = {
        value: data
      };
    } else if (data instanceof Buffer) {
      opts = {
        buffer: data
      };
    } else {
      opts = data;
    }

    const accessor = { ...this.settings,
      ...opts
    };

    if (ArrayBuffer.isView(opts.value)) {
      const is64Bit = this.doublePrecision && opts.value instanceof Float64Array;
      accessor.type = opts.type || (is64Bit ? 5126 : getGLTypeFromTypedArray(opts.value));
      accessor.bytesPerElement = opts.value.BYTES_PER_ELEMENT;
      accessor.stride = getStride(accessor);
    }

    state.bounds = null;

    if (opts.constant) {
      let value = opts.value;
      value = this._normalizeValue(value, [], 0);

      if (this.settings.normalized) {
        value = this.normalizeConstant(value);
      }

      const hasChanged = !state.constant || !this._areValuesEqual(value, this.value);

      if (!hasChanged) {
        return false;
      }

      state.externalBuffer = null;
      state.constant = true;
      this.value = ArrayBuffer.isView(value) ? value : new Float32Array(value);
    } else if (opts.buffer) {
      const buffer = opts.buffer;
      state.externalBuffer = buffer;
      state.constant = false;
      this.value = opts.value || null;
    } else if (opts.value) {
      this._checkExternalBuffer(opts);

      let value = opts.value;
      state.externalBuffer = null;
      state.constant = false;
      this.value = value;
      let {
        buffer
      } = this;
      const stride = getStride(accessor);
      const byteOffset = (accessor.vertexOffset || 0) * stride;

      if (this.doublePrecision && value instanceof Float64Array) {
        value = toDoublePrecisionArray(value, accessor);
      }

      if (this.settings.isIndexed) {
        const ArrayType = this.settings.defaultType;

        if (value.constructor !== ArrayType) {
          value = new ArrayType(value);
        }
      }

      const requiredBufferSize = value.byteLength + byteOffset + stride * 2;

      if (buffer.byteLength < requiredBufferSize) {
        buffer = this._createBuffer(requiredBufferSize);
      }

      buffer.write(value, byteOffset);
    }

    this.setAccessor(accessor);
    return true;
  }

  updateSubBuffer(opts = {}) {
    this.state.bounds = null;
    const value = this.value;
    const {
      startOffset = 0,
      endOffset
    } = opts;
    this.buffer.write(this.doublePrecision && value instanceof Float64Array ? toDoublePrecisionArray(value, {
      size: this.size,
      startIndex: startOffset,
      endIndex: endOffset
    }) : value.subarray(startOffset, endOffset), startOffset * value.BYTES_PER_ELEMENT + this.byteOffset);
  }

  allocate(numInstances, copy = false) {
    const {
      state
    } = this;
    const oldValue = state.allocatedValue;
    const value = typedArrayManager.allocate(oldValue, numInstances + 1, {
      size: this.size,
      type: this.settings.defaultType,
      copy
    });
    this.value = value;
    const {
      byteOffset
    } = this;
    let {
      buffer
    } = this;

    if (buffer.byteLength < value.byteLength + byteOffset) {
      buffer = this._createBuffer(value.byteLength + byteOffset);

      if (copy && oldValue) {
        buffer.write(oldValue instanceof Float64Array ? toDoublePrecisionArray(oldValue, this) : oldValue, byteOffset);
      }
    }

    state.allocatedValue = value;
    state.constant = false;
    state.externalBuffer = null;
    this.setAccessor(this.settings);
    return true;
  }

  _checkExternalBuffer(opts) {
    const {
      value
    } = opts;

    if (!ArrayBuffer.isView(value)) {
      throw new Error(`Attribute ${this.id} value is not TypedArray`);
    }

    const ArrayType = this.settings.defaultType;
    let illegalArrayType = false;

    if (this.doublePrecision) {
      illegalArrayType = value.BYTES_PER_ELEMENT < 4;
    }

    if (illegalArrayType) {
      throw new Error(`Attribute ${this.id} does not support ${value.constructor.name}`);
    }

    if (!(value instanceof ArrayType) && this.settings.normalized && !('normalized' in opts)) {
      log.warn(`Attribute ${this.id} is normalized`)();
    }
  }

  normalizeConstant(value) {
    switch (this.settings.type) {
      case 5120:
        return new Float32Array(value).map(x => (x + 128) / 255 * 2 - 1);

      case 5122:
        return new Float32Array(value).map(x => (x + 32768) / 65535 * 2 - 1);

      case 5121:
        return new Float32Array(value).map(x => x / 255);

      case 5123:
        return new Float32Array(value).map(x => x / 65535);

      default:
        return value;
    }
  }

  _normalizeValue(value, out, start) {
    const {
      defaultValue,
      size
    } = this.settings;

    if (Number.isFinite(value)) {
      out[start] = value;
      return out;
    }

    if (!value) {
      let i = size;

      while (--i >= 0) {
        out[start + i] = defaultValue[i];
      }

      return out;
    }

    switch (size) {
      case 4:
        out[start + 3] = Number.isFinite(value[3]) ? value[3] : defaultValue[3];

      case 3:
        out[start + 2] = Number.isFinite(value[2]) ? value[2] : defaultValue[2];

      case 2:
        out[start + 1] = Number.isFinite(value[1]) ? value[1] : defaultValue[1];

      case 1:
        out[start + 0] = Number.isFinite(value[0]) ? value[0] : defaultValue[0];
        break;

      default:
        let i = size;

        while (--i >= 0) {
          out[start + i] = Number.isFinite(value[i]) ? value[i] : defaultValue[i];
        }

    }

    return out;
  }

  _areValuesEqual(value1, value2) {
    if (!value1 || !value2) {
      return false;
    }

    const {
      size
    } = this;

    for (let i = 0; i < size; i++) {
      if (value1[i] !== value2[i]) {
        return false;
      }
    }

    return true;
  }

  _createBuffer(byteLength) {
    var _this$_buffer;

    if (this._buffer) {
      this._buffer.destroy();
    }

    const {
      isIndexed,
      type
    } = this.settings;
    this._buffer = this.device.createBuffer({ ...((_this$_buffer = this._buffer) === null || _this$_buffer === void 0 ? void 0 : _this$_buffer.props),
      id: this.id,
      usage: isIndexed ? Buffer.INDEX : Buffer.VERTEX,
      indexType: isIndexed ? type === 5123 ? 'uint16' : 'uint32' : undefined,
      byteLength
    });
    return this._buffer;
  }

}
//# sourceMappingURL=data-column.js.map