import { m as macro } from '../../../macros2.js';
import { A as degreesFromRadians } from '../../../Common/Core/Math/index.js';
import vtkActor from '../../../Rendering/Core/Actor.js';
import vtkCamera from '../../../Rendering/Core/Camera.js';
import vtkDataArray from '../../../Common/Core/DataArray.js';
import vtkPolyData from '../../../Common/DataModel/PolyData.js';
import vtkMapper from '../../../Rendering/Core/Mapper.js';
import vtkCellArray from '../../../Common/Core/CellArray.js';
import vtkTransform from '../../../Common/Transform/Transform.js';
import GLTFParser from './Parser.js';
import { ALPHA_MODE, SEMANTIC_ATTRIBUTE_MAP, MODES } from './Constants.js';
import { loadImage, createVTKTextureFromGLTFTexture } from './Utils.js';
import { handleKHRMaterialsSpecular, handleKHRMaterialsIor, handleKHRMaterialsUnlit, handleKHRMaterialsVariants, handleKHRLightsPunctual, handleKHRDracoMeshCompression } from './Extensions.js';
import { mat4, vec3, quat } from 'gl-matrix';

const {
  vtkWarningMacro,
  vtkDebugMacro
} = macro;

/**
 * Parses a GLTF objects
 * @param {Object} gltf - The GLTF object to parse
 * @returns {glTF} The parsed GLTF object
 */
async function parseGLTF(gltf, options) {
  const parser = new GLTFParser(gltf, options);
  const tree = await parser.parse();
  return tree;
}

/**
 * Creates VTK polydata from a GLTF mesh primitive
 * @param {GLTFPrimitive} primitive - The GLTF mesh primitive
 * @returns {vtkPolyData} The created VTK polydata
 */
async function createPolyDataFromGLTFMesh(primitive) {
  if (!primitive || !primitive.attributes) {
    vtkWarningMacro('Primitive has no position data, skipping');
    return null;
  }
  if (primitive.extensions?.KHR_draco_mesh_compression) {
    return handleKHRDracoMeshCompression(primitive.extensions.KHR_draco_mesh_compression);
  }
  const polyData = vtkPolyData.newInstance();
  const cells = vtkCellArray.newInstance();
  const pointData = polyData.getPointData();
  const attrs = Object.entries(primitive.attributes);
  attrs.forEach(async _ref => {
    let [attributeName, accessor] = _ref;
    switch (attributeName) {
      case SEMANTIC_ATTRIBUTE_MAP.POSITION:
        {
          const position = primitive.attributes.position.value;
          polyData.getPoints().setData(position, primitive.attributes.position.component);
          break;
        }
      case SEMANTIC_ATTRIBUTE_MAP.NORMAL:
        {
          const normals = primitive.attributes.normal.value;
          pointData.setNormals(vtkDataArray.newInstance({
            name: 'Normals',
            values: normals,
            numberOfComponents: primitive.attributes.normal.components
          }));
          break;
        }
      case SEMANTIC_ATTRIBUTE_MAP.COLOR_0:
        {
          const color = primitive.attributes.color.value;
          pointData.setScalars(vtkDataArray.newInstance({
            name: 'Scalars',
            values: color,
            numberOfComponents: primitive.attributes.color.components
          }));
          break;
        }
      case SEMANTIC_ATTRIBUTE_MAP.TEXCOORD_0:
        {
          const tcoords0 = primitive.attributes.texcoord0.value;
          const da = vtkDataArray.newInstance({
            name: 'TEXCOORD_0',
            values: tcoords0,
            numberOfComponents: primitive.attributes.texcoord0.components
          });
          pointData.addArray(da);
          pointData.setActiveTCoords(da.getName());
          break;
        }
      case SEMANTIC_ATTRIBUTE_MAP.TEXCOORD_1:
        {
          const tcoords = primitive.attributes.texcoord1.value;
          const dac = vtkDataArray.newInstance({
            name: 'TEXCOORD_1',
            values: tcoords,
            numberOfComponents: primitive.attributes.texcoord1.components
          });
          pointData.addArray(dac);
          break;
        }
      case SEMANTIC_ATTRIBUTE_MAP.TANGENT:
        {
          const tangent = primitive.attributes.tangent.value;
          const dat = vtkDataArray.newInstance({
            name: 'Tangents',
            values: tangent,
            numberOfComponents: primitive.attributes.tangent.components
          });
          pointData.addArray(dat);
          break;
        }
      default:
        vtkWarningMacro(`Unhandled attribute: ${attributeName}`);
    }
  });

  // Handle indices if available
  if (primitive.indices != null) {
    const indices = primitive.indices.value;
    const nCells = indices.length - 2;
    switch (primitive.mode) {
      case MODES.GL_LINE_STRIP:
      case MODES.GL_TRIANGLE_STRIP:
      case MODES.GL_LINE_LOOP:
        vtkWarningMacro('GL_LINE_LOOP not implemented');
        break;
      default:
        cells.resize(4 * indices.length / 3);
        for (let cellId = 0; cellId < nCells; cellId += 3) {
          const cell = indices.slice(cellId, cellId + 3);
          cells.insertNextCell(cell);
        }
    }
  }
  switch (primitive.mode) {
    case MODES.GL_TRIANGLES:
    case MODES.GL_TRIANGLE_FAN:
      polyData.setPolys(cells);
      break;
    case MODES.GL_LINES:
    case MODES.GL_LINE_STRIP:
    case MODES.GL_LINE_LOOP:
      polyData.setLines(cells);
      break;
    case MODES.GL_POINTS:
      polyData.setVerts(cells);
      break;
    case MODES.GL_TRIANGLE_STRIP:
      polyData.setStrips(cells);
      break;
    default:
      vtkWarningMacro('Invalid primitive draw mode. Ignoring connectivity.');
  }
  return polyData;
}

/**
 * Creates a VTK property from a GLTF material
 * @param {object} model - The vtk model object
 * @param {GLTFMaterial} material - The GLTF material
 * @param {vtkActor} actor - The VTK actor
 */
async function createPropertyFromGLTFMaterial(model, material, actor) {
  let metallicFactor = 1.0;
  let roughnessFactor = 1.0;
  const emissiveFactor = material.emissiveFactor;
  const property = actor.getProperty();
  const pbr = material.pbrMetallicRoughness;
  if (pbr != null) {
    if (!pbr?.metallicFactor || pbr?.metallicFactor <= 0 || pbr?.metallicFactor >= 1) {
      vtkDebugMacro('Invalid material.pbrMetallicRoughness.metallicFactor value. Using default value instead.');
    } else metallicFactor = pbr.metallicFactor;
    if (!pbr?.roughnessFactor || pbr?.roughnessFactor <= 0 || pbr?.roughnessFactor >= 1) {
      vtkDebugMacro('Invalid material.pbrMetallicRoughness.roughnessFactor value. Using default value instead.');
    } else roughnessFactor = pbr.roughnessFactor;
    const color = pbr.baseColorFactor;
    if (color != null) {
      property.setDiffuseColor(color[0], color[1], color[2]);
      property.setOpacity(color[3]);
    }
    property.setMetallic(metallicFactor);
    property.setRoughness(roughnessFactor);
    property.setEmission(emissiveFactor);
    if (pbr.baseColorTexture) {
      pbr.baseColorTexture.extensions;
      const tex = pbr.baseColorTexture.texture;
      if (tex.extensions != null) {
        const extensionsNames = Object.keys(tex.extensions);
        extensionsNames.forEach(extensionName => {
          // TODO: Handle KHR_texture_basisu extension
          // const extension = tex.extensions[extensionName];
          switch (extensionName) {
            default:
              vtkWarningMacro(`Unhandled extension: ${extensionName}`);
          }
        });
      }
      const sampler = tex.sampler;
      const image = await loadImage(tex.source);
      const diffuseTex = createVTKTextureFromGLTFTexture(image, sampler);

      // FIXME: Workaround for textures not showing up in WebGL
      const viewAPI = model.renderer.getRenderWindow();
      const isWebGL = viewAPI.getViews()[0].isA('vtkOpenGLRenderWindow');
      if (isWebGL) {
        actor.addTexture(diffuseTex);
      } else {
        property.setDiffuseTexture(diffuseTex);
      }
    }
    if (pbr.metallicRoughnessTexture) {
      pbr.metallicRoughnessTexture.extensions;
      const tex = pbr.metallicRoughnessTexture.texture;
      const sampler = tex.sampler;
      const metallicImage = await loadImage(tex.source, 'b');
      const metallicTex = createVTKTextureFromGLTFTexture(metallicImage, sampler);
      property.setMetallicTexture(metallicTex);
      const roughnessImage = await loadImage(tex.source, 'g');
      const roughnessTex = createVTKTextureFromGLTFTexture(roughnessImage, sampler);
      property.setRoughnessTexture(roughnessTex);
    }

    // Handle ambient occlusion texture (occlusionTexture)
    if (material.occlusionTexture) {
      material.occlusionTexture.extensions;
      const tex = material.occlusionTexture.texture;
      const sampler = tex.sampler;
      const aoImage = await loadImage(tex.source, 'r');
      const aoTex = createVTKTextureFromGLTFTexture(aoImage, sampler);
      property.setAmbientOcclusionTexture(aoTex);
    }

    // Handle emissive texture (emissiveTexture)
    if (material.emissiveTexture) {
      material.emissiveTexture.extensions;
      const tex = material.emissiveTexture.texture;
      const sampler = tex.sampler;
      const emissiveImage = await loadImage(tex.source);
      const emissiveTex = createVTKTextureFromGLTFTexture(emissiveImage, sampler);
      property.setEmissionTexture(emissiveTex);

      // Handle mutiple Uvs
      if (material.emissiveTexture.texCoord != null) {
        const pd = actor.getMapper().getInputData().getPointData();
        pd.setActiveTCoords(`TEXCOORD_${material.emissiveTexture.texCoord}`);
      }
    }

    // Handle normal texture (normalTexture)
    if (material.normalTexture) {
      material.normalTexture.extensions;
      const tex = material.normalTexture.texture;
      const sampler = tex.sampler;
      const normalImage = await loadImage(tex.source);
      const normalTex = createVTKTextureFromGLTFTexture(normalImage, sampler);
      property.setNormalTexture(normalTex);
      if (material.normalTexture.scale != null) {
        property.setNormalStrength(material.normalTexture.scale);
      }
    }
  }

  // Material extensions
  if (material.extensions != null) {
    const extensionsNames = Object.keys(material.extensions);
    extensionsNames.forEach(extensionName => {
      const extension = material.extensions[extensionName];
      switch (extensionName) {
        case 'KHR_materials_unlit':
          handleKHRMaterialsUnlit(extension, property);
          break;
        case 'KHR_materials_ior':
          handleKHRMaterialsIor(extension, property);
          break;
        case 'KHR_materials_specular':
          handleKHRMaterialsSpecular(extension, property);
          break;
        default:
          vtkWarningMacro(`Unhandled extension: ${extensionName}`);
      }
    });
  }
  if (material.alphaMode !== ALPHA_MODE.OPAQUE) {
    actor.setForceTranslucent(true);
  }
  property.setBackfaceCulling(!material.doubleSided);
}

/**
 * Handles primitive extensions
 * @param {string} nodeId The GLTF node id
 * @param {*} extensions The extensions object
 * @param {*} model The vtk model object
 */
function handlePrimitiveExtensions(nodeId, extensions, model) {
  const extensionsNames = Object.keys(extensions);
  extensionsNames.forEach(extensionName => {
    const extension = extensions[extensionName];
    switch (extensionName) {
      case 'KHR_materials_variants':
        model.variantMappings.set(nodeId, extension.mappings);
        break;
      default:
        vtkWarningMacro(`Unhandled extension: ${extensionName}`);
    }
  });
}

/**
 * Creates a VTK actor from a GLTF mesh
 * @param {GLTFMesh} mesh - The GLTF mesh
 * @returns {vtkActor} The created VTK actor
 */
async function createActorFromGTLFNode(worldMatrix) {
  const actor = vtkActor.newInstance();
  const mapper = vtkMapper.newInstance();
  mapper.setColorModeToDirectScalars();
  actor.setMapper(mapper);
  actor.setUserMatrix(worldMatrix);
  const polydata = vtkPolyData.newInstance();
  mapper.setInputData(polydata);
  return actor;
}

/**
 * Creates a VTK actor from a GLTF mesh
 * @param {GLTFMesh} mesh - The GLTF mesh
 * @returns {vtkActor} The created VTK actor
 */
async function createActorFromGTLFPrimitive(model, primitive, worldMatrix) {
  const actor = vtkActor.newInstance();
  const mapper = vtkMapper.newInstance();
  mapper.setColorModeToDirectScalars();
  actor.setMapper(mapper);
  actor.setUserMatrix(worldMatrix);
  const polydata = await createPolyDataFromGLTFMesh(primitive);
  mapper.setInputData(polydata);

  // Support for materials
  if (primitive.material != null) {
    await createPropertyFromGLTFMaterial(model, primitive.material, actor);
  }
  if (primitive.extensions != null) {
    handlePrimitiveExtensions(`${primitive.name}`, primitive.extensions, model);
  }
  return actor;
}

/**
 * Creates a GLTF animation object
 * @param {GLTFAnimation} animation
 * @returns
 */
function createGLTFAnimation(animation) {
  vtkDebugMacro('Creating animation:', animation);
  return {
    name: animation.name,
    channels: animation.channels,
    samplers: animation.samplers,
    getChannelByTargetNode(nodeIndex) {
      return this.channels.filter(channel => channel.target.node === nodeIndex);
    }
  };
}

/**
 * Gets the transformation matrix for a GLTF node
 * @param {GLTFNode} node - The GLTF node
 * @returns {mat4} The transformation matrix
 */
function getTransformationMatrix(node) {
  // TRS
  const translation = node.translation ?? vec3.create();
  const rotation = node.rotation ?? quat.create();
  const scale = node.scale ?? vec3.fromValues(1.0, 1.0, 1.0);
  const matrix = node.matrix != null ? mat4.clone(node.matrix) : mat4.fromRotationTranslationScale(mat4.create(), rotation, translation, scale);
  return matrix;
}

/**
 * Processes a GLTF node
 * @param {GLTFnode} node - The GLTF node
 * @param {object} model The model object
 * @param {vtkActor} parentActor The parent actor
 * @param {mat4} parentMatrix The parent matrix
 */
async function processNode(node, model) {
  let parentActor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
  let parentMatrix = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : mat4.create();
  node.transform = getTransformationMatrix(node);
  const worldMatrix = mat4.multiply(mat4.create(), parentMatrix, node.transform);

  // Create actor for the current node
  if (node.mesh != null) {
    const nodeActor = await createActorFromGTLFNode(worldMatrix);
    if (parentActor) {
      nodeActor.setParentProp(parentActor);
    }
    model.actors.set(`${node.id}`, nodeActor);
    await Promise.all(node.mesh.primitives.map(async (primitive, i) => {
      const actor = await createActorFromGTLFPrimitive(model, primitive, worldMatrix);
      actor.setParentProp(nodeActor);
      model.actors.set(`${node.id}_${primitive.name}`, actor);
    }));
  }

  // Handle KHRLightsPunctual extension
  if (node.extensions?.KHR_lights_punctual) {
    handleKHRLightsPunctual(node.extensions.KHR_lights_punctual, node.transform, model);
  }
  if (node.children && Array.isArray(node.children) && node.children.length > 0) {
    await Promise.all(node.children.map(async child => {
      const parent = model.actors.get(node.id);
      await processNode(child, model, parent, worldMatrix);
    }));
  }
}

/**
 * Creates VTK actors from a GLTF object
 * @param {glTF} glTF - The GLTF object
 * @param {number} sceneId - The scene index to create actors for
 * @returns {vtkActor[]} The created VTK actors
 */
async function createVTKObjects(model) {
  model.animations = model.glTFTree.animations?.map(createGLTFAnimation);
  const extensionsNames = Object.keys(model.glTFTree?.extensions || []);
  extensionsNames.forEach(extensionName => {
    const extension = model.glTFTree.extensions[extensionName];
    switch (extensionName) {
      case 'KHR_materials_variants':
        handleKHRMaterialsVariants(extension, model);
        break;
      case 'KHR_draco_mesh_compression':
        break;
      default:
        vtkWarningMacro(`Unhandled extension: ${extensionName}`);
    }
  });

  // Get the sceneId to process
  const sceneId = model.sceneId ?? model.glTFTree.scene;
  if (model.glTFTree.scenes?.length && model.glTFTree.scenes[sceneId]?.nodes) {
    await Promise.all(model.glTFTree.scenes[sceneId].nodes.map(async node => {
      if (node) {
        await processNode(node, model);
      } else {
        vtkWarningMacro(`Node not found in glTF.nodes`);
      }
    }));
  } else {
    vtkWarningMacro('No valid scenes found in the glTF data');
  }
}

/**
 * Sets up the camera for a vtk renderer based on the bounds of the given actors.
 *
 * @param {GLTCamera} camera - The GLTF camera object
 */
function GLTFCameraToVTKCamera(glTFCamera) {
  const camera = vtkCamera.newInstance();
  if (glTFCamera.type === 'perspective') {
    const {
      yfov,
      znear,
      zfar
    } = glTFCamera.perspective;
    camera.setClippingRange(znear, zfar);
    camera.setParallelProjection(false);
    camera.setViewAngle(degreesFromRadians(yfov));
  } else if (glTFCamera.type === 'orthographic') {
    const {
      ymag,
      znear,
      zfar
    } = glTFCamera.orthographic;
    camera.setClippingRange(znear, zfar);
    camera.setParallelProjection(true);
    camera.setParallelScale(ymag);
  } else {
    throw new Error('Unsupported camera type');
  }
  return camera;
}

/**
 *
 * @param {vtkCamera} camera
 * @param {*} transformMatrix
 */
function applyTransformToCamera(camera, transformMatrix) {
  if (!camera || !transformMatrix) {
    return;
  }

  // At identity, camera position is origin, +y up, -z view direction
  const position = [0, 0, 0];
  const viewUp = [0, 1, 0];
  const focus = [0, 0, -1];
  const t = vtkTransform.newInstance();
  t.setMatrix(transformMatrix);

  // Transform position
  t.transformPoint(position, position);
  t.transformPoints(viewUp, viewUp);
  t.transformPoints(focus, focus);
  focus[0] += position[0];
  focus[1] += position[1];
  focus[2] += position[2];

  // Apply the transformed values to the camera
  camera.setPosition(position);
  camera.setFocalPoint(focus);
  camera.setViewUp(viewUp);
}

export { GLTFCameraToVTKCamera, applyTransformToCamera, createPropertyFromGLTFMaterial, createVTKObjects, parseGLTF };
