use blake3::Hasher;
use rbx_dom_weak::types::{ContentType, PhysicalProperties, Variant, Vector3};

macro_rules! round {
    ($value:expr) => {
        (($value * 10.0).round() / 10.0)
    };
}

macro_rules! n_hash {
    ($hash:ident, $($num:expr),*) => {
        {$(
            $hash.update(&($num).to_le_bytes());
        )*}
    };
}

macro_rules! hash {
    ($hash:ident, $value:expr) => {{
        $hash.update($value);
    }};
}

/// Places `value` into the provided hasher.
pub fn hash_variant(hasher: &mut Hasher, value: &Variant) {
    // We need to round floats, though I'm not sure to what degree we can
    // realistically do that.
    match value {
        Variant::Attributes(attrs) => {
            let mut sorted: Vec<(&String, &Variant)> = attrs.iter().collect();
            sorted.sort_unstable_by_key(|(name, _)| *name);
            for (name, attribute) in sorted {
                hasher.update(name.as_bytes());
                hash_variant(hasher, attribute);
            }
        }
        Variant::Axes(a) => hash!(hasher, &[a.bits()]),
        Variant::BinaryString(bytes) => hash!(hasher, bytes.as_ref()),
        Variant::Bool(bool) => hash!(hasher, &[*bool as u8]),
        Variant::BrickColor(color) => n_hash!(hasher, *color as u16),
        Variant::CFrame(cf) => {
            vector_hash(hasher, cf.position);
            vector_hash(hasher, cf.orientation.x);
            vector_hash(hasher, cf.orientation.y);
            vector_hash(hasher, cf.orientation.z);
        }
        Variant::Color3(color) => {
            n_hash!(hasher, round!(color.r), round!(color.g), round!(color.b))
        }
        Variant::Color3uint8(color) => hash!(hasher, &[color.r, color.b, color.g]),
        Variant::ColorSequence(seq) => {
            let mut new = Vec::with_capacity(seq.keypoints.len());
            for keypoint in &seq.keypoints {
                new.push(keypoint);
            }
            new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap());
            for keypoint in new {
                n_hash!(
                    hasher,
                    round!(keypoint.time),
                    round!(keypoint.color.r),
                    round!(keypoint.color.g),
                    round!(keypoint.color.b)
                )
            }
        }
        Variant::Content(content) => match content.value() {
            ContentType::None => {
                hash!(hasher, &[0]);
            }
            ContentType::Uri(uri) => {
                hash!(hasher, &[1]);
                hash!(hasher, uri.as_bytes());
            }
            ContentType::Object(referent) => {
                hash!(hasher, &[2]);
                hash!(hasher, referent.to_string().as_bytes())
            }
            other => {
                panic!("the ContentType {other:?} cannot be hashed as a Variant")
            }
        },
        Variant::ContentId(content) => {
            let s: &str = content.as_ref();
            hash!(hasher, s.as_bytes())
        }
        Variant::Enum(e) => n_hash!(hasher, e.to_u32()),
        Variant::EnumItem(e) => {
            hash!(hasher, e.ty.as_bytes());
            n_hash!(hasher, e.value);
        }
        Variant::Faces(f) => hash!(hasher, &[f.bits()]),
        Variant::Float32(n) => n_hash!(hasher, round!(*n)),
        Variant::Float64(n) => n_hash!(hasher, round!(n)),
        Variant::Font(f) => {
            n_hash!(hasher, f.weight as u16);
            n_hash!(hasher, f.style as u8);
            hash!(hasher, f.family.as_bytes());
            if let Some(cache) = &f.cached_face_id {
                hash!(hasher, &[0x01]);
                hash!(hasher, cache.as_bytes());
            } else {
                hash!(hasher, &[0x00]);
            }
        }
        Variant::Int32(n) => n_hash!(hasher, n),
        Variant::Int64(n) => n_hash!(hasher, n),
        Variant::MaterialColors(n) => hash!(hasher, n.encode().as_slice()),
        Variant::NetAssetRef(net_asset) => hash!(hasher, net_asset.hash().as_bytes()),
        Variant::NumberRange(nr) => n_hash!(hasher, round!(nr.max), round!(nr.min)),
        Variant::NumberSequence(seq) => {
            let mut new = Vec::with_capacity(seq.keypoints.len());
            for keypoint in &seq.keypoints {
                new.push(keypoint);
            }
            new.sort_unstable_by(|a, b| round!(a.time).partial_cmp(&round!(b.time)).unwrap());
            for keypoint in new {
                n_hash!(
                    hasher,
                    round!(keypoint.time),
                    round!(keypoint.value),
                    round!(keypoint.envelope)
                )
            }
        }
        Variant::OptionalCFrame(maybe_cf) => {
            if let Some(cf) = maybe_cf {
                hash!(hasher, &[0x01]);
                vector_hash(hasher, cf.position);
                vector_hash(hasher, cf.orientation.x);
                vector_hash(hasher, cf.orientation.y);
                vector_hash(hasher, cf.orientation.z);
            } else {
                hash!(hasher, &[0x00]);
            }
        }
        Variant::PhysicalProperties(properties) => match properties {
            PhysicalProperties::Default => hash!(hasher, &[0x00]),
            PhysicalProperties::Custom(custom) => {
                hash!(hasher, &[0x00]);
                n_hash!(
                    hasher,
                    round!(custom.density()),
                    round!(custom.friction()),
                    round!(custom.elasticity()),
                    round!(custom.friction_weight()),
                    round!(custom.elasticity_weight()),
                    round!(custom.acoustic_absorption())
                )
            }
        },
        Variant::Ray(ray) => {
            vector_hash(hasher, ray.origin);
            vector_hash(hasher, ray.direction);
        }
        Variant::Rect(rect) => n_hash!(
            hasher,
            round!(rect.max.x),
            round!(rect.max.y),
            round!(rect.min.x),
            round!(rect.min.y)
        ),
        Variant::Ref(referent) => hash!(hasher, referent.to_string().as_bytes()),
        Variant::Region3(region) => {
            vector_hash(hasher, region.max);
            vector_hash(hasher, region.min);
        }
        Variant::Region3int16(region) => {
            n_hash!(
                hasher,
                region.max.x,
                region.max.y,
                region.max.z,
                region.min.x,
                region.min.y,
                region.min.z
            )
        }
        Variant::SecurityCapabilities(capabilities) => n_hash!(hasher, capabilities.bits()),
        Variant::SharedString(sstr) => hash!(hasher, sstr.hash().as_bytes()),
        Variant::String(str) => hash!(hasher, str.as_bytes()),
        Variant::Tags(tags) => {
            let mut dupe: Vec<&str> = tags.iter().collect();
            dupe.sort_unstable();
            for tag in dupe {
                hash!(hasher, tag.as_bytes())
            }
        }
        Variant::UDim(udim) => n_hash!(hasher, round!(udim.scale), udim.offset),
        Variant::UDim2(udim) => n_hash!(
            hasher,
            round!(udim.y.scale),
            udim.y.offset,
            round!(udim.x.scale),
            udim.x.offset
        ),
        Variant::Vector2(v2) => n_hash!(hasher, round!(v2.x), round!(v2.y)),
        Variant::Vector2int16(v2) => n_hash!(hasher, v2.x, v2.y),
        Variant::Vector3(v3) => vector_hash(hasher, *v3),
        Variant::Vector3int16(v3) => n_hash!(hasher, v3.x, v3.y, v3.z),

        // Hashing UniqueId properties doesn't make sense
        Variant::UniqueId(_) => (),

        unknown => {
            log::warn!(
                "Encountered unknown Variant {:?} while hashing",
                unknown.ty()
            )
        }
    }
}

fn vector_hash(hasher: &mut Hasher, vector: Vector3) {
    n_hash!(hasher, round!(vector.x), round!(vector.y), round!(vector.z))
}
