use rue_diagnostic::DiagnosticKind;
use rue_hir::{Hir, TypePath, UnaryOp, Value};
use rue_types::{Type, Union};

use crate::{Compiler, GetTextRange};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Field<'a> {
    Named(&'a str),
    First,
    Rest,
}

#[derive(Debug, Clone)]
pub enum FieldResult {
    Value(Value),
    Unknown,
    Error,
}

pub fn compile_pair_fields(
    ctx: &mut Compiler,
    node: &impl GetTextRange,
    expr: &Value,
) -> (Value, Value) {
    let first = compile_field(ctx, expr.clone(), &Field::First);
    let rest = compile_field(ctx, expr.clone(), &Field::Rest);

    let (FieldResult::Value(first), FieldResult::Value(rest)) = (first, rest) else {
        let name = ctx.type_name(expr.ty);
        ctx.diagnostic(node, DiagnosticKind::CannotDestructurePair(name));
        return (
            ctx.builtins().unresolved.clone(),
            ctx.builtins().unresolved.clone(),
        );
    };

    (first, rest)
}

pub fn compile_field(ctx: &mut Compiler, expr: Value, name: &Field<'_>) -> FieldResult {
    let ty = rue_types::unwrap_semantic(ctx.types_mut(), expr.ty, true);

    match ctx.ty(ty).clone() {
        Type::Apply(_) | Type::Alias(_) | Type::Ref(_) => unreachable!(),
        Type::Unresolved => FieldResult::Unknown,
        Type::Generic(_) | Type::Function(_) | Type::Never | Type::Any => FieldResult::Unknown,
        Type::Atom(_) => {
            if let &Field::Named(name) = name
                && name == "length"
            {
                let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::Strlen, expr.hir));
                FieldResult::Value(Value::new(hir, ctx.builtins().types.int))
            } else {
                FieldResult::Unknown
            }
        }
        Type::Pair(_) | Type::Union(_) => {
            let pairs = rue_types::extract_pairs(ctx.types_mut(), ty, true);

            match name {
                Field::Named("first") | Field::First if !pairs.is_empty() => {
                    let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::First, expr.hir));

                    let first = if pairs.len() == 1 {
                        pairs[0].first
                    } else {
                        ctx.alloc_type(Type::Union(Union::new(
                            pairs.iter().map(|pair| pair.first).collect(),
                        )))
                    };

                    let mut value = Value::new(hir, first);

                    if let Some(mut reference) = expr.reference {
                        reference.path.push(TypePath::First);
                        value = value.with_reference(reference);
                    }

                    FieldResult::Value(value)
                }
                Field::Named("rest") | Field::Rest if !pairs.is_empty() => {
                    let hir = ctx.alloc_hir(Hir::Unary(UnaryOp::Rest, expr.hir));

                    let rest = if pairs.len() == 1 {
                        pairs[0].rest
                    } else {
                        ctx.alloc_type(Type::Union(Union::new(
                            pairs.iter().map(|pair| pair.rest).collect(),
                        )))
                    };

                    let mut value = Value::new(hir, rest);

                    if let Some(mut reference) = expr.reference {
                        reference.path.push(TypePath::Rest);
                        value = value.with_reference(reference);
                    }

                    FieldResult::Value(value)
                }
                _ => FieldResult::Unknown,
            }
        }
        Type::Struct(ty) => {
            let &Field::Named(name) = name else {
                return FieldResult::Unknown;
            };

            let Some(index) = ty.fields.get_index_of(name) else {
                return FieldResult::Unknown;
            };

            let mut hir = expr.hir;
            let mut field_type = ty.inner;
            let mut reference = expr.reference;

            let needs_first = index + 1 < ty.fields.len() || ty.nil_terminated;

            for i in 0..index {
                hir = ctx.alloc_hir(Hir::Unary(UnaryOp::Rest, hir));

                let pairs = rue_types::extract_pairs(ctx.types_mut(), field_type, true);

                if pairs.is_empty() || (pairs.len() > 1 && (i + 1 < index || needs_first)) {
                    return FieldResult::Error;
                }

                field_type = if pairs.len() == 1 {
                    pairs[0].rest
                } else {
                    ctx.alloc_type(Type::Union(Union::new(
                        pairs.into_iter().map(|pair| pair.rest).collect(),
                    )))
                };

                if let Some(reference) = reference.as_mut() {
                    reference.path.push(TypePath::Rest);
                }
            }

            if needs_first {
                hir = ctx.alloc_hir(Hir::Unary(UnaryOp::First, hir));

                let pairs = rue_types::extract_pairs(ctx.types_mut(), field_type, true);

                if pairs.is_empty() {
                    return FieldResult::Error;
                }

                field_type = if pairs.len() == 1 {
                    pairs[0].first
                } else {
                    ctx.alloc_type(Type::Union(Union::new(
                        pairs.into_iter().map(|pair| pair.first).collect(),
                    )))
                };

                if let Some(reference) = reference.as_mut() {
                    reference.path.push(TypePath::First);
                }
            }

            let mut value = Value::new(hir, field_type);

            if let Some(reference) = reference {
                value = value.with_reference(reference);
            }

            FieldResult::Value(value)
        }
    }
}
