"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const LWO2Parser = require("./LWO2Parser.cjs");
const LWO3Parser = require("./LWO3Parser.cjs");
class IFFParser {
  constructor() {
    this.debugger = new Debugger();
  }
  parse(buffer) {
    this.reader = new DataViewReader(buffer);
    this.tree = {
      materials: {},
      layers: [],
      tags: [],
      textures: []
    };
    this.currentLayer = this.tree;
    this.currentForm = this.tree;
    this.parseTopForm();
    if (this.tree.format === void 0)
      return;
    if (this.tree.format === "LWO2") {
      this.parser = new LWO2Parser.LWO2Parser(this);
      while (!this.reader.endOfFile())
        this.parser.parseBlock();
    } else if (this.tree.format === "LWO3") {
      this.parser = new LWO3Parser.LWO3Parser(this);
      while (!this.reader.endOfFile())
        this.parser.parseBlock();
    }
    this.debugger.offset = this.reader.offset;
    this.debugger.closeForms();
    return this.tree;
  }
  parseTopForm() {
    this.debugger.offset = this.reader.offset;
    var topForm = this.reader.getIDTag();
    if (topForm !== "FORM") {
      console.warn("LWOLoader: Top-level FORM missing.");
      return;
    }
    var length = this.reader.getUint32();
    this.debugger.dataOffset = this.reader.offset;
    this.debugger.length = length;
    var type = this.reader.getIDTag();
    if (type === "LWO2") {
      this.tree.format = type;
    } else if (type === "LWO3") {
      this.tree.format = type;
    }
    this.debugger.node = 0;
    this.debugger.nodeID = type;
    this.debugger.log();
    return;
  }
  ///
  // FORM PARSING METHODS
  ///
  // Forms are organisational and can contain any number of sub chunks and sub forms
  // FORM ::= 'FORM'[ID4], length[U4], type[ID4], ( chunk[CHUNK] | form[FORM] ) * }
  parseForm(length) {
    var type = this.reader.getIDTag();
    switch (type) {
      case "ISEQ":
      case "ANIM":
      case "STCC":
      case "VPVL":
      case "VPRM":
      case "NROT":
      case "WRPW":
      case "WRPH":
      case "FUNC":
      case "FALL":
      case "OPAC":
      case "GRAD":
      case "ENVS":
      case "VMOP":
      case "VMBG":
      case "OMAX":
      case "STEX":
      case "CKBG":
      case "CKEY":
      case "VMLA":
      case "VMLB":
        this.debugger.skipped = true;
        this.skipForm(length);
        break;
      case "META":
      case "NNDS":
      case "NODS":
      case "NDTA":
      case "ADAT":
      case "AOVS":
      case "BLOK":
      case "IBGC":
      case "IOPC":
      case "IIMG":
      case "TXTR":
        this.debugger.length = 4;
        this.debugger.skipped = true;
        break;
      case "IFAL":
      case "ISCL":
      case "IPOS":
      case "IROT":
      case "IBMP":
      case "IUTD":
      case "IVTD":
        this.parseTextureNodeAttribute(type);
        break;
      case "ENVL":
        this.parseEnvelope(length);
        break;
      case "CLIP":
        if (this.tree.format === "LWO2") {
          this.parseForm(length);
        } else {
          this.parseClip(length);
        }
        break;
      case "STIL":
        this.parseImage();
        break;
      case "XREF":
        this.reader.skip(8);
        this.currentForm.referenceTexture = {
          index: this.reader.getUint32(),
          refName: this.reader.getString()
          // internal unique ref
        };
        break;
      case "IMST":
        this.parseImageStateForm(length);
        break;
      case "SURF":
        this.parseSurfaceForm(length);
        break;
      case "VALU":
        this.parseValueForm(length);
        break;
      case "NTAG":
        this.parseSubNode(length);
        break;
      case "ATTR":
      case "SATR":
        this.setupForm("attributes", length);
        break;
      case "NCON":
        this.parseConnections(length);
        break;
      case "SSHA":
        this.parentForm = this.currentForm;
        this.currentForm = this.currentSurface;
        this.setupForm("surfaceShader", length);
        break;
      case "SSHD":
        this.setupForm("surfaceShaderData", length);
        break;
      case "ENTR":
        this.parseEntryForm(length);
        break;
      case "IMAP":
        this.parseImageMap(length);
        break;
      case "TAMP":
        this.parseXVAL("amplitude", length);
        break;
      case "TMAP":
        this.setupForm("textureMap", length);
        break;
      case "CNTR":
        this.parseXVAL3("center", length);
        break;
      case "SIZE":
        this.parseXVAL3("scale", length);
        break;
      case "ROTA":
        this.parseXVAL3("rotation", length);
        break;
      default:
        this.parseUnknownForm(type, length);
    }
    this.debugger.node = 0;
    this.debugger.nodeID = type;
    this.debugger.log();
  }
  setupForm(type, length) {
    if (!this.currentForm)
      this.currentForm = this.currentNode;
    this.currentFormEnd = this.reader.offset + length;
    this.parentForm = this.currentForm;
    if (!this.currentForm[type]) {
      this.currentForm[type] = {};
      this.currentForm = this.currentForm[type];
    } else {
      console.warn("LWOLoader: form already exists on parent: ", type, this.currentForm);
      this.currentForm = this.currentForm[type];
    }
  }
  skipForm(length) {
    this.reader.skip(length - 4);
  }
  parseUnknownForm(type, length) {
    console.warn("LWOLoader: unknown FORM encountered: " + type, length);
    printBuffer(this.reader.dv.buffer, this.reader.offset, length - 4);
    this.reader.skip(length - 4);
  }
  parseSurfaceForm(length) {
    this.reader.skip(8);
    var name = this.reader.getString();
    var surface = {
      attributes: {},
      // LWO2 style non-node attributes will go here
      connections: {},
      name,
      inputName: name,
      nodes: {},
      source: this.reader.getString()
    };
    this.tree.materials[name] = surface;
    this.currentSurface = surface;
    this.parentForm = this.tree.materials;
    this.currentForm = surface;
    this.currentFormEnd = this.reader.offset + length;
  }
  parseSurfaceLwo2(length) {
    var name = this.reader.getString();
    var surface = {
      attributes: {},
      // LWO2 style non-node attributes will go here
      connections: {},
      name,
      nodes: {},
      source: this.reader.getString()
    };
    this.tree.materials[name] = surface;
    this.currentSurface = surface;
    this.parentForm = this.tree.materials;
    this.currentForm = surface;
    this.currentFormEnd = this.reader.offset + length;
  }
  parseSubNode(length) {
    this.reader.skip(8);
    var name = this.reader.getString();
    var node = {
      name
    };
    this.currentForm = node;
    this.currentNode = node;
    this.currentFormEnd = this.reader.offset + length;
  }
  // collect attributes from all nodes at the top level of a surface
  parseConnections(length) {
    this.currentFormEnd = this.reader.offset + length;
    this.parentForm = this.currentForm;
    this.currentForm = this.currentSurface.connections;
  }
  // surface node attribute data, e.g. specular, roughness etc
  parseEntryForm(length) {
    this.reader.skip(8);
    var name = this.reader.getString();
    this.currentForm = this.currentNode.attributes;
    this.setupForm(name, length);
  }
  // parse values from material - doesn't match up to other LWO3 data types
  // sub form of entry form
  parseValueForm() {
    this.reader.skip(8);
    var valueType = this.reader.getString();
    if (valueType === "double") {
      this.currentForm.value = this.reader.getUint64();
    } else if (valueType === "int") {
      this.currentForm.value = this.reader.getUint32();
    } else if (valueType === "vparam") {
      this.reader.skip(24);
      this.currentForm.value = this.reader.getFloat64();
    } else if (valueType === "vparam3") {
      this.reader.skip(24);
      this.currentForm.value = this.reader.getFloat64Array(3);
    }
  }
  // holds various data about texture node image state
  // Data other thanmipMapLevel unknown
  parseImageStateForm() {
    this.reader.skip(8);
    this.currentForm.mipMapLevel = this.reader.getFloat32();
  }
  // LWO2 style image data node OR LWO3 textures defined at top level in editor (not as SURF node)
  parseImageMap(length) {
    this.currentFormEnd = this.reader.offset + length;
    this.parentForm = this.currentForm;
    if (!this.currentForm.maps)
      this.currentForm.maps = [];
    var map = {};
    this.currentForm.maps.push(map);
    this.currentForm = map;
    this.reader.skip(10);
  }
  parseTextureNodeAttribute(type) {
    this.reader.skip(28);
    this.reader.skip(20);
    switch (type) {
      case "ISCL":
        this.currentNode.scale = this.reader.getFloat32Array(3);
        break;
      case "IPOS":
        this.currentNode.position = this.reader.getFloat32Array(3);
        break;
      case "IROT":
        this.currentNode.rotation = this.reader.getFloat32Array(3);
        break;
      case "IFAL":
        this.currentNode.falloff = this.reader.getFloat32Array(3);
        break;
      case "IBMP":
        this.currentNode.amplitude = this.reader.getFloat32();
        break;
      case "IUTD":
        this.currentNode.uTiles = this.reader.getFloat32();
        break;
      case "IVTD":
        this.currentNode.vTiles = this.reader.getFloat32();
        break;
    }
    this.reader.skip(2);
  }
  // ENVL forms are currently ignored
  parseEnvelope(length) {
    this.reader.skip(length - 4);
  }
  ///
  // CHUNK PARSING METHODS
  ///
  // clips can either be defined inside a surface node, or at the top
  // level and they have a different format in each case
  parseClip(length) {
    var tag = this.reader.getIDTag();
    if (tag === "FORM") {
      this.reader.skip(16);
      this.currentNode.fileName = this.reader.getString();
      return;
    }
    this.reader.setOffset(this.reader.offset - 4);
    this.currentFormEnd = this.reader.offset + length;
    this.parentForm = this.currentForm;
    this.reader.skip(8);
    var texture = {
      index: this.reader.getUint32()
    };
    this.tree.textures.push(texture);
    this.currentForm = texture;
  }
  parseClipLwo2(length) {
    var texture = {
      index: this.reader.getUint32(),
      fileName: ""
    };
    while (true) {
      var tag = this.reader.getIDTag();
      var n_length = this.reader.getUint16();
      if (tag === "STIL") {
        texture.fileName = this.reader.getString();
        break;
      }
      if (n_length >= length) {
        break;
      }
    }
    this.tree.textures.push(texture);
    this.currentForm = texture;
  }
  parseImage() {
    this.reader.skip(8);
    this.currentForm.fileName = this.reader.getString();
  }
  parseXVAL(type, length) {
    var endOffset = this.reader.offset + length - 4;
    this.reader.skip(8);
    this.currentForm[type] = this.reader.getFloat32();
    this.reader.setOffset(endOffset);
  }
  parseXVAL3(type, length) {
    var endOffset = this.reader.offset + length - 4;
    this.reader.skip(8);
    this.currentForm[type] = {
      x: this.reader.getFloat32(),
      y: this.reader.getFloat32(),
      z: this.reader.getFloat32()
    };
    this.reader.setOffset(endOffset);
  }
  // Tags associated with an object
  // OTAG { type[ID4], tag-string[S0] }
  parseObjectTag() {
    if (!this.tree.objectTags)
      this.tree.objectTags = {};
    this.tree.objectTags[this.reader.getIDTag()] = {
      tagString: this.reader.getString()
    };
  }
  // Signals the start of a new layer. All the data chunks which follow will be included in this layer until another layer chunk is encountered.
  // LAYR: number[U2], flags[U2], pivot[VEC12], name[S0], parent[U2]
  parseLayer(length) {
    var layer = {
      number: this.reader.getUint16(),
      flags: this.reader.getUint16(),
      // If the least significant bit of flags is set, the layer is hidden.
      pivot: this.reader.getFloat32Array(3),
      // Note: this seems to be superflous, as the geometry is translated when pivot is present
      name: this.reader.getString()
    };
    this.tree.layers.push(layer);
    this.currentLayer = layer;
    var parsedLength = 16 + stringOffset(this.currentLayer.name);
    this.currentLayer.parent = parsedLength < length ? this.reader.getUint16() : -1;
  }
  // VEC12 * ( F4 + F4 + F4 ) array of x,y,z vectors
  // Converting from left to right handed coordinate system:
  // x -> -x and switch material FrontSide -> BackSide
  parsePoints(length) {
    this.currentPoints = [];
    for (var i = 0; i < length / 4; i += 3) {
      this.currentPoints.push(this.reader.getFloat32(), this.reader.getFloat32(), -this.reader.getFloat32());
    }
  }
  // parse VMAP or VMAD
  // Associates a set of floating-point vectors with a set of points.
  // VMAP: { type[ID4], dimension[U2], name[S0], ( vert[VX], value[F4] # dimension ) * }
  // VMAD Associates a set of floating-point vectors with the vertices of specific polygons.
  // Similar to VMAP UVs, but associates with polygon vertices rather than points
  // to solve to problem of UV seams:  VMAD chunks are paired with VMAPs of the same name,
  // if they exist. The vector values in the VMAD will then replace those in the
  // corresponding VMAP, but only for calculations involving the specified polygons.
  // VMAD { type[ID4], dimension[U2], name[S0], ( vert[VX], poly[VX], value[F4] # dimension ) * }
  parseVertexMapping(length, discontinuous) {
    var finalOffset = this.reader.offset + length;
    var channelName = this.reader.getString();
    if (this.reader.offset === finalOffset) {
      this.currentForm.UVChannel = channelName;
      return;
    }
    this.reader.setOffset(this.reader.offset - stringOffset(channelName));
    var type = this.reader.getIDTag();
    this.reader.getUint16();
    var name = this.reader.getString();
    var remainingLength = length - 6 - stringOffset(name);
    switch (type) {
      case "TXUV":
        this.parseUVMapping(name, finalOffset, discontinuous);
        break;
      case "MORF":
      case "SPOT":
        this.parseMorphTargets(name, finalOffset, type);
        break;
      case "APSL":
      case "NORM":
      case "WGHT":
      case "MNVW":
      case "PICK":
      case "RGB ":
      case "RGBA":
        this.reader.skip(remainingLength);
        break;
      default:
        console.warn("LWOLoader: unknown vertex map type: " + type);
        this.reader.skip(remainingLength);
    }
  }
  parseUVMapping(name, finalOffset, discontinuous) {
    var uvIndices = [];
    var polyIndices = [];
    var uvs = [];
    while (this.reader.offset < finalOffset) {
      uvIndices.push(this.reader.getVariableLengthIndex());
      if (discontinuous)
        polyIndices.push(this.reader.getVariableLengthIndex());
      uvs.push(this.reader.getFloat32(), this.reader.getFloat32());
    }
    if (discontinuous) {
      if (!this.currentLayer.discontinuousUVs)
        this.currentLayer.discontinuousUVs = {};
      this.currentLayer.discontinuousUVs[name] = {
        uvIndices,
        polyIndices,
        uvs
      };
    } else {
      if (!this.currentLayer.uvs)
        this.currentLayer.uvs = {};
      this.currentLayer.uvs[name] = {
        uvIndices,
        uvs
      };
    }
  }
  parseMorphTargets(name, finalOffset, type) {
    var indices = [];
    var points = [];
    type = type === "MORF" ? "relative" : "absolute";
    while (this.reader.offset < finalOffset) {
      indices.push(this.reader.getVariableLengthIndex());
      points.push(this.reader.getFloat32(), this.reader.getFloat32(), -this.reader.getFloat32());
    }
    if (!this.currentLayer.morphTargets)
      this.currentLayer.morphTargets = {};
    this.currentLayer.morphTargets[name] = {
      indices,
      points,
      type
    };
  }
  // A list of polygons for the current layer.
  // POLS { type[ID4], ( numvert+flags[U2], vert[VX] # numvert ) * }
  parsePolygonList(length) {
    var finalOffset = this.reader.offset + length;
    var type = this.reader.getIDTag();
    var indices = [];
    var polygonDimensions = [];
    while (this.reader.offset < finalOffset) {
      var numverts = this.reader.getUint16();
      numverts = numverts & 1023;
      polygonDimensions.push(numverts);
      for (var j = 0; j < numverts; j++)
        indices.push(this.reader.getVariableLengthIndex());
    }
    var geometryData = {
      type,
      vertexIndices: indices,
      polygonDimensions,
      points: this.currentPoints
    };
    if (polygonDimensions[0] === 1)
      geometryData.type = "points";
    else if (polygonDimensions[0] === 2)
      geometryData.type = "lines";
    this.currentLayer.geometry = geometryData;
  }
  // Lists the tag strings that can be associated with polygons by the PTAG chunk.
  // TAGS { tag-string[S0] * }
  parseTagStrings(length) {
    this.tree.tags = this.reader.getStringArray(length);
  }
  // Associates tags of a given type with polygons in the most recent POLS chunk.
  // PTAG { type[ID4], ( poly[VX], tag[U2] ) * }
  parsePolygonTagMapping(length) {
    var finalOffset = this.reader.offset + length;
    var type = this.reader.getIDTag();
    if (type === "SURF")
      this.parseMaterialIndices(finalOffset);
    else {
      this.reader.skip(length - 4);
    }
  }
  parseMaterialIndices(finalOffset) {
    this.currentLayer.geometry.materialIndices = [];
    while (this.reader.offset < finalOffset) {
      var polygonIndex = this.reader.getVariableLengthIndex();
      var materialIndex = this.reader.getUint16();
      this.currentLayer.geometry.materialIndices.push(polygonIndex, materialIndex);
    }
  }
  parseUnknownCHUNK(blockID, length) {
    console.warn("LWOLoader: unknown chunk type: " + blockID + " length: " + length);
    var data = this.reader.getString(length);
    this.currentForm[blockID] = data;
  }
}
class DataViewReader {
  constructor(buffer) {
    this.dv = new DataView(buffer);
    this.offset = 0;
    this._textDecoder = new TextDecoder();
    this._bytes = new Uint8Array(buffer);
  }
  size() {
    return this.dv.buffer.byteLength;
  }
  setOffset(offset) {
    if (offset > 0 && offset < this.dv.buffer.byteLength) {
      this.offset = offset;
    } else {
      console.error("LWOLoader: invalid buffer offset");
    }
  }
  endOfFile() {
    if (this.offset >= this.size())
      return true;
    return false;
  }
  skip(length) {
    this.offset += length;
  }
  getUint8() {
    var value = this.dv.getUint8(this.offset);
    this.offset += 1;
    return value;
  }
  getUint16() {
    var value = this.dv.getUint16(this.offset);
    this.offset += 2;
    return value;
  }
  getInt32() {
    var value = this.dv.getInt32(this.offset, false);
    this.offset += 4;
    return value;
  }
  getUint32() {
    var value = this.dv.getUint32(this.offset, false);
    this.offset += 4;
    return value;
  }
  getUint64() {
    var low, high;
    high = this.getUint32();
    low = this.getUint32();
    return high * 4294967296 + low;
  }
  getFloat32() {
    var value = this.dv.getFloat32(this.offset, false);
    this.offset += 4;
    return value;
  }
  getFloat32Array(size) {
    var a = [];
    for (var i = 0; i < size; i++) {
      a.push(this.getFloat32());
    }
    return a;
  }
  getFloat64() {
    var value = this.dv.getFloat64(this.offset, this.littleEndian);
    this.offset += 8;
    return value;
  }
  getFloat64Array(size) {
    var a = [];
    for (var i = 0; i < size; i++) {
      a.push(this.getFloat64());
    }
    return a;
  }
  // get variable-length index data type
  // VX ::= index[U2] | (index + 0xFF000000)[U4]
  // If the index value is less than 65,280 (0xFF00),then VX === U2
  // otherwise VX === U4 with bits 24-31 set
  // When reading an index, if the first byte encountered is 255 (0xFF), then
  // the four-byte form is being used and the first byte should be discarded or masked out.
  getVariableLengthIndex() {
    var firstByte = this.getUint8();
    if (firstByte === 255) {
      return this.getUint8() * 65536 + this.getUint8() * 256 + this.getUint8();
    }
    return firstByte * 256 + this.getUint8();
  }
  // An ID tag is a sequence of 4 bytes containing 7-bit ASCII values
  getIDTag() {
    return this.getString(4);
  }
  getString(size) {
    if (size === 0)
      return;
    const start = this.offset;
    let result;
    let length;
    if (size) {
      length = size;
      result = this._textDecoder.decode(new Uint8Array(this.dv.buffer, start, size));
    } else {
      length = this._bytes.indexOf(0, start) - start;
      result = this._textDecoder.decode(new Uint8Array(this.dv.buffer, start, length));
      length++;
      length += length % 2;
    }
    this.skip(length);
    return result;
  }
  getStringArray(size) {
    var a = this.getString(size);
    a = a.split("\0");
    return a.filter(Boolean);
  }
}
class Debugger {
  constructor() {
    this.active = false;
    this.depth = 0;
    this.formList = [];
  }
  enable() {
    this.active = true;
  }
  log() {
    if (!this.active)
      return;
    var nodeType;
    switch (this.node) {
      case 0:
        nodeType = "FORM";
        break;
      case 1:
        nodeType = "CHK";
        break;
      case 2:
        nodeType = "S-CHK";
        break;
    }
    console.log(
      "| ".repeat(this.depth) + nodeType,
      this.nodeID,
      `( ${this.offset} ) -> ( ${this.dataOffset + this.length} )`,
      this.node == 0 ? " {" : "",
      this.skipped ? "SKIPPED" : "",
      this.node == 0 && this.skipped ? "}" : ""
    );
    if (this.node == 0 && !this.skipped) {
      this.depth += 1;
      this.formList.push(this.dataOffset + this.length);
    }
    this.skipped = false;
  }
  closeForms() {
    if (!this.active)
      return;
    for (var i = this.formList.length - 1; i >= 0; i--) {
      if (this.offset >= this.formList[i]) {
        this.depth -= 1;
        console.log("| ".repeat(this.depth) + "}");
        this.formList.splice(-1, 1);
      }
    }
  }
}
function isEven(num) {
  return num % 2;
}
function stringOffset(string) {
  return string.length + 1 + (isEven(string.length + 1) ? 1 : 0);
}
function printBuffer(buffer, from, to) {
  console.log(new TextDecoder().decode(new Uint8Array(buffer, from, to)));
}
exports.IFFParser = IFFParser;
//# sourceMappingURL=IFFParser.cjs.map
