use super::*;

#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Bindings {
    pub(super) types: HashMap<String, ty_types::Type>,
    pub(super) constants: HashMap<String, ty_types::ConstExpression>,
}

impl Bindings {
    pub(super) fn new() -> Self {
        Self {
            types: HashMap::new(),
            constants: HashMap::new(),
        }
    }

    pub(super) fn contains(&self, name: &str) -> bool {
        self.types.contains_key(name) || self.constants.contains_key(name)
    }
}

pub(super) fn bindings_from_explicit_arguments(
    parameters: &[ty_types::GenericParameter],
    arguments: &[ty_types::GenericArgument],
) -> Option<Bindings> {
    if parameters.len() != arguments.len() {
        return None;
    }
    let mut bindings = Bindings::new();
    for (parameter, argument) in parameters.iter().zip(arguments) {
        match (&parameter.kind, argument) {
            (ty_types::GenericParameterKind::Type, ty_types::GenericArgument::Type(concrete)) => {
                bindings
                    .types
                    .insert(parameter.name.clone(), concrete.clone());
            }
            (
                ty_types::GenericParameterKind::Const(_),
                ty_types::GenericArgument::Const(constant),
            ) => {
                bindings
                    .constants
                    .insert(parameter.name.clone(), constant.clone());
            }
            _ => return None,
        }
    }
    Some(bindings)
}

pub(super) fn is_generic_name(name: &str, generic_names: &[&str]) -> bool {
    generic_names.contains(&name)
}

pub(super) fn unify(
    parameter_type: &ty_types::Type,
    argument_type: &ty_types::Type,
    bindings: &mut Bindings,
    generic_names: &[&str],
) -> Result<(), ()> {
    use ty_types::Type;

    let parameter_type = match parameter_type {
        Type::Named(name, arguments)
            if arguments.is_empty() && is_generic_name(name, generic_names) =>
        {
            return unify(
                &Type::Generic(name.clone()),
                argument_type,
                bindings,
                generic_names,
            );
        }
        other => other,
    };

    match (parameter_type, argument_type) {
        (Type::Generic(name), concrete) => {
            if let Some(existing) = bindings.types.get(name) {
                if existing != concrete {
                    return Err(());
                }
            } else {
                bindings.types.insert(name.clone(), concrete.clone());
            }
            Ok(())
        }
        (Type::Pointer(_, _, inner_parameter), Type::Pointer(_, _, inner_argument))
        | (Type::Reference(_, _, inner_parameter), Type::Reference(_, _, inner_argument))
        | (Type::Slice(_, inner_parameter), Type::Slice(_, inner_argument)) => {
            unify(inner_parameter, inner_argument, bindings, generic_names)
        }
        (
            Type::Array(inner_parameter, size_parameter),
            Type::Array(inner_argument, size_argument),
        )
        | (
            Type::Vector(inner_parameter, size_parameter),
            Type::Vector(inner_argument, size_argument),
        ) => {
            unify_const(size_parameter, size_argument, bindings, generic_names);
            unify(inner_parameter, inner_argument, bindings, generic_names)
        }
        (Type::Tuple(fields_parameter), Type::Tuple(fields_argument))
        | (Type::Enum(fields_parameter), Type::Enum(fields_argument)) => {
            if fields_parameter.len() != fields_argument.len() {
                return Err(());
            }
            for (field_parameter, field_argument) in fields_parameter.iter().zip(fields_argument) {
                unify(field_parameter, field_argument, bindings, generic_names)?;
            }
            Ok(())
        }
        (Type::Function(signature_parameter), Type::Function(signature_argument)) => {
            if signature_parameter.parameters.len() != signature_argument.parameters.len() {
                return Err(());
            }
            for (parameter, argument) in signature_parameter
                .parameters
                .iter()
                .zip(&signature_argument.parameters)
            {
                unify(parameter, argument, bindings, generic_names)?;
            }
            unify(
                &signature_parameter.return_type,
                &signature_argument.return_type,
                bindings,
                generic_names,
            )
        }
        (Type::Named(name1, args1), Type::Named(name2, args2))
            if name1 == name2 && args1.len() == args2.len() =>
        {
            for (arg1, arg2) in args1.iter().zip(args2) {
                match (arg1, arg2) {
                    (
                        ty_types::GenericArgument::Type(type1),
                        ty_types::GenericArgument::Type(type2),
                    ) => {
                        unify(type1, type2, bindings, generic_names)?;
                    }
                    (
                        ty_types::GenericArgument::Const(const1),
                        ty_types::GenericArgument::Const(const2),
                    ) => {
                        unify_const(const1, const2, bindings, generic_names);
                    }
                    _ => {}
                }
            }
            Ok(())
        }
        _ => Ok(()),
    }
}

pub(super) fn unify_const(
    parameter: &ty_types::ConstExpression,
    argument: &ty_types::ConstExpression,
    bindings: &mut Bindings,
    generic_names: &[&str],
) {
    if let ty_types::ConstExpression::Named(name) = parameter
        && generic_names.contains(&name.as_str())
    {
        bindings.constants.insert(name.clone(), argument.clone());
    }
}

pub(super) fn contains_unresolved_generic(
    checked_type: &ty_types::Type,
    generic_parameter_names: &HashSet<String>,
) -> bool {
    match checked_type {
        ty_types::Type::Generic(name) => generic_parameter_names.contains(name),
        ty_types::Type::Named(name, arguments) if arguments.is_empty() => {
            generic_parameter_names.contains(name)
        }
        ty_types::Type::Named(_, arguments) => arguments.iter().any(|argument| match argument {
            ty_types::GenericArgument::Type(inner) => {
                contains_unresolved_generic(inner, generic_parameter_names)
            }
            ty_types::GenericArgument::Const(ty_types::ConstExpression::Named(name)) => {
                generic_parameter_names.contains(name)
            }
            _ => false,
        }),
        ty_types::Type::Pointer(_, _, inner)
        | ty_types::Type::Reference(_, _, inner)
        | ty_types::Type::Slice(_, inner) => {
            contains_unresolved_generic(inner, generic_parameter_names)
        }
        ty_types::Type::Array(inner, size) | ty_types::Type::Vector(inner, size) => {
            contains_unresolved_generic(inner, generic_parameter_names)
                || const_contains_unresolved(size, generic_parameter_names)
        }
        ty_types::Type::Tuple(fields) | ty_types::Type::Enum(fields) => fields
            .iter()
            .any(|field| contains_unresolved_generic(field, generic_parameter_names)),
        ty_types::Type::Function(signature) => {
            signature
                .parameters
                .iter()
                .any(|parameter| contains_unresolved_generic(parameter, generic_parameter_names))
                || contains_unresolved_generic(&signature.return_type, generic_parameter_names)
        }
        _ => false,
    }
}

pub(super) fn const_contains_unresolved(
    expression: &ty_types::ConstExpression,
    generic_parameter_names: &HashSet<String>,
) -> bool {
    use ty_types::ConstExpression as C;
    match expression {
        C::Named(name) => generic_parameter_names.contains(name),
        C::Integer(_) => false,
        C::Add(left, right)
        | C::Subtract(left, right)
        | C::Multiply(left, right)
        | C::Divide(left, right)
        | C::Modulo(left, right)
        | C::BitwiseAnd(left, right)
        | C::BitwiseOr(left, right)
        | C::BitwiseXor(left, right)
        | C::ShiftLeft(left, right)
        | C::ShiftRight(left, right) => {
            const_contains_unresolved(left, generic_parameter_names)
                || const_contains_unresolved(right, generic_parameter_names)
        }
        C::Call(_, arguments) => arguments
            .iter()
            .any(|argument| const_contains_unresolved(argument, generic_parameter_names)),
        C::Field(inner, _) => const_contains_unresolved(inner, generic_parameter_names),
        C::Construction(fields) => fields
            .iter()
            .any(|(_, value)| const_contains_unresolved(value, generic_parameter_names)),
    }
}

pub(super) fn mangle_name(
    name: &str,
    bindings: &Bindings,
    parameters: &[ty_types::GenericParameter],
) -> String {
    let mut mangled = name.to_string();
    for parameter in parameters {
        match &parameter.kind {
            ty_types::GenericParameterKind::Type => {
                if let Some(concrete) = bindings.types.get(&parameter.name) {
                    mangled.push('$');
                    mangled.push_str(&mangle_type(concrete));
                }
            }
            ty_types::GenericParameterKind::Const(_) => {
                if let Some(constant) = bindings.constants.get(&parameter.name) {
                    mangled.push('$');
                    mangled.push_str(&mangle_const(constant));
                }
            }
        }
    }
    mangled
}

pub(super) fn mangle_const(constant: &ty_types::ConstExpression) -> String {
    use ty_types::ConstExpression as C;
    match constant {
        C::Integer(value) => value.to_string(),
        C::Named(name) => name.clone(),
        C::Add(left, right) => format!("{}_plus_{}", mangle_const(left), mangle_const(right)),
        C::Subtract(left, right) => {
            format!("{}_minus_{}", mangle_const(left), mangle_const(right))
        }
        C::Multiply(left, right) => {
            format!("{}_times_{}", mangle_const(left), mangle_const(right))
        }
        C::Divide(left, right) => format!("{}_div_{}", mangle_const(left), mangle_const(right)),
        C::Modulo(left, right) => format!("{}_mod_{}", mangle_const(left), mangle_const(right)),
        C::BitwiseAnd(left, right) => {
            format!("{}_band_{}", mangle_const(left), mangle_const(right))
        }
        C::BitwiseOr(left, right) => {
            format!("{}_bor_{}", mangle_const(left), mangle_const(right))
        }
        C::BitwiseXor(left, right) => {
            format!("{}_bxor_{}", mangle_const(left), mangle_const(right))
        }
        C::ShiftLeft(left, right) => {
            format!("{}_shl_{}", mangle_const(left), mangle_const(right))
        }
        C::ShiftRight(left, right) => {
            format!("{}_shr_{}", mangle_const(left), mangle_const(right))
        }
        C::Call(name, arguments) => {
            let argument_strings: Vec<_> = arguments.iter().map(mangle_const).collect();
            format!("{name}_{}", argument_strings.join("_"))
        }
        C::Field(inner, field) => format!("{}_{field}", mangle_const(inner)),
        C::Construction(fields) => {
            let parts: Vec<_> = fields
                .iter()
                .map(|(name, value)| format!("{name}_{}", mangle_const(value)))
                .collect();
            parts.join("_")
        }
    }
}

pub(super) fn mangle_type(mangled_type: &ty_types::Type) -> String {
    use ty_types::Type;
    match mangled_type {
        Type::Bool => "bool".into(),
        Type::Int(width, signedness) => {
            let prefix = match signedness {
                ty_types::Signedness::Signed => "i",
                ty_types::Signedness::Unsigned => "u",
            };
            let bits = match width {
                ty_types::IntWidth::W8 => "8",
                ty_types::IntWidth::W16 => "16",
                ty_types::IntWidth::W32 => "32",
                ty_types::IntWidth::W64 => "64",
                ty_types::IntWidth::W128 => "128",
                ty_types::IntWidth::WSize => "size",
            };
            format!("{prefix}{bits}")
        }
        Type::Float(width) => match width {
            ty_types::FloatWidth::W32 => "f32".into(),
            ty_types::FloatWidth::W64 => "f64".into(),
        },
        Type::Named(name, arguments) => {
            if arguments.is_empty() {
                name.clone()
            } else {
                let mut result = name.clone();
                for argument in arguments {
                    result.push('$');
                    match argument {
                        ty_types::GenericArgument::Type(inner) => {
                            result.push_str(&mangle_type(inner));
                        }
                        ty_types::GenericArgument::Const(constant) => {
                            result.push_str(&mangle_const(constant));
                        }
                        ty_types::GenericArgument::Lifetime(_) => {}
                    }
                }
                result
            }
        }
        Type::Pointer(_, _, inner) => format!("ptr_{}", mangle_type(inner)),
        Type::Reference(_, _, inner) => format!("ref_{}", mangle_type(inner)),
        _ => "unknown".into(),
    }
}
