import {
  TextureLoader,
  BufferAttribute,
  Color,
  DoubleSide,
  MeshPhongMaterial,
  PointsMaterial,
  LineBasicMaterial,
} from "three";

export const GL_COMPONENT_TYPES = {
  5120: Int8Array,
  5121: Uint8Array,
  5122: Int16Array,
  5123: Uint16Array,
  5125: Uint32Array,
  5126: Float32Array,
};

export const GL_CONSTANTS = {
  FLOAT: 5126,
  //FLOAT_MAT2: 35674,
  FLOAT_MAT3: 35675,
  FLOAT_MAT4: 35676,
  FLOAT_VEC2: 35664,
  FLOAT_VEC3: 35665,
  FLOAT_VEC4: 35666,
  LINEAR: 9729,
  REPEAT: 10497,
  SAMPLER_2D: 35678,
  POINTS: 0,
  LINES: 1,
  LINE_LOOP: 2,
  LINE_STRIP: 3,
  TRIANGLES: 4,
  TRIANGLE_STRIP: 5,
  TRIANGLE_FAN: 6,
  UNSIGNED_BYTE: 5121,
  UNSIGNED_SHORT: 5123,
};

const MAX_GAP = 128 * 1024; // 128 KB
const MAX_CHUNK = 30 * 1024 * 1024; // 100 MB

export class GltfStructure {
  constructor(id, loadController) {
    this.id = `${id}`;
    this.json = null;
    this.loadController = loadController;
    this.loader = null;
    this.batchDelay = 10;
    this.maxBatchSize = 5 * 1024 * 1024;
    this.maxRangesPerRequest = 512;
    this.pendingRequests = [];
    this.batchTimeout = null;
    this.textureLoader = new TextureLoader();
    this.materials = new Map();
    this.textureCache = new Map();
    this.materialCache = new Map();
    this.uri = "";
    this._nextObjectId = 1;
    this.loadingAborted = false;
    this.criticalError = null;
  }

  async initialize(loader) {
    const json = await this.loadController.loadJson();

    if (json.asset === undefined || json.asset.version[0] < 2) {
      throw new Error("GltfStructure: Unsupported asset. glTF versions >=2.0 are supported.");
    }

    this.json = json;
    this.loader = loader;
    this.uri = this.json.buffers[0].uri || "";
  }

  clear() {
    this.json = null;
    this.loadController = null;
    this.pendingRequests = [];
    if (this.batchTimeout) {
      clearTimeout(this.batchTimeout);
      this.batchTimeout = null;
    }

    this.disposeMaterials();
    this.textureCache.clear();
    this.materials.clear();

    this.activeChunkLoads = 0;
    this.chunkQueue = [];
    this.loadingAborted = false;
    this.criticalError = null;
  }

  getJson() {
    return this.json;
  }

  scheduleRequest(request) {
    return new Promise((resolve, reject) => {
      if (this.loadingAborted) {
        reject(this.criticalError || new Error("Structure loading has been aborted due to critical error"));
        return;
      }
      this.pendingRequests.push({
        ...request,
        _resolve: resolve,
        _reject: reject,
      });
    });
  }

  isCriticalHttpError(error) {
    if (!error) return false;

    const status = error.status || error.statusCode || error.code;

    if (typeof status === "number") {
      return status >= 400 && status < 600;
    }

    if (error.message) {
      const match = error.message.match(/HTTP\s+(\d{3})/i);
      if (match) {
        const code = parseInt(match[1], 10);
        return code >= 400 && code < 600;
      }
    }

    return false;
  }

  abortLoading(error) {
    if (this.loadingAborted) {
      return;
    }

    this.loadingAborted = true;
    this.criticalError = error;

    const requests = [...this.pendingRequests];
    this.pendingRequests = [];

    for (const req of requests) {
      if (req._reject) {
        req._reject(error);
      }
    }

    console.error(
      `❌ Critical error for structure "${this.id}". All further loading aborted.`,
      `\n   Error: ${error.message || error}`,
      `\n   Rejected ${requests.length} pending chunk requests.`
    );

    throw error;
  }

  async flushBufferRequests() {
    if (!this.pendingRequests || this.pendingRequests.length === 0) return;
    const requests = [...this.pendingRequests];
    this.pendingRequests = [];

    requests.sort((a, b) => a.offset - b.offset);
    const mergedRanges = [];
    let current = {
      start: requests[0].offset,
      end: requests[0].offset + requests[0].length,
      requests: [requests[0]],
    };
    for (let i = 1; i < requests.length; i++) {
      const req = requests[i];
      const gap = req.offset - current.end;
      const newEnd = Math.max(current.end, req.offset + req.length);
      const projectedSize = newEnd - current.start;
      if (gap <= MAX_GAP && projectedSize <= MAX_CHUNK) {
        current.end = newEnd;
        current.requests.push(req);
      } else {
        mergedRanges.push(current);
        current = {
          start: req.offset,
          end: req.offset + req.length,
          requests: [req],
        };
      }
    }
    mergedRanges.push(current);
    const finalRanges = [];
    for (const range of mergedRanges) {
      let { start, end, requests } = range;
      while (end - start > MAX_CHUNK) {
        let splitIdx = 0;
        for (let i = 0; i < requests.length; i++) {
          if (requests[i].offset + requests[i].length - start > MAX_CHUNK) {
            break;
          }
          splitIdx = i;
        }
        const chunkRequests = requests.slice(0, splitIdx + 1);
        const chunkEnd =
          chunkRequests[chunkRequests.length - 1].offset + chunkRequests[chunkRequests.length - 1].length;
        finalRanges.push({
          start,
          end: chunkEnd,
          requests: chunkRequests,
        });
        requests = requests.slice(splitIdx + 1);
        if (requests.length > 0) {
          start = requests[0].offset;
          end = requests[0].offset + requests[0].length;
          for (let i = 1; i < requests.length; i++) {
            end = Math.max(end, requests[i].offset + requests[i].length);
          }
        }
      }
      if (requests.length > 0) {
        finalRanges.push({ start, end, requests });
      }
    }

    const promises = finalRanges.map(async (range, index) => {
      if (this.loadingAborted) {
        for (const req of range.requests) {
          req._reject(this.criticalError || new Error("Structure loading aborted"));
        }
        return;
      }

      await this.loader.waitForChunkSlot();

      try {
        const length = range.end - range.start;
        const buffer = await this.loadController.loadBinaryData([{ offset: range.start, length }], this.uri);

        for (const req of range.requests) {
          const relOffset = req.offset - range.start;
          try {
            req._resolve({ buffer, relOffset, length: req.length });
          } catch (e) {
            req._reject(e);
          }
        }
      } catch (error) {
        for (const req of range.requests) {
          req._reject(error);
        }

        if (this.isCriticalHttpError(error)) {
          this.abortLoading(error);
        } else {
          console.warn(`Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`, error);
        }
      } finally {
        this.loader.releaseChunkSlot();
      }
    });

    await Promise.all(promises);

    this.pendingRequests = [];
  }

  getBufferView(byteOffset, byteLength, componentType) {
    return this.scheduleRequest({
      offset: byteOffset,
      length: byteLength,
      componentType,
    });
  }

  createTypedArray(buffer, offset, length, componentType) {
    try {
      if (!buffer || !(buffer instanceof ArrayBuffer)) {
        throw new Error("Invalid buffer");
      }

      let elementSize;
      switch (componentType) {
        case 5120:
        case 5121:
          elementSize = 1;
          break; // BYTE, UNSIGNED_BYTE
        case 5122:
        case 5123:
          elementSize = 2;
          break; // SHORT, UNSIGNED_SHORT
        case 5125:
        case 5126:
          elementSize = 4;
          break; // UNSIGNED_INT, FLOAT
        default:
          throw new Error(`Unsupported component type: ${componentType}`);
      }

      const numElements = length / elementSize;
      if (!Number.isInteger(numElements)) {
        throw new Error(`Invalid length ${length} for component type ${componentType}`);
      }

      if (length > buffer.byteLength) {
        throw new Error(`Buffer too small: need ${length} bytes, but buffer is ${buffer.byteLength} bytes`);
      }

      const ArrayType = GL_COMPONENT_TYPES[componentType];
      return new ArrayType(buffer, offset, numElements);
    } catch (error) {
      if (error.name !== "AbortError") {
        console.error("Error creating typed array:", {
          bufferSize: buffer?.byteLength,
          offset,
          length,
          componentType,
          error,
        });
      }
      throw error;
    }
  }

  async createBufferAttribute(accessorIndex) {
    if (!this.json) {
      throw new Error("No GLTF structure loaded");
    }

    const gltf = this.json;
    const accessor = gltf.accessors[accessorIndex];
    const bufferView = gltf.bufferViews[accessor.bufferView];

    try {
      const byteOffset = (bufferView.byteOffset || 0) + (accessor.byteOffset || 0);
      const components = this.getNumComponents(accessor.type);
      const count = accessor.count;
      const byteLength = count * components * this.getComponentSize(accessor.componentType);

      const array = await this.getBufferView(byteOffset, byteLength, accessor.componentType);

      const attribute = new BufferAttribute(array, components);

      if (accessor.min !== undefined) attribute.min = accessor.min;
      if (accessor.max !== undefined) attribute.max = accessor.max;

      return attribute;
    } catch (error) {
      if (error.name !== "AbortError") {
        console.error("Error creating buffer attribute:", {
          error,
          accessor,
          bufferView,
        });
      }

      throw error;
    }
  }

  getComponentSize(componentType) {
    switch (componentType) {
      case 5120: // BYTE
      case 5121: // UNSIGNED_BYTE
        return 1;
      case 5122: // SHORT
      case 5123: // UNSIGNED_SHORT
        return 2;
      case 5125: // UNSIGNED_INT
      case 5126: // FLOAT
        return 4;
      default:
        throw new Error(`Unknown component type: ${componentType}`);
    }
  }

  getNumComponents(type) {
    switch (type) {
      case "SCALAR":
        return 1;
      case "VEC2":
        return 2;
      case "VEC3":
        return 3;
      case "VEC4":
        return 4;
      case "MAT2":
        return 4;
      case "MAT3":
        return 9;
      case "MAT4":
        return 16;
      default:
        throw new Error(`Unknown type: ${type}`);
    }
  }

  async loadTextures() {
    if (!this.json.textures) return;

    const loadTexture = async (imageIndex) => {
      const image = this.json.images[imageIndex];

      if (image.uri) {
        const fullUrl = await this.loadController.resolveURL(image.uri);
        return this.textureLoader.loadAsync(fullUrl);
      } else if (image.bufferView !== undefined) {
        const bufferView = this.json.bufferViews[image.bufferView];
        const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121);
        const blob = new Blob([array], { type: image.mimeType });
        const url = URL.createObjectURL(blob);
        const texture = await this.textureLoader.loadAsync(url);
        URL.revokeObjectURL(url);
        texture.flipY = false;
        return texture;
      }
    };

    const texturePromises = [];
    for (let i = 0; i < this.json.textures.length; i++) {
      texturePromises.push(
        loadTexture(this.json.textures[i].source).then((texture) => this.textureCache.set(i, texture))
      );
    }
    await Promise.all(texturePromises);
  }

  loadMaterials() {
    if (!this.json.materials) return this.materials;

    for (let i = 0; i < this.json.materials.length; i++) {
      const materialDef = this.json.materials[i];

      const materialCacheKey = `material_${i}`;
      this.materialCache.set(materialCacheKey, {
        mesh: this.createMaterial(materialDef, GL_CONSTANTS.TRIANGLES),
        points: this.createMaterial(materialDef, GL_CONSTANTS.POINTS),
        lines: this.createMaterial(materialDef, GL_CONSTANTS.LINES),
      });

      this.materialCache.get(materialCacheKey).mesh.name = materialDef.name;
      this.materialCache.get(materialCacheKey).points.name = materialDef.name;
      this.materialCache.get(materialCacheKey).lines.name = materialDef.name;

      this.materials.set(i, this.materialCache.get(materialCacheKey).mesh);
    }
    return this.materials;
  }

  createMaterial(materialDef, primitiveMode = undefined) {
    const params = {};

    if (materialDef.pbrMetallicRoughness) {
      const pbr = materialDef.pbrMetallicRoughness;
      if (pbr.baseColorFactor) {
        params.color = new Color().fromArray(pbr.baseColorFactor);
        params.opacity = pbr.baseColorFactor[3];
        if (params.opacity < 1.0) params.transparent = true;
      }
      if (pbr.baseColorTexture) {
        params.map = this.textureCache.get(pbr.baseColorTexture.index);
      }
    }

    if (materialDef.emissiveFactor) {
      params.emissive = new Color().fromArray(materialDef.emissiveFactor);
    }

    if (materialDef.alphaMode === "BLEND") {
      params.transparent = true;
    }

    if (materialDef.doubleSided) {
      params.side = DoubleSide;
    }
    let material;

    if (primitiveMode === GL_CONSTANTS.POINTS) {
      params.sizeAttenuation = false;
      material = new PointsMaterial(params);
      material.sizeAttenuation = false;
    } else if (
      primitiveMode === GL_CONSTANTS.LINES ||
      primitiveMode === GL_CONSTANTS.LINE_STRIP ||
      primitiveMode === GL_CONSTANTS.LINE_LOOP
    ) {
      material = new LineBasicMaterial(params);
    } else {
      params.specular = 0x222222;
      params.shininess = 10;
      params.reflectivity = 0.05;
      params.polygonOffset = true;
      params.polygonOffsetFactor = 1;
      params.polygonOffsetUnits = 1;

      if (materialDef.normalTexture) {
        params.normalMap = this.textureCache.get(materialDef.normalTexture.index);
      }

      material = new MeshPhongMaterial(params);
    }

    return material;
  }

  getCachedMaterial(materialIndex, primitiveMode) {
    const materialCacheKey = `material_${materialIndex}`;
    const materialCache = this.materialCache.get(materialCacheKey);

    if (materialCache) {
      if (primitiveMode === GL_CONSTANTS.POINTS) {
        return materialCache.points;
      } else if (
        primitiveMode === GL_CONSTANTS.LINES ||
        primitiveMode === GL_CONSTANTS.LINE_STRIP ||
        primitiveMode === GL_CONSTANTS.LINE_LOOP
      ) {
        return materialCache.lines;
      } else {
        return materialCache.mesh;
      }
    }

    return null;
  }

  disposeMaterials() {
    this.textureCache.forEach((texture) => texture.dispose());
    this.textureCache.clear();

    this.materials.forEach((material) => {
      if (material.map) material.map.dispose();
      if (material.lightMap) material.lightMap.dispose();
      if (material.bumpMap) material.bumpMap.dispose();
      if (material.normalMap) material.normalMap.dispose();
      if (material.specularMap) material.specularMap.dispose();
      if (material.envMap) material.envMap.dispose();
      if (material.aoMap) material.aoMap.dispose();
      if (material.metalnessMap) material.metalnessMap.dispose();
      if (material.roughnessMap) material.roughnessMap.dispose();
      if (material.emissiveMap) material.emissiveMap.dispose();
      material.dispose();
    });
    this.materials.clear();

    this.materialCache.forEach((materialCache) => {
      if (materialCache.mesh) {
        if (materialCache.mesh.map) materialCache.mesh.map.dispose();
        if (materialCache.mesh.lightMap) materialCache.mesh.lightMap.dispose();
        if (materialCache.mesh.bumpMap) materialCache.mesh.bumpMap.dispose();
        if (materialCache.mesh.normalMap) materialCache.mesh.normalMap.dispose();
        if (materialCache.mesh.specularMap) materialCache.mesh.specularMap.dispose();
        if (materialCache.mesh.envMap) materialCache.mesh.envMap.dispose();
        if (materialCache.mesh.aoMap) materialCache.mesh.aoMap.dispose();
        if (materialCache.mesh.metalnessMap) materialCache.mesh.metalnessMap.dispose();
        if (materialCache.mesh.roughnessMap) materialCache.mesh.roughnessMap.dispose();
        if (materialCache.mesh.emissiveMap) materialCache.mesh.emissiveMap.dispose();
        materialCache.mesh.dispose();
      }

      if (materialCache.points) {
        if (materialCache.points.map) materialCache.points.map.dispose();
        materialCache.points.dispose();
      }

      if (materialCache.lines) {
        if (materialCache.lines.map) materialCache.lines.map.dispose();
        materialCache.lines.dispose();
      }
    });
    this.materialCache.clear();
  }

  estimateNodeSize(meshIndex) {
    if (!this.json.meshes) return 0;

    const meshDef = this.json.meshes[meshIndex];
    if (!meshDef || !meshDef.primitives) return 0;

    let totalSize = 0;
    for (const primitive of meshDef.primitives) {
      if (primitive.attributes) {
        for (const [, accessorIndex] of Object.entries(primitive.attributes)) {
          if (accessorIndex === undefined) continue;

          const accessor = this.json.accessors[accessorIndex];
          if (!accessor) continue;

          const numComponents = this.getNumComponents(accessor.type);
          const bytesPerComponent = this.getComponentSize(accessor.componentType);
          totalSize += accessor.count * numComponents * bytesPerComponent;
        }
      }

      if (primitive.indices !== undefined) {
        const accessor = this.json.accessors[primitive.indices];
        if (accessor) {
          const bytesPerComponent = this.getComponentSize(accessor.componentType);
          totalSize += accessor.count * bytesPerComponent;
        }
      }
    }

    return totalSize;
  }
}
