import { Geometry } from './p5.Geometry.js';
import { Vector } from '../math/p5.Vector.js';
import { d as request } from '../rendering-D5Svpe1O.js';
import '../constants-DFRExFht.js';
import './p5.DataArray.js';
import '../io/utilities.js';
import '../creating_reading-DLkHH80h.js';
import 'colorjs.io/fn';
import '../color/color_spaces/hsb.js';
import '../strands/ir_types.js';
import '../dom/p5.Element.js';
import '../dom/p5.File.js';
import '../io/p5.XML.js';
import '../p5.Renderer-Dh1sXxDM.js';
import '../image/filters.js';
import '../shape/custom_shapes.js';
import '../core/States.js';
import '../dom/p5.MediaElement.js';
import '../shape/2d_primitives.js';
import '../core/helpers.js';
import '../shape/attributes.js';
import '../shape/curves.js';
import '../shape/vertex.js';
import '../color/setting.js';
import 'omggif';
import '../io/csv.js';
import '../image/pixels.js';
import '../core/transform.js';
import './GeometryBuilder.js';
import '../math/p5.Matrix.js';
import '../math/Matrices/Matrix.js';
import '../math/Matrices/MatrixInterface.js';
import './p5.Quat.js';
import './ShapeBuilder.js';
import 'libtess';
import './p5.RenderBuffer.js';
import './GeometryBufferCache.js';
import '../image/const.js';
import '../type/textCore.js';
import '../core/filterShaders.js';
import './enums.js';
import './p5.Shader.js';
import '../math/trigonometry.js';

/**
 * @module Shape
 * @submodule 3D Models
 * @for p5
 */


async function fileExists(url) {
  try {
    const response = await fetch(url, { method: 'HEAD' });
    return response.ok;
  } catch (error) {
    return false;
  }
}

function loading(p5, fn){
  /**
   * Loads a 3D model to create a
   * <a href="#/p5.Geometry">p5.Geometry</a> object.
   *
   * `loadModel()` can load 3D models from OBJ and STL files. Once the model is
   * loaded, it can be displayed with the
   * <a href="#/p5/model">model()</a> function, as in `model(shape)`.
   *
   * There are three ways to call `loadModel()` with optional parameters to help
   * process the model.
   *
   * The first parameter, `path`, is a `String` with the path to the file. Paths
   * to local files should be relative, as in `loadModel('assets/model.obj')`.
   * URLs such as `'https://example.com/model.obj'` may be blocked due to browser
   * security. The `path` parameter can also be defined as a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)
   * object for more advanced usage.
   * Note: When loading a `.obj` file that references materials stored in
   * `.mtl` files, p5.js will attempt to load and apply those materials.
   * To ensure that the `.obj` file reads the `.mtl` file correctly include the
   * `.mtl` file alongside it.
   *
   * The first way to call `loadModel()` has three optional parameters after the
   * file path. The first optional parameter, `successCallback`, is a function
   * to call once the model loads. For example,
   * `loadModel('assets/model.obj', handleModel)` will call the `handleModel()`
   * function once the model loads. The second optional parameter,
   * `failureCallback`, is a function to call if the model fails to load. For
   * example, `loadModel('assets/model.obj', handleModel, handleFailure)` will
   * call the `handleFailure()` function if an error occurs while loading. The
   * third optional parameter, `fileType`, is the model’s file extension as a
   * string. For example,
   * `loadModel('assets/model', handleModel, handleFailure, '.obj')` will try to
   * load the file model as a `.obj` file.
   *
   * The second way to call `loadModel()` has four optional parameters after the
   * file path. The first optional parameter is a `Boolean` value. If `true` is
   * passed, as in `loadModel('assets/model.obj', true)`, then the model will be
   * resized to ensure it fits the canvas. The next three parameters are
   * `successCallback`, `failureCallback`, and `fileType` as described above.
   *
   * The third way to call `loadModel()` has one optional parameter after the
   * file path. The optional parameter, `options`, is an `Object` with options,
   * as in `loadModel('assets/model.obj', options)`. The `options` object can
   * have the following properties:
   *
   * ```js
   * let options = {
   *   // Enables standardized size scaling during loading if set to true.
   *   normalize: true,
   *
   *   // Function to call once the model loads.
   *   successCallback: handleModel,
   *
   *   // Function to call if an error occurs while loading.
   *   failureCallback: handleError,
   *
   *   // Model's file extension.
   *   fileType: '.stl',
   *
   *   // Flips the U texture coordinates of the model.
   *   flipU: false,
   *
   *   // Flips the V texture coordinates of the model.
   *   flipV: false
   * };
   *
   * // Pass the options object to loadModel().
   * loadModel('assets/model.obj', options);
   * ```
   *
   * This function returns a `Promise` and should be used in an `async` setup with
   * `await`. See the examples for the usage syntax.
   *
   * Note: There’s no support for colored STL files. STL files with color will
   * be rendered without color.
   *
   * @method loadModel
   * @param  {String|Request} path      path of the model to be loaded.
   * @param  {String} [fileType]          model’s file extension. Either `'.obj'` or `'.stl'`.
   * @param  {Boolean} [normalize]        if `true`, scale the model to fit the canvas.
   * @param  {function(p5.Geometry)} [successCallback] function to call once the model is loaded. Will be passed
   *                                                   the <a href="#/p5.Geometry">p5.Geometry</a> object.
   * @param  {function(Event)} [failureCallback] function to call if the model fails to load. Will be passed an `Error` event object.
   * @return {Promise<p5.Geometry>} the <a href="#/p5.Geometry">p5.Geometry</a> object
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * // Load the file and create a p5.Geometry object.
   * async function setup() {
   *   shape = await loadModel('assets/teapot.obj');
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * // Load the file and create a p5.Geometry object.
   * // Normalize the geometry's size to fit the canvas.
   * async function setup() {
   *   shape = await loadModel('assets/teapot.obj', true);
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * // Load the file and create a p5.Geometry object.
   * async function setup() {
   *   await loadModel('assets/teapot.obj', true, handleModel);
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * // Set the shape variable and log the geometry's
   * // ID to the console.
   * function handleModel(data) {
   *   shape = data;
   *   console.log(shape.gid);
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * // Load the file and create a p5.Geometry object.
   * async function setup() {
   *   await loadModel('assets/teapot.obj', true, handleModel, handleError);
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * // Set the shape variable and print the geometry's
   * // ID to the console.
   * function handleModel(data) {
   *   shape = data;
   *   console.log(shape.gid);
   * }
   *
   * // Print an error message if the file doesn't load.
   * function handleError(error) {
   *   console.error('Oops!', error);
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * // Load the file and create a p5.Geometry object.
   * async function setup() {
   *   await loadModel('assets/teapot.obj', '.obj', true, handleModel, handleError);
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * // Set the shape variable and print the geometry's
   * // ID to the console.
   * function handleModel(data) {
   *   shape = data;
   *   console.log(shape.gid);
   * }
   *
   * // Print an error message if the file doesn't load.
   * function handleError(error) {
   *   console.error('Oops!', error);
   * }
   *
   * @example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   * let options = {
   *   fileType: '.obj',
   *   normalize: true,
   *   successCallback: handleModel,
   *   failureCallback: handleError
   * };
   *
   * // Load the file and create a p5.Geometry object.
   * async function setup() {
   *   await loadModel('assets/teapot.obj', options);
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white teapot drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   *
   * // Set the shape variable and print the geometry's
   * // ID to the console.
   * function handleModel(data) {
   *   shape = data;
   *   console.log(shape.gid);
   * }
   *
   * // Print an error message if the file doesn't load.
   * function handleError(error) {
   *   console.error('Oops!', error);
   * }
   */
  /**
   * @method loadModel
   * @param  {String|Request} path
   * @param  {String} [fileType]
   * @param  {function(p5.Geometry)} [successCallback]
   * @param  {function(Event)} [failureCallback]
   * @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
   */
  /**
   * @method loadModel
   * @param  {String|Request} path
   * @param  {Object} [options] loading options.
   * @param  {String} [options.fileType]
   * @param  {function(p5.Geometry)} [options.successCallback]
   * @param  {function(Event)} [options.failureCallback]
   * @param  {Boolean} [options.normalize]
   * @param  {Boolean} [options.flipU]
   * @param  {Boolean} [options.flipV]
   * @return {Promise<p5.Geometry>} new <a href="#/p5.Geometry">p5.Geometry</a> object.
   */
  fn.loadModel = async function (
    path,
    fileType,
    normalize,
    successCallback,
    failureCallback
  ) {
    // p5._validateParameters('loadModel', arguments);

    let flipU = false;
    let flipV = false;

    if (typeof fileType === 'object') {
      // Passing in options object
      normalize = fileType.normalize || false;
      successCallback = fileType.successCallback;
      failureCallback = fileType.failureCallback;
      fileType = fileType.fileType || fileType;
      flipU = fileType.flipU || false;
      flipV = fileType.flipV || false;

    } else {
      // Passing in individual parameters
      if(typeof arguments[arguments.length-1] === 'function'){
        if(typeof arguments[arguments.length-2] === 'function'){
          successCallback = arguments[arguments.length-2];
          failureCallback = arguments[arguments.length-1];
        }else {
          successCallback = arguments[arguments.length-1];
        }
      }

      if (typeof fileType === 'string') {
        if(typeof normalize !== 'boolean') normalize = false;

      } else if (typeof fileType === 'boolean') {
        normalize = fileType;
        fileType = path.slice(-4);

      } else {
        fileType = path.slice(-4);
        normalize = false;
      }
    }

    if (fileType.toLowerCase() !== '.obj' && fileType.toLowerCase() !== '.stl') {
      fileType = '.obj';
    }

    const model = new Geometry(undefined, undefined, undefined, this._renderer);
    model.gid = `${path}|${normalize}`;

    async function getMaterials(lines) {
      const parsedMaterialPromises = [];

      for (let line of lines) {
        const mtllibMatch = line.match(/^mtllib (.+)/);

        if (mtllibMatch) {
          // Object has material
          let mtlPath = '';
          const mtlFilename = mtllibMatch[1];
          const objPathParts = path.split('/');
          if (objPathParts.length > 1) {
            objPathParts.pop();
            const objFolderPath = objPathParts.join('/');
            mtlPath = objFolderPath + '/' + mtlFilename;
          } else {
            mtlPath = mtlFilename;
          }

          parsedMaterialPromises.push(
            fileExists(mtlPath).then(exists => {
              if (exists) {
                return parseMtl(mtlPath);
              } else {
                console.warn(`MTL file not found or error in parsing; proceeding without materials: ${mtlPath}`);
                return {};

              }
            }).catch(error => {
              console.warn(`Error loading MTL file: ${mtlPath}`, error);
              return {};
            })
          );
        }
      }

      try {
        const parsedMaterials = await Promise.all(parsedMaterialPromises);
        const materials = Object.assign({}, ...parsedMaterials);
        return materials;
      } catch (error) {
        return {};
      }
    }

    try{
      if (fileType.match(/\.stl$/i)) {
        const { data } = await request(path, 'arrayBuffer');
        const cb = () => {
          parseSTL(model, data);

          if (normalize) {
            model.normalize();
          }

          if (flipU) {
            model.flipU();
          }

          if (flipV) {
            model.flipV();
          }
          model._makeTriangleEdges();

          if (successCallback) {
            return successCallback(model);
          } else {
            return model;
          }
        };
        return this._internal ? this._internal(cb) : cb();

      } else if (fileType.match(/\.obj$/i)) {
        const { data } = await request(path, 'text');
        const lines = data.split('\n');

        const parsedMaterials = await getMaterials(lines);
        const cb = () => {
          parseObj(model, lines, parsedMaterials);

          if (normalize) {
            model.normalize();
          }
          if (flipU) {
            model.flipU();
          }
          if (flipV) {
            model.flipV();
          }
          model._makeTriangleEdges();

          if (successCallback) {
            return successCallback(model);
          } else {
            return model;
          }
        };
        return this._internal ? this._internal(cb) : cb();
      }
    } catch(err) {
      p5._friendlyFileLoadError(3, path);
      if(failureCallback) {
        return failureCallback(err);
      } else {
        throw err;
      }
    }
  };

  /**
   * @private
   */
  async function parseMtl(mtlPath) {
    let currentMaterial = null;
    let materials = {};

    const { data } = await request(mtlPath, 'text');
    const lines = data.split('\n');

    for (let line = 0; line < lines.length; ++line) {
      const tokens = lines[line].trim().split(/\s+/);
      if (tokens[0] === 'newmtl') {
        const materialName = tokens[1];
        currentMaterial = materialName;
        materials[currentMaterial] = {};
      } else if (tokens[0] === 'Kd') {
        //Diffuse color
        materials[currentMaterial].diffuseColor = [
          parseFloat(tokens[1]),
          parseFloat(tokens[2]),
          parseFloat(tokens[3])
        ];
      } else if (tokens[0] === 'Ka') {
        //Ambient Color
        materials[currentMaterial].ambientColor = [
          parseFloat(tokens[1]),
          parseFloat(tokens[2]),
          parseFloat(tokens[3])
        ];
      } else if (tokens[0] === 'Ks') {
        //Specular color
        materials[currentMaterial].specularColor = [
          parseFloat(tokens[1]),
          parseFloat(tokens[2]),
          parseFloat(tokens[3])
        ];

      } else if (tokens[0] === 'map_Kd') {
        //Texture path
        materials[currentMaterial].texturePath = tokens[1];
      }
    }

    return materials;
  }

  /**
   * @private
   * Parse OBJ lines into model. For reference, this is what a simple model of a
   * square might look like:
   *
   * v -0.5 -0.5 0.5
   * v -0.5 -0.5 -0.5
   * v -0.5 0.5 -0.5
   * v -0.5 0.5 0.5
   *
   * f 4 3 2 1
   */
  function parseObj(model, lines, materials = {}) {
    // OBJ allows a face to specify an index for a vertex (in the above example),
    // but it also allows you to specify a custom combination of vertex, UV
    // coordinate, and vertex normal. So, "3/4/3" would mean, "use vertex 3 with
    // UV coordinate 4 and vertex normal 3". In WebGL, every vertex with different
    // parameters must be a different vertex, so loadedVerts is used to
    // temporarily store the parsed vertices, normals, etc., and indexedVerts is
    // used to map a specific combination (keyed on, for example, the string
    // "3/4/3"), to the actual index of the newly created vertex in the final
    // object.
    const loadedVerts = {
      v: [],
      vt: [],
      vn: []
    };


    // Map from source index → Map of material → destination index
    const usedVerts = {}; // Track colored vertices
    let currentMaterial = null;
    let hasColoredVertices = false;
    for (let line = 0; line < lines.length; ++line) {
      // Each line is a separate object (vertex, face, vertex normal, etc)
      // For each line, split it into tokens on whitespace. The first token
      // describes the type.
      const tokens = lines[line].trim().split(/\b\s+/);

      if (tokens.length > 0) {
        if (tokens[0] === 'usemtl') {
          // Switch to a new material
          currentMaterial = tokens[1];
        } else if (tokens[0] === 'v' || tokens[0] === 'vn') {
          // Check if this line describes a vertex or vertex normal.
          // It will have three numeric parameters.
          const vertex = new Vector(
            parseFloat(tokens[1]),
            parseFloat(tokens[2]),
            parseFloat(tokens[3])
          );
          loadedVerts[tokens[0]].push(vertex);
        } else if (tokens[0] === 'vt') {
          // Check if this line describes a texture coordinate.
          // It will have two numeric parameters U and V (W is omitted).
          // Because of WebGL texture coordinates rendering behaviour, the V
          // coordinate is inversed.
          const texVertex = [parseFloat(tokens[1]), 1 - parseFloat(tokens[2])];
          loadedVerts[tokens[0]].push(texVertex);
        } else if (tokens[0] === 'f') {
          // Check if this line describes a face.
          // OBJ faces can have more than three points. Triangulate points.
          for (let tri = 3; tri < tokens.length; ++tri) {
            const face = [];
            const vertexTokens = [1, tri - 1, tri];

            for (let tokenInd = 0; tokenInd < vertexTokens.length; ++tokenInd) {
              // Now, convert the given token into an index
              const vertString = tokens[vertexTokens[tokenInd]];
              let vertParts = vertString.split('/');

              for (let i = 0; i < vertParts.length; i++) {
                let index = parseInt(vertParts[i]);
                if (index > 0) index -= 1; // OBJ uses 1-based indexing
                vertParts[i] = index;
              }

              if (!usedVerts[vertString]) {
                usedVerts[vertString] = {};
              }

              if (usedVerts[vertString][currentMaterial] === undefined) {
                const vertIndex = model.vertices.length;
                model.vertices.push(loadedVerts.v.at(vertParts[0]).copy());
                model.uvs.push(loadedVerts.vt.at(vertParts[1]) ?
                  loadedVerts.vt.at(vertParts[1]).slice() : [0, 0]);
                model.vertexNormals.push(loadedVerts.vn.at(vertParts[2]) ?
                  loadedVerts.vn.at(vertParts[2]).copy() : new Vector(0, 0, 0));

                usedVerts[vertString][currentMaterial] = vertIndex;
                face.push(vertIndex);
                if (currentMaterial
                  && materials[currentMaterial]
                  && materials[currentMaterial].diffuseColor) {
                  hasColoredVertices = true;
                  const materialDiffuseColor =
                    materials[currentMaterial].diffuseColor;
                  model.vertexColors.push(materialDiffuseColor[0]);
                  model.vertexColors.push(materialDiffuseColor[1]);
                  model.vertexColors.push(materialDiffuseColor[2]);
                  model.vertexColors.push(1);
                } else {
                  model.vertexColors.push(-1, -1, -1, -1);
                }
              } else {
                face.push(usedVerts[vertString][currentMaterial]);
              }
            }

            if (
              face[0] !== face[1] &&
              face[0] !== face[2] &&
              face[1] !== face[2]
            ) {
              model.faces.push(face);
            }
          }
        }
      }
    }
    // If the model doesn't have normals, compute the normals
    if (model.vertexNormals.length === 0) {
      model.computeNormals();
    }
    if (!hasColoredVertices) {
      model.vertexColors = [];
    }

    return model;
  }

  /**
   * @private
   * STL files can be of two types, ASCII and Binary,
   *
   * We need to convert the arrayBuffer to an array of strings,
   * to parse it as an ASCII file.
   */
  function parseSTL(model, buffer) {
    if (isBinary(buffer)) {
      parseBinarySTL(model, buffer);
    } else {
      const reader = new DataView(buffer);

      if (!('TextDecoder' in window)) {
        console.warn(
          'Sorry, ASCII STL loading only works in browsers that support TextDecoder (https://caniuse.com/#feat=textencoder)'
        );
        return model;
      }

      const decoder = new TextDecoder('utf-8');
      const lines = decoder.decode(reader);
      const lineArray = lines.split('\n');
      parseASCIISTL(model, lineArray);
    }
    return model;
  }

  /**
   * @private
   * This function checks if the file is in ASCII format or in Binary format
   *
   * It is done by searching keyword `solid` at the start of the file.
   *
   * An ASCII STL data must begin with `solid` as the first six bytes.
   * However, ASCII STLs lacking the SPACE after the `d` are known to be
   * plentiful. So, check the first 5 bytes for `solid`.
   *
   * Several encodings, such as UTF-8, precede the text with up to 5 bytes:
   * https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
   * Search for `solid` to start anywhere after those prefixes.
   */
  function isBinary(data) {
    const reader = new DataView(data);

    // US-ASCII ordinal values for `s`, `o`, `l`, `i`, `d`
    const solid = [115, 111, 108, 105, 100];
    for (let off = 0; off < 5; off++) {
      // If "solid" text is matched to the current offset, declare it to be an ASCII STL.
      if (matchDataViewAt(solid, reader, off)) return false;
    }

    // Couldn't find "solid" text at the beginning; it is binary STL.
    return true;
  }

  /**
   * @private
   * This function matches the `query` at the provided `offset`
   */
  function matchDataViewAt(query, reader, offset) {
    // Check if each byte in query matches the corresponding byte from the current offset
    for (let i = 0, il = query.length; i < il; i++) {
      if (query[i] !== reader.getUint8(offset + i, false)) return false;
    }

    return true;
  }

  /**
   * @private
   * This function parses the Binary STL files.
   * https://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL
   *
   * Currently there is no support for the colors provided in STL files.
   */
  function parseBinarySTL(model, buffer) {
    const reader = new DataView(buffer);

    // Number of faces is present following the header
    const faces = reader.getUint32(80, true);
    let r,
      g,
      b,
      hasColors = false,
      colors;
    let defaultR, defaultG, defaultB;

    // Binary files contain 80-byte header, which is generally ignored.
    for (let index = 0; index < 80 - 10; index++) {
      // Check for `COLOR=`
      if (
        reader.getUint32(index, false) === 0x434f4c4f /*COLO*/ &&
        reader.getUint8(index + 4) === 0x52 /*'R'*/ &&
        reader.getUint8(index + 5) === 0x3d /*'='*/
      ) {
        hasColors = true;
        colors = [];

        defaultR = reader.getUint8(index + 6) / 255;
        defaultG = reader.getUint8(index + 7) / 255;
        defaultB = reader.getUint8(index + 8) / 255;
        // To be used when color support is added
        // alpha = reader.getUint8(index + 9) / 255;
      }
    }
    const dataOffset = 84;
    const faceLength = 12 * 4 + 2;

    // Iterate the faces
    for (let face = 0; face < faces; face++) {
      const start = dataOffset + face * faceLength;
      const normalX = reader.getFloat32(start, true);
      const normalY = reader.getFloat32(start + 4, true);
      const normalZ = reader.getFloat32(start + 8, true);

      if (hasColors) {
        const packedColor = reader.getUint16(start + 48, true);

        if ((packedColor & 0x8000) === 0) {
          // facet has its own unique color
          r = (packedColor & 0x1f) / 31;
          g = ((packedColor >> 5) & 0x1f) / 31;
          b = ((packedColor >> 10) & 0x1f) / 31;
        } else {
          r = defaultR;
          g = defaultG;
          b = defaultB;
        }
      }
      const newNormal = new Vector(normalX, normalY, normalZ);

      for (let i = 1; i <= 3; i++) {
        const vertexstart = start + i * 12;

        const newVertex = new Vector(
          reader.getFloat32(vertexstart, true),
          reader.getFloat32(vertexstart + 4, true),
          reader.getFloat32(vertexstart + 8, true)
        );

        model.vertices.push(newVertex);
        model.vertexNormals.push(newNormal);

        if (hasColors) {
          colors.push(r, g, b);
        }
      }

      model.faces.push([3 * face, 3 * face + 1, 3 * face + 2]);
      model.uvs.push([0, 0], [0, 0], [0, 0]);
    }
    return model;
  }

  /**
   * @private
   * ASCII STL file starts with `solid 'nameOfFile'`
   * Then contain the normal of the face, starting with `facet normal`
   * Next contain a keyword indicating the start of face vertex, `outer loop`
   * Next comes the three vertex, starting with `vertex x y z`
   * Vertices ends with `endloop`
   * Face ends with `endfacet`
   * Next face starts with `facet normal`
   * The end of the file is indicated by `endsolid`
   */
  function parseASCIISTL(model, lines) {
    let state = '';
    let curVertexIndex = [];
    let newNormal, newVertex;

    for (let iterator = 0; iterator < lines.length; ++iterator) {
      const line = lines[iterator].trim();
      const parts = line.split(' ');

      for (
        let partsiterator = 0;
        partsiterator < parts.length;
        ++partsiterator
      ) {
        if (parts[partsiterator] === '') {
          // Ignoring multiple whitespaces
          parts.splice(partsiterator, 1);
        }
      }

      if (parts.length === 0) {
        // Remove newline
        continue;
      }

      switch (state) {
        case '': // First run
          if (parts[0] !== 'solid') {
            // Invalid state
            console.error(line);
            console.error(`Invalid state "${parts[0]}", should be "solid"`);
            return;
          } else {
            state = 'solid';
          }
          break;

        case 'solid': // First face
          if (parts[0] !== 'facet' || parts[1] !== 'normal') {
            // Invalid state
            console.error(line);
            console.error(
              `Invalid state "${parts[0]}", should be "facet normal"`
            );
            return;
          } else {
            // Push normal for first face
            newNormal = new Vector(
              parseFloat(parts[2]),
              parseFloat(parts[3]),
              parseFloat(parts[4])
            );
            model.vertexNormals.push(newNormal, newNormal, newNormal);
            state = 'facet normal';
          }
          break;

        case 'facet normal': // After normal is defined
          if (parts[0] !== 'outer' || parts[1] !== 'loop') {
            // Invalid State
            console.error(line);
            console.error(`Invalid state "${parts[0]}", should be "outer loop"`);
            return;
          } else {
            // Next should be vertices
            state = 'vertex';
          }
          break;

        case 'vertex':
          if (parts[0] === 'vertex') {
            //Vertex of triangle
            newVertex = new Vector(
              parseFloat(parts[1]),
              parseFloat(parts[2]),
              parseFloat(parts[3])
            );
            model.vertices.push(newVertex);
            model.uvs.push([0, 0]);
            curVertexIndex.push(model.vertices.indexOf(newVertex));
          } else if (parts[0] === 'endloop') {
            // End of vertices
            model.faces.push(curVertexIndex);
            curVertexIndex = [];
            state = 'endloop';
          } else {
            // Invalid State
            console.error(line);
            console.error(
              `Invalid state "${parts[0]}", should be "vertex" or "endloop"`
            );
            return;
          }
          break;

        case 'endloop':
          if (parts[0] !== 'endfacet') {
            // End of face
            console.error(line);
            console.error(`Invalid state "${parts[0]}", should be "endfacet"`);
            return;
          } else {
            state = 'endfacet';
          }
          break;

        case 'endfacet':
          if (parts[0] === 'endsolid') ; else if (parts[0] === 'facet' && parts[1] === 'normal') {
            // Next face
            newNormal = new Vector(
              parseFloat(parts[2]),
              parseFloat(parts[3]),
              parseFloat(parts[4])
            );
            model.vertexNormals.push(newNormal, newNormal, newNormal);
            state = 'facet normal';
          } else {
            // Invalid State
            console.error(line);
            console.error(
              `Invalid state "${parts[0]
              }", should be "endsolid" or "facet normal"`
            );
            return;
          }
          break;

        default:
          console.error(`Invalid state "${state}"`);
          break;
      }
    }
    return model;
  }

  /**
   * Draws a <a href="#/p5.Geometry">p5.Geometry</a> object to the canvas.
   *
   * The first parameter, `model`, is the
   * <a href="#/p5.Geometry">p5.Geometry</a> object to draw.
   * <a href="#/p5.Geometry">p5.Geometry</a> objects can be built with
   * <a href="#/p5/buildGeometry">buildGeometry()</a>. They can also be loaded from
   * a file with <a href="#/p5/loadGeometry">loadGeometry()</a>.
   *
   * Note: `model()` can only be used in WebGL mode.
   *
   * ```js example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create the p5.Geometry object.
   *   shape = buildGeometry(createShape);
   *
   *   describe('A white cone drawn on a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the p5.Geometry object.
   *   model(shape);
   * }
   *
   * // Create p5.Geometry object from a single cone.
   * function createShape() {
   *   cone();
   * }
   * ```
   *
   * ```js example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *
   *   // Create the p5.Geometry object.
   *   shape = buildGeometry(createArrow);
   *
   *   describe('Two white arrows drawn on a gray background. The arrow on the right rotates slowly.');
   * }
   *
   * function draw() {
   *   background(50);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Turn on the lights.
   *   lights();
   *
   *   // Style the arrows.
   *   noStroke();
   *
   *   // Draw the p5.Geometry object.
   *   model(shape);
   *
   *   // Translate and rotate the coordinate system.
   *   translate(30, 0, 0);
   *   rotateZ(frameCount * 0.01);
   *
   *   // Draw the p5.Geometry object again.
   *   model(shape);
   * }
   *
   * function createArrow() {
   *   // Add shapes to the p5.Geometry object.
   *   push();
   *   rotateX(PI);
   *   cone(10);
   *   translate(0, -10, 0);
   *   cylinder(3, 20);
   *   pop();
   * }
   * ```
   *
   * ```js example
   * // Click and drag the mouse to view the scene from different angles.
   *
   * let shape;
   *
   * async function setup() {
   *   shape = await loadModel('assets/octahedron.obj');
   *
   *   createCanvas(100, 100, WEBGL);
   *
   *   describe('A white octahedron drawn against a gray background.');
   * }
   *
   * function draw() {
   *   background(200);
   *
   *   // Enable orbiting with the mouse.
   *   orbitControl();
   *
   *   // Draw the shape.
   *   model(shape);
   * }
   * ```
   *
   * Multiple instances can be drawn at once with `model(geometry, count)`. On its own,
   * all the instances get drawn to the same spot, but you can use
   * <a href="#/p5/instanceID">`instanceID()`</a> inside of a shader to handle each instance.
   * At large counts, this often runs faster than using a `for` loop.
   *
   * ```js example
   * let instancesShader;
   * let instance;
   * let count = 5;
   *
   * function drawInstance() {
   *   sphere(15);
   * }
   *
   * function setup() {
   *   createCanvas(200, 200, WEBGL);
   *   instance = buildGeometry(drawInstance);
   *   instancesShader = buildMaterialShader(drawSpaced);
   * }
   *
   * function drawSpaced() {
   *   worldInputs.begin();
   *   // Spread spheres evenly across the canvas based on their index
   *   let spacing = width / count;
   *   worldInputs.position.x +=
   *     (instanceID() - (count - 1) / 2) * spacing;
   *   worldInputs.end();
   * }
   *
   * function draw() {
   *   background(220);
   *   lights();
   *   noStroke();
   *   fill('red');
   *   shader(instancesShader);
   *   model(instance, count);
   * }
   * ```
   *
   * @method model
   * @param  {p5.Geometry} model 3D shape to be drawn.
   *
   * @param {Number} [count=1] number of instances to draw.
   */
  fn.model = function (model, count = 1) {
    this._assert3d('model');
    // p5._validateParameters('model', arguments);
    this._renderer.model(model, count);
  };

  /**
   * Load a 3d model from an OBJ or STL string.
   *
   * OBJ and STL files lack a built-in sense of scale, causing models exported from different programs to vary in size.
   * If your model doesn't display correctly, consider using `loadModel()` with `normalize` set to `true` to standardize its size.
   * Further adjustments can be made using the `scale()` function.
   *
   * Also, the support for colored STL files is not present. STL files with color will be
   * rendered without color properties.
   *
   * * Options can include:
   * - `modelString`: Specifies the plain text string of either an stl or obj file to be loaded.
   * - `fileType`: Defines the file extension of the model.
   * - `normalize`: Enables standardized size scaling during loading if set to true.
   * - `successCallback`: Callback for post-loading actions with the 3D model object.
   * - `failureCallback`: Handles errors if model loading fails, receiving an event error.
   * - `flipU`: Flips the U texture coordinates of the model.
   * - `flipV`: Flips the V texture coordinates of the model.
   *
   *
   * @method createModel
   * @param  {String} modelString         String of the object to be loaded
   * @param  {String} [fileType]          The file extension of the model
   *                                      (<code>.stl</code>, <code>.obj</code>).
   * @param  {Boolean} [normalize]        If true, scale the model to a
   *                                      standardized size when loading
   * @param  {function(p5.Geometry)} [successCallback] Function to be called
   *                                     once the model is loaded. Will be passed
   *                                     the 3D model object.
   * @param  {function(Event)} [failureCallback] called with event error if
   *                                         the model fails to load.
   * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
   *
   * @example
   * const octahedron_model = `
   * v 0.000000E+00 0.000000E+00 40.0000
   * v 22.5000 22.5000 0.000000E+00
   * v 22.5000 -22.5000 0.000000E+00
   * v -22.5000 -22.5000 0.000000E+00
   * v -22.5000 22.5000 0.000000E+00
   * v 0.000000E+00 0.000000E+00 -40.0000
   * f     1 2 3
   * f     1 3 4
   * f     1 4 5
   * f     1 5 2
   * f     6 5 4
   * f     6 4 3
   * f     6 3 2
   * f     6 2 5
   * `;
   * //draw a spinning octahedron
   * let octahedron;
   *
   * function setup() {
   *   createCanvas(100, 100, WEBGL);
   *   octahedron = createModel(octahedron_model, '.obj');
   *   describe('Vertically rotating 3D octahedron.');
   * }
   *
   * function draw() {
   *   background(200);
   *   rotateX(frameCount * 0.01);
   *   rotateY(frameCount * 0.01);
   *   model(octahedron);
   * }
   */
  /**
   * @method createModel
   * @param  {String} modelString
   * @param  {String} [fileType]
   * @param  {function(p5.Geometry)} [successCallback]
   * @param  {function(Event)} [failureCallback]
   * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
   */
  /**
   * @method createModel
   * @param  {String} modelString
   * @param  {String} [fileType]
   * @param  {Object} [options]
   * @param  {function(p5.Geometry)} [options.successCallback]
   * @param  {function(Event)} [options.failureCallback]
   * @param  {boolean} [options.normalize]
   * @param  {boolean} [options.flipU]
   * @param  {boolean} [options.flipV]
   * @return {p5.Geometry} the <a href="#/p5.Geometry">p5.Geometry</a> object
   */
  let modelCounter = 0;
  fn.createModel = function(modelString, fileType=' ', options) {
    // p5._validateParameters('createModel', arguments);
    let normalize= false;
    let successCallback;
    let failureCallback;
    let flipU = false;
    let flipV = false;
    if (options && typeof options === 'object') {
      normalize = options.normalize || false;
      successCallback = options.successCallback;
      failureCallback = options.failureCallback;
      flipU = options.flipU || false;
      flipV = options.flipV || false;
    } else if (typeof options === 'boolean') {
      normalize = options;
      successCallback = arguments[3];
      failureCallback = arguments[4];
    } else {
      successCallback = typeof arguments[2] === 'function' ? arguments[2] : undefined;
      failureCallback = arguments[3];
    }
    const model = new p5.Geometry();
    model.gid = `${fileType}|${normalize}|${modelCounter++}`;

    if (fileType.match(/\.stl$/i)) {
      try {
        let uint8array = new TextEncoder().encode(modelString);
        let arrayBuffer = uint8array.buffer;
        parseSTL(model, arrayBuffer);
      } catch (error) {
        if (failureCallback) {
          failureCallback(error);
        } else {
          p5._friendlyError('Error during parsing: ' + error.message);
        }
        return;
      }
    } else if (fileType.match(/\.obj$/i)) {
      try {
        const lines = modelString.split('\n');
        parseObj(model, lines);
      } catch (error) {
        if (failureCallback) {
          failureCallback(error);
        } else {
          p5._friendlyError('Error during parsing: ' + error.message);
        }
        return;
      }
    } else {
      p5._friendlyFileLoadError(3, modelString);
      if (failureCallback) {
        failureCallback();
      } else {
        p5._friendlyError(
          'Sorry, the file type is invalid. Only OBJ and STL files are supported.'
        );
      }
    }
    if (normalize) {
      model.normalize();
    }

    if (flipU) {
      model.flipU();
    }

    if (flipV) {
      model.flipV();
    }

    model._makeTriangleEdges();

    if (typeof successCallback === 'function') {
      successCallback(model);
    }

    return model;
  };
}

if(typeof p5 !== 'undefined'){
  loading(p5, p5.prototype);
}

export { loading as default };
