use crate::functions::extend_type_pack::extend_type_pack;
use crate::functions::follow_type::follow_type_id;
use crate::functions::is_pending::is_pending;
use crate::functions::solve_function_call::solve_function_call;
use crate::functions::try_distribute_type_function_app::try_distribute_type_function_app;
use crate::records::never_type::NeverType;
use crate::records::type_function_context::TypeFunctionContext;
use crate::records::type_function_reduction_result::TypeFunctionReductionResult;
use crate::type_aliases::error_vec::ErrorVec;
use crate::type_aliases::type_id::TypeId;
use crate::type_aliases::type_pack_id::TypePackId;
use luaur_ast::records::location::Location;
use luaur_common::FFlag;

pub fn concat_type_function(
    instance: TypeId,
    type_params: alloc::vec::Vec<TypeId>,
    pack_params: alloc::vec::Vec<TypePackId>,
    ctx: *mut TypeFunctionContext,
) -> TypeFunctionReductionResult {
    let ctx_ref = unsafe { &*ctx };
    if type_params.len() != 2 || !pack_params.is_empty() {
        unsafe {
            (*ctx_ref.ice.as_ptr()).ice_string("concat type function: encountered a type function instance without the required argument structure")
        };
        luaur_common::macros::luau_assert::LUAU_ASSERT!(false);
    }

    let lhs_ty = unsafe { follow_type_id(type_params[0]) };
    let rhs_ty = unsafe { follow_type_id(type_params[1]) };

    if lhs_ty == instance || rhs_ty == instance {
        return TypeFunctionReductionResult {
            result: Some(unsafe { (*ctx_ref.builtins.as_ptr()).neverType }),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    if is_pending(lhs_ty, ctx_ref.solver) {
        return TypeFunctionReductionResult {
            result: None,
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::from([lhs_ty]),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    } else if is_pending(rhs_ty, ctx_ref.solver) {
        return TypeFunctionReductionResult {
            result: None,
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::from([rhs_ty]),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    let norm_lhs_ty_ref = unsafe { (*ctx_ref.normalizer.as_ptr()).normalize(lhs_ty) };
    let norm_rhs_ty_ref = unsafe { (*ctx_ref.normalizer.as_ptr()).normalize(rhs_ty) };

    if norm_lhs_ty_ref.should_suppress_errors() || norm_rhs_ty_ref.should_suppress_errors() {
        return TypeFunctionReductionResult {
            result: Some(unsafe { (*ctx_ref.builtins.as_ptr()).anyType }),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    if unsafe {
        crate::functions::get_type_alt_j::get_type_id::<NeverType>(lhs_ty)
            .as_ref()
            .is_some()
            || crate::functions::get_type_alt_j::get_type_id::<NeverType>(rhs_ty)
                .as_ref()
                .is_some()
    } {
        return TypeFunctionReductionResult {
            result: Some(unsafe { (*ctx_ref.builtins.as_ptr()).neverType }),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    if (norm_lhs_ty_ref.is_subtype_of_string() || norm_lhs_ty_ref.is_exactly_number())
        && (norm_rhs_ty_ref.is_subtype_of_string() || norm_rhs_ty_ref.is_exactly_number())
    {
        return TypeFunctionReductionResult {
            result: Some(unsafe { (*ctx_ref.builtins.as_ptr()).stringType }),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    if let Some(result) = try_distribute_type_function_app(
        concat_type_function,
        instance,
        &type_params,
        &pack_params,
        ctx,
    ) {
        return result;
    }

    let mut dummy: ErrorVec = alloc::vec::Vec::new();
    let mut mm_type = crate::functions::find_metatable_entry::find_metatable_entry(
        ctx_ref.builtins.as_ptr(),
        &mut dummy,
        lhs_ty,
        "__concat",
        Location::new(
            luaur_ast::records::position::Position::default(),
            luaur_ast::records::position::Position::default(),
        ),
    );
    let mut reversed = false;
    if mm_type.is_none() {
        mm_type = crate::functions::find_metatable_entry::find_metatable_entry(
            ctx_ref.builtins.as_ptr(),
            &mut dummy,
            rhs_ty,
            "__concat",
            Location::new(
                luaur_ast::records::position::Position::default(),
                luaur_ast::records::position::Position::default(),
            ),
        );
        reversed = true;
    }

    if mm_type.is_none() {
        return TypeFunctionReductionResult {
            result: None,
            reduction_status: crate::enums::reduction::Reduction::Erroneous,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    let mm_type_ref = unsafe { follow_type_id(mm_type.unwrap()) };
    if is_pending(mm_type_ref, ctx_ref.solver) {
        return TypeFunctionReductionResult {
            result: None,
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::from([mm_type_ref]),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }

    let mut inferred_args: alloc::vec::Vec<TypeId> = alloc::vec::Vec::new();
    if !reversed {
        inferred_args.push(lhs_ty);
        inferred_args.push(rhs_ty);
    } else {
        inferred_args.push(rhs_ty);
        inferred_args.push(lhs_ty);
    }

    if FFlag::LuauConcatDoesntAlwaysReturnString.get() {
        let ret_pack = solve_function_call(
            ctx,
            if !ctx_ref.constraint.is_null() {
                unsafe { (*ctx_ref.constraint).location }
            } else {
                Location::new(
                    luaur_ast::records::position::Position::default(),
                    luaur_ast::records::position::Position::default(),
                )
            },
            mm_type_ref,
            unsafe {
                (*ctx_ref.arena.as_ptr())
                    .add_type_pack_vector_type_id_optional_type_pack_id(inferred_args, None)
            },
        );
        if ret_pack.is_none() {
            return TypeFunctionReductionResult {
                result: None,
                reduction_status: crate::enums::reduction::Reduction::Erroneous,
                blocked_types: alloc::vec::Vec::new(),
                blocked_packs: alloc::vec::Vec::new(),
                error: None,
                messages: alloc::vec::Vec::new(),
            };
        }

        let extracted = extend_type_pack(
            unsafe { &mut *ctx_ref.arena.as_ptr() },
            ctx_ref.builtins.as_ptr(),
            ret_pack.unwrap(),
            1,
            alloc::vec::Vec::new(),
        );
        if extracted.head.is_empty() {
            return TypeFunctionReductionResult {
                result: None,
                reduction_status: crate::enums::reduction::Reduction::Erroneous,
                blocked_types: alloc::vec::Vec::new(),
                blocked_packs: alloc::vec::Vec::new(),
                error: None,
                messages: alloc::vec::Vec::new(),
            };
        }

        return TypeFunctionReductionResult {
            result: Some(extracted.head[0]),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    } else {
        if solve_function_call(
            ctx,
            if !ctx_ref.constraint.is_null() {
                unsafe { (*ctx_ref.constraint).location }
            } else {
                Location::new(
                    luaur_ast::records::position::Position::default(),
                    luaur_ast::records::position::Position::default(),
                )
            },
            mm_type_ref,
            unsafe {
                (*ctx_ref.arena.as_ptr())
                    .add_type_pack_vector_type_id_optional_type_pack_id(inferred_args, None)
            },
        )
        .is_none()
        {
            return TypeFunctionReductionResult {
                result: None,
                reduction_status: crate::enums::reduction::Reduction::Erroneous,
                blocked_types: alloc::vec::Vec::new(),
                blocked_packs: alloc::vec::Vec::new(),
                error: None,
                messages: alloc::vec::Vec::new(),
            };
        }

        return TypeFunctionReductionResult {
            result: Some(unsafe { (*ctx_ref.builtins.as_ptr()).stringType }),
            reduction_status: crate::enums::reduction::Reduction::MaybeOk,
            blocked_types: alloc::vec::Vec::new(),
            blocked_packs: alloc::vec::Vec::new(),
            error: None,
            messages: alloc::vec::Vec::new(),
        };
    }
}
