import 'package:vector_math/vector_math.dart';

/// In-memory representation of the parts of glTF 2.0 that flutter_scene
/// consumes. Field names match the glTF spec (camelCase). Indexes into other
/// arrays are stored as `int?` and resolved at use time.

class GltfDocument {
  GltfDocument({
    this.scene,
    this.scenes = const [],
    this.nodes = const [],
    this.meshes = const [],
    this.accessors = const [],
    this.bufferViews = const [],
    this.buffers = const [],
    this.materials = const [],
    this.textures = const [],
    this.images = const [],
    this.samplers = const [],
    this.skins = const [],
    this.animations = const [],
    this.lights = const [],
    this.materialsVariants = const [],
  });

  final int? scene;
  final List<GltfScene> scenes;
  final List<GltfNode> nodes;
  final List<GltfMesh> meshes;
  final List<GltfAccessor> accessors;
  final List<GltfBufferView> bufferViews;
  final List<GltfBuffer> buffers;
  final List<GltfMaterial> materials;
  final List<GltfTexture> textures;
  final List<GltfImage> images;
  final List<GltfSampler> samplers;
  final List<GltfSkin> skins;
  final List<GltfAnimation> animations;

  /// Punctual lights declared by the `KHR_lights_punctual` extension, indexed
  /// by [GltfNode.light]. Empty when the extension is absent.
  final List<GltfPunctualLight> lights;

  /// Variant names declared by the `KHR_materials_variants` extension, in
  /// declaration order. Empty when the extension is absent. Primitive
  /// mappings ([GltfMeshPrimitive.variantMappings]) index into this list.
  final List<String> materialsVariants;
}

/// A `KHR_lights_punctual` light definition. Fields match the extension spec.
/// [innerConeAngle]/[outerConeAngle] are only meaningful for `spot` lights.
class GltfPunctualLight {
  GltfPunctualLight({
    this.name,
    required this.type,
    Vector3? color,
    this.intensity = 1.0,
    this.range,
    this.innerConeAngle = 0.0,
    this.outerConeAngle = 0.7853981633974483, // pi / 4
  }) : color = color ?? Vector3(1.0, 1.0, 1.0);

  final String? name;

  /// One of `directional`, `point`, or `spot`.
  final String type;
  final Vector3 color;
  final double intensity;

  /// Distance cutoff, or null for infinite range (point and spot only).
  final double? range;
  final double innerConeAngle;
  final double outerConeAngle;
}

class GltfScene {
  GltfScene({this.name, this.nodes = const []});
  final String? name;
  final List<int> nodes;
}

class GltfNode {
  GltfNode({
    this.name,
    this.mesh,
    this.skin,
    this.light,
    this.children = const [],
    this.matrix,
    this.translation,
    this.rotation,
    this.scale,
  });

  final String? name;
  final int? mesh;
  final int? skin;

  /// Index into [GltfDocument.lights] (`KHR_lights_punctual`), or null.
  final int? light;
  final List<int> children;

  /// If [matrix] is set, [translation]/[rotation]/[scale] are ignored.
  final Matrix4? matrix;
  final Vector3? translation;
  final Quaternion? rotation;
  final Vector3? scale;
}

/// The engine-side name for the glTF node at [index].
///
/// glTF lets nodes omit their name, and many real exports leave most
/// nodes unnamed. Animation channels resolve their target nodes by
/// name, so every unnamed node sharing the empty string would make all
/// channels collide on the same node. Synthesizing a unique,
/// index-derived name keeps channel binding correct. Both the offline
/// scene emitter and the runtime GLB importer must call this so a
/// model animates identically whichever path imported it.
///
/// Named nodes are returned unchanged. The synthetic `node_<index>`
/// form could in theory collide with an authored name; resolving that
/// fully would mean index- or path-based channel binding.
String resolveGltfNodeName(String? gltfName, int index) {
  if (gltfName != null && gltfName.isNotEmpty) return gltfName;
  return 'node_$index';
}

class GltfMesh {
  GltfMesh({this.name, this.primitives = const []});
  final String? name;
  final List<GltfMeshPrimitive> primitives;
}

class GltfMeshPrimitive {
  GltfMeshPrimitive({
    this.attributes = const {},
    this.indices,
    this.material,
    this.mode = 4,
    this.variantMappings = const {},
  });

  /// Maps glTF attribute names ('POSITION', 'NORMAL', 'TEXCOORD_0',
  /// 'COLOR_0', 'JOINTS_0', 'WEIGHTS_0', 'TANGENT') to accessor indexes.
  final Map<String, int> attributes;
  final int? indices;
  final int? material;

  /// Primitive topology. 4 = TRIANGLES (the only mode flutter_scene supports).
  final int mode;

  /// `KHR_materials_variants` mappings, variant index (into
  /// [GltfDocument.materialsVariants]) to material index. Empty when the
  /// primitive declares no mappings; [material] stays the default.
  final Map<int, int> variantMappings;
}

/// Component types from glTF spec section 5.1.1.
enum GltfComponentType {
  byte_(5120, 1, true),
  unsignedByte(5121, 1, false),
  short(5122, 2, true),
  unsignedShort(5123, 2, false),
  unsignedInt(5125, 4, false),
  float(5126, 4, true);

  const GltfComponentType(this.glValue, this.bytes, this.signed);
  final int glValue;
  final int bytes;
  final bool signed;

  static GltfComponentType fromGlValue(int v) {
    return values.firstWhere(
      (e) => e.glValue == v,
      orElse: () => throw FormatException('Unknown glTF componentType: $v'),
    );
  }
}

/// Accessor "type" enum from spec section 5.1.1.
enum GltfAccessorType {
  scalar('SCALAR', 1),
  vec2('VEC2', 2),
  vec3('VEC3', 3),
  vec4('VEC4', 4),
  mat2('MAT2', 4),
  mat3('MAT3', 9),
  mat4('MAT4', 16);

  const GltfAccessorType(this.name_, this.componentCount);
  final String name_;
  final int componentCount;

  static GltfAccessorType fromName(String s) {
    return values.firstWhere(
      (e) => e.name_ == s,
      orElse: () => throw FormatException('Unknown glTF accessor type: $s'),
    );
  }
}

class GltfAccessor {
  GltfAccessor({
    required this.componentType,
    required this.count,
    required this.type,
    this.bufferView,
    this.byteOffset = 0,
    this.normalized = false,
    this.min,
    this.max,
  });

  final GltfComponentType componentType;
  final int count;
  final GltfAccessorType type;
  final int? bufferView;
  final int byteOffset;
  final bool normalized;

  /// Componentwise min/max bounds, when supplied by the glTF asset. The
  /// spec requires these on POSITION accessors, so consumers can use them
  /// to skip a vertex scan when computing bounding volumes.
  final List<double>? min;
  final List<double>? max;
}

class GltfBufferView {
  GltfBufferView({
    required this.buffer,
    required this.byteLength,
    this.byteOffset = 0,
    this.byteStride,
  });

  final int buffer;
  final int byteLength;
  final int byteOffset;
  final int? byteStride;
}

class GltfBuffer {
  GltfBuffer({required this.byteLength, this.uri});
  final int byteLength;
  final String? uri;
}

class GltfMaterial {
  GltfMaterial({
    this.name,
    this.pbrMetallicRoughness,
    this.normalTexture,
    this.occlusionTexture,
    this.emissiveTexture,
    this.emissiveFactor = const [0.0, 0.0, 0.0],
    this.alphaMode = 'OPAQUE',
    this.alphaCutoff = 0.5,
    this.doubleSided = false,
    this.unlit = false,
    this.anisotropy,
    this.clearcoat,
    this.diffuseTransmission,
    this.dispersion = 0.0,
    this.emissiveStrength = 1.0,
    this.ior = 1.5,
    this.iridescence,
    this.sheen,
    this.specular,
    this.transmission,
    this.volume,
  });

  final String? name;
  final GltfPbrMetallicRoughness? pbrMetallicRoughness;
  final GltfTextureInfo? normalTexture;
  final GltfTextureInfo? occlusionTexture;
  final GltfTextureInfo? emissiveTexture;
  final List<double> emissiveFactor;
  final String alphaMode;
  final double alphaCutoff;
  final bool doubleSided;
  final bool unlit;
  final GltfMaterialAnisotropy? anisotropy;
  final GltfMaterialClearcoat? clearcoat;
  final GltfMaterialDiffuseTransmission? diffuseTransmission;
  final double dispersion;
  final double emissiveStrength;
  final double ior;
  final GltfMaterialIridescence? iridescence;
  final GltfMaterialSheen? sheen;
  final GltfMaterialSpecular? specular;
  final GltfMaterialTransmission? transmission;
  final GltfMaterialVolume? volume;

  bool get requiresPhysicalMaterial =>
      anisotropy != null ||
      clearcoat != null ||
      diffuseTransmission != null ||
      dispersion != 0.0 ||
      ior != 1.5 ||
      iridescence != null ||
      sheen != null ||
      specular != null ||
      transmission != null ||
      volume != null;
}

class GltfMaterialAnisotropy {
  GltfMaterialAnisotropy({
    this.strength = 0.0,
    this.rotation = 0.0,
    this.texture,
  });

  final double strength;
  final double rotation;
  final GltfTextureInfo? texture;
}

class GltfMaterialClearcoat {
  GltfMaterialClearcoat({
    this.factor = 0.0,
    this.texture,
    this.roughnessFactor = 0.0,
    this.roughnessTexture,
    this.normalTexture,
  });

  final double factor;
  final GltfTextureInfo? texture;
  final double roughnessFactor;
  final GltfTextureInfo? roughnessTexture;
  final GltfTextureInfo? normalTexture;
}

class GltfMaterialDiffuseTransmission {
  GltfMaterialDiffuseTransmission({
    this.factor = 0.0,
    this.texture,
    this.colorFactor = const [1.0, 1.0, 1.0],
    this.colorTexture,
  });

  final double factor;
  final GltfTextureInfo? texture;
  final List<double> colorFactor;
  final GltfTextureInfo? colorTexture;
}

class GltfMaterialIridescence {
  GltfMaterialIridescence({
    this.factor = 0.0,
    this.texture,
    this.ior = 1.3,
    this.thicknessMinimum = 100.0,
    this.thicknessMaximum = 400.0,
    this.thicknessTexture,
  });

  final double factor;
  final GltfTextureInfo? texture;
  final double ior;
  final double thicknessMinimum;
  final double thicknessMaximum;
  final GltfTextureInfo? thicknessTexture;
}

class GltfMaterialSheen {
  GltfMaterialSheen({
    this.colorFactor = const [0.0, 0.0, 0.0],
    this.colorTexture,
    this.roughnessFactor = 0.0,
    this.roughnessTexture,
  });

  final List<double> colorFactor;
  final GltfTextureInfo? colorTexture;
  final double roughnessFactor;
  final GltfTextureInfo? roughnessTexture;
}

class GltfMaterialSpecular {
  GltfMaterialSpecular({
    this.factor = 1.0,
    this.texture,
    this.colorFactor = const [1.0, 1.0, 1.0],
    this.colorTexture,
  });

  final double factor;
  final GltfTextureInfo? texture;
  final List<double> colorFactor;
  final GltfTextureInfo? colorTexture;
}

class GltfMaterialTransmission {
  GltfMaterialTransmission({this.factor = 0.0, this.texture});

  final double factor;
  final GltfTextureInfo? texture;
}

class GltfMaterialVolume {
  GltfMaterialVolume({
    this.thicknessFactor = 0.0,
    this.thicknessTexture,
    this.attenuationDistance = double.infinity,
    this.attenuationColor = const [1.0, 1.0, 1.0],
  });

  final double thicknessFactor;
  final GltfTextureInfo? thicknessTexture;
  final double attenuationDistance;
  final List<double> attenuationColor;
}

class GltfPbrMetallicRoughness {
  GltfPbrMetallicRoughness({
    this.baseColorFactor = const [1.0, 1.0, 1.0, 1.0],
    this.baseColorTexture,
    this.metallicFactor = 1.0,
    this.roughnessFactor = 1.0,
    this.metallicRoughnessTexture,
  });

  final List<double> baseColorFactor;
  final GltfTextureInfo? baseColorTexture;
  final double metallicFactor;
  final double roughnessFactor;
  final GltfTextureInfo? metallicRoughnessTexture;
}

class GltfTextureInfo {
  GltfTextureInfo({
    required this.index,
    this.texCoord = 0,
    this.scale,
    this.strength,
    this.transform,
  });
  final int index;
  final int texCoord;

  /// Set on normal textures (otherwise null).
  final double? scale;

  /// Set on occlusion textures (otherwise null).
  final double? strength;

  final GltfTextureTransform? transform;
}

class GltfTextureTransform {
  GltfTextureTransform({
    this.offset = const [0.0, 0.0],
    this.rotation = 0.0,
    this.scale = const [1.0, 1.0],
    this.texCoord,
  });

  final List<double> offset;
  final double rotation;
  final List<double> scale;
  final int? texCoord;
}

class GltfTexture {
  GltfTexture({this.source, this.sampler});
  final int? source;
  final int? sampler;
}

class GltfImage {
  GltfImage({this.uri, this.bufferView, this.mimeType});
  final String? uri;
  final int? bufferView;
  final String? mimeType;
}

class GltfSampler {
  GltfSampler({
    this.magFilter,
    this.minFilter,
    this.wrapS = 10497,
    this.wrapT = 10497,
  });
  final int? magFilter;
  final int? minFilter;
  final int wrapS;
  final int wrapT;
}

class GltfSkin {
  GltfSkin({
    this.name,
    this.inverseBindMatrices,
    this.skeleton,
    this.joints = const [],
  });
  final String? name;
  final int? inverseBindMatrices;
  final int? skeleton;
  final List<int> joints;
}

class GltfAnimation {
  GltfAnimation({
    this.name,
    this.channels = const [],
    this.samplers = const [],
  });
  final String? name;
  final List<GltfAnimationChannel> channels;
  final List<GltfAnimationSampler> samplers;
}

class GltfAnimationChannel {
  GltfAnimationChannel({
    required this.sampler,
    required this.targetNode,
    required this.targetPath,
  });
  final int sampler;
  final int? targetNode;

  /// One of 'translation', 'rotation', 'scale', 'weights'.
  final String targetPath;
}

class GltfAnimationSampler {
  GltfAnimationSampler({
    required this.input,
    required this.output,
    this.interpolation = 'LINEAR',
  });
  final int input;
  final int output;
  final String interpolation;
}
