use crate::{
    DbIndex, LuaAliasCallKind, LuaAliasCallType, LuaMemberInfo, LuaMemberKey, LuaObjectType,
    LuaType, LuaTypeNode, TypeOps, VariadicType, get_member_map,
    semantic::{
        generic::key_type_to_member_key,
        member::{find_members, infer_raw_member_type},
        type_check,
    },
};
use hashbrown::HashMap;
use std::{ops::Deref, vec};

use super::{
    GenericInstantiateContext, GenericResolveMode, TypeSubstitutor, instantiate_type_generic_inner,
};

pub(super) fn instantiate_alias_call(
    context: &GenericInstantiateContext,
    alias_call: &LuaAliasCallType,
) -> LuaType {
    let operand_exprs = alias_call.get_operands();
    let operands = operand_exprs
        .iter()
        .map(|it| instantiate_type_generic_inner(context, it))
        .collect::<Vec<_>>();

    match alias_call.get_call_kind() {
        LuaAliasCallKind::Sub => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }
            // 如果类型为`Union`且只有一个类型, 则会解开`Union`包装
            TypeOps::Remove.apply(context.db, &operands[0], &operands[1])
        }
        LuaAliasCallKind::Add => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }

            TypeOps::Union.apply(context.db, &operands[0], &operands[1])
        }
        LuaAliasCallKind::KeyOf => {
            if operands.len() != 1 {
                return LuaType::Unknown;
            }

            let owner = instantiate_alias_origin_operand(context, &operands[0])
                .unwrap_or_else(|| operands[0].clone());
            let members = get_keyof_members(context.db, &owner).unwrap_or_default();
            // keyof 表示可取键的联合类型, 不是按位置展开的 tuple.
            let member_key_types = members
                .iter()
                .filter_map(|m| match &m.key {
                    LuaMemberKey::Integer(i) => Some(LuaType::DocIntegerConst(*i)),
                    LuaMemberKey::Name(s) => Some(LuaType::DocStringConst(s.clone().into())),
                    _ => None,
                })
                .collect::<Vec<_>>();
            TypeOps::union_all(context.db, member_key_types)
        }
        // 条件类型不在此处理
        LuaAliasCallKind::Extends => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }

            if operands[0].contains_tpl_node() || operands[1].contains_tpl_node() {
                return LuaType::Call(
                    LuaAliasCallType::new(LuaAliasCallKind::Extends, operands).into(),
                );
            }

            let compact =
                type_check::check_type_compact(context.db, &operands[0], &operands[1]).is_ok();
            LuaType::BooleanConst(compact)
        }
        LuaAliasCallKind::Select => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }

            instantiate_select_call(&operands[0], &operands[1])
        }
        LuaAliasCallKind::Unpack => instantiate_unpack_call(context.db, &operands),
        LuaAliasCallKind::RawGet => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }

            if operands.iter().any(LuaType::contains_tpl_node) {
                return LuaType::Call(
                    LuaAliasCallType::new(LuaAliasCallKind::RawGet, operands).into(),
                );
            }

            let key = resolve_literal_operand(operand_exprs.get(1), context.substitutor)
                .or_else(|| instantiate_alias_origin_operand(context, &operands[1]))
                .unwrap_or_else(|| operands[1].clone());

            instantiate_rawget_call(context.db, &operands[0], &key)
        }
        LuaAliasCallKind::Index => {
            if operands.len() != 2 {
                return LuaType::Unknown;
            }

            if operands.iter().any(LuaType::contains_tpl_node) {
                return LuaType::Call(
                    LuaAliasCallType::new(LuaAliasCallKind::Index, operands).into(),
                );
            }

            let key = resolve_literal_operand(operand_exprs.get(1), context.substitutor)
                .or_else(|| instantiate_alias_origin_operand(context, &operands[1]))
                .unwrap_or_else(|| operands[1].clone());

            instantiate_index_call(context.db, &operands[0], &key)
        }
        LuaAliasCallKind::Merge => instantiate_merge_call(context.db, &operands),
    }
}

fn instantiate_alias_origin_operand(
    context: &GenericInstantiateContext,
    operand: &LuaType,
) -> Option<LuaType> {
    let LuaType::Ref(type_id) = operand else {
        return None;
    };
    let type_decl = context.db.get_type_index().get_type_decl(type_id)?;
    if !type_decl.is_alias() {
        return None;
    }

    let origin = type_decl.get_alias_origin(context.db, Some(context.substitutor))?;
    Some(instantiate_type_generic_inner(context, &origin))
}

fn instantiate_merge_call(db: &DbIndex, operands: &[LuaType]) -> LuaType {
    if operands.len() != 2 {
        return LuaType::Unknown;
    }

    let left_members = find_members(db, &operands[0]);
    let right_members = find_members(db, &operands[1]);
    if left_members.is_none() && right_members.is_none() {
        return LuaType::Unknown;
    }

    let mut left_map: HashMap<_, _> = HashMap::new();
    for member in left_members.unwrap_or_default() {
        left_map.entry(member.key).or_insert(member.typ);
    }

    let mut right_map: HashMap<_, _> = HashMap::new();
    for member in right_members.unwrap_or_default() {
        right_map.entry(member.key).or_insert(member.typ);
    }

    let mut fields = left_map;
    for (k, v) in right_map {
        fields.insert(k, v);
    }

    LuaType::Object(LuaObjectType::new_with_fields(fields, Vec::new()).into())
}

fn resolve_literal_operand(
    operand: Option<&LuaType>,
    substitutor: &TypeSubstitutor,
) -> Option<LuaType> {
    match operand {
        Some(LuaType::TplRef(tpl_ref)) => substitutor
            .resolve_type(
                tpl_ref.get_tpl_id(),
                GenericResolveMode::Literal,
                tpl_ref.is_const(),
            )
            .cloned(),
        _ => None,
    }
}

#[derive(Debug)]
enum NumOrLen {
    Num(i64),
    Len,
    LenUnknown,
}

fn instantiate_select_call(source: &LuaType, index: &LuaType) -> LuaType {
    let num_or_len = match index {
        LuaType::DocIntegerConst(i) => {
            if *i == 0 {
                return LuaType::Unknown;
            }
            NumOrLen::Num(*i)
        }
        LuaType::IntegerConst(i) => {
            if *i == 0 {
                return LuaType::Unknown;
            }
            NumOrLen::Num(*i)
        }
        LuaType::DocStringConst(s) => {
            if s.as_str() == "#" {
                NumOrLen::Len
            } else {
                NumOrLen::LenUnknown
            }
        }
        LuaType::StringConst(s) => {
            if s.as_str() == "#" {
                NumOrLen::Len
            } else {
                NumOrLen::LenUnknown
            }
        }
        _ => return LuaType::Unknown,
    };

    let multi_return = if let LuaType::Variadic(multi) = source {
        multi.deref()
    } else {
        &VariadicType::Base(source.clone())
    };

    match num_or_len {
        NumOrLen::Num(i) => match multi_return {
            VariadicType::Base(_) => LuaType::Variadic(multi_return.clone().into()),
            VariadicType::Multi(_) => {
                let Some(total_len) = multi_return.get_min_len() else {
                    return source.clone();
                };

                let start = if i < 0 { total_len as i64 + i } else { i - 1 };
                if start < 0 || start >= (total_len as i64) {
                    return source.clone();
                }

                let multi = multi_return.get_new_variadic_from(start as usize);
                LuaType::Variadic(multi.into())
            }
        },
        NumOrLen::Len => {
            let len = multi_return.get_min_len();
            if let Some(len) = len {
                LuaType::IntegerConst(len as i64)
            } else {
                LuaType::Integer
            }
        }
        NumOrLen::LenUnknown => LuaType::Integer,
    }
}

fn instantiate_unpack_call(db: &DbIndex, operands: &[LuaType]) -> LuaType {
    if operands.is_empty() {
        return LuaType::Unknown;
    }

    let need_unpack_type = &operands[0];
    let mut start = -1;
    // todo use end
    #[allow(unused)]
    let mut end = -1;
    if operands.len() > 1 {
        if let LuaType::DocIntegerConst(i) = &operands[1] {
            start = *i - 1;
        } else if let LuaType::IntegerConst(i) = &operands[1] {
            start = *i - 1;
        }
    }

    #[allow(unused)]
    if operands.len() > 2 {
        if let LuaType::DocIntegerConst(i) = &operands[2] {
            end = *i;
        } else if let LuaType::IntegerConst(i) = &operands[2] {
            end = *i;
        }
    }

    match &need_unpack_type {
        LuaType::Tuple(tuple) => {
            let mut types = tuple.get_types().to_vec();
            if start > 0 {
                if start as usize > types.len() {
                    return LuaType::Unknown;
                }

                if start < types.len() as i64 {
                    types = types[start as usize..].to_vec();
                }
            }

            LuaType::Variadic(VariadicType::Multi(types).into())
        }
        LuaType::Array(array_type) => LuaType::Variadic(
            VariadicType::Base(TypeOps::Union.apply(db, array_type.get_base(), &LuaType::Nil))
                .into(),
        ),
        LuaType::TableGeneric(table) => {
            if table.len() != 2 {
                return LuaType::Unknown;
            }

            let value = table[1].clone();
            LuaType::Variadic(
                VariadicType::Base(TypeOps::Union.apply(db, &value, &LuaType::Nil)).into(),
            )
        }
        LuaType::Unknown | LuaType::Any => LuaType::Unknown,
        _ => {
            // may cost many
            let mut multi_types = vec![];
            let members = match get_member_map(db, need_unpack_type) {
                Some(members) => members,
                None => return LuaType::Unknown,
            };

            for i in 1..10 {
                let member_key = LuaMemberKey::Integer(i);
                if let Some(member_info) = members.get(&member_key) {
                    let mut member_type = LuaType::Never;
                    for sub_member_info in member_info {
                        member_type = TypeOps::Union.apply(db, &member_type, &sub_member_info.typ);
                    }
                    multi_types.push(member_type);
                } else {
                    break;
                }
            }

            LuaType::Variadic(VariadicType::Multi(multi_types).into())
        }
    }
}

fn instantiate_rawget_call(db: &DbIndex, owner: &LuaType, key: &LuaType) -> LuaType {
    if let LuaType::Union(union) = key {
        let mut result = LuaType::Never;
        for member in union.into_vec() {
            let member_type = instantiate_rawget_call(db, owner, &member);
            result = TypeOps::Union.apply(db, &result, &member_type);
        }
        return result;
    }

    if let LuaType::MultiLineUnion(multi) = key {
        let mut result = LuaType::Never;
        for (member, _) in multi.get_unions() {
            let member_type = instantiate_rawget_call(db, owner, member);
            result = TypeOps::Union.apply(db, &result, &member_type);
        }
        return result;
    }

    let member_key = match key {
        LuaType::DocStringConst(s) => LuaMemberKey::Name(s.deref().clone()),
        LuaType::StringConst(s) => LuaMemberKey::Name(s.deref().clone()),
        LuaType::DocIntegerConst(i) => LuaMemberKey::Integer(*i),
        LuaType::IntegerConst(i) => LuaMemberKey::Integer(*i),
        _ => return LuaType::Unknown,
    };

    infer_raw_member_type(db, owner, &member_key).unwrap_or(LuaType::Unknown)
}

fn instantiate_index_call(db: &DbIndex, owner: &LuaType, key: &LuaType) -> LuaType {
    if owner.is_unknown() {
        return LuaType::Unknown;
    }

    if let LuaType::Union(union) = key {
        let mut result = LuaType::Never;
        for member in union.into_vec() {
            let member_type = instantiate_index_call(db, owner, &member);
            result = TypeOps::Union.apply(db, &result, &member_type);
        }
        return result;
    }

    if let LuaType::MultiLineUnion(multi) = key {
        let mut result = LuaType::Never;
        for (member, _) in multi.get_unions() {
            let member_type = instantiate_index_call(db, owner, member);
            result = TypeOps::Union.apply(db, &result, &member_type);
        }
        return result;
    }

    if let LuaType::Variadic(variadic) = owner {
        match variadic.deref() {
            VariadicType::Base(base) => {
                return base.clone();
            }
            VariadicType::Multi(types) => {
                if let LuaType::IntegerConst(key) | LuaType::DocIntegerConst(key) = key {
                    return types.get(*key as usize).cloned().unwrap_or(LuaType::Never);
                }
            }
        }
    }

    if let Some(member_key) = key_type_to_member_key(key) {
        infer_raw_member_type(db, owner, &member_key).unwrap_or(LuaType::Never)
    } else {
        LuaType::Never
    }
}

pub fn get_keyof_members(db: &DbIndex, prefix_type: &LuaType) -> Option<Vec<LuaMemberInfo>> {
    match prefix_type {
        LuaType::Variadic(variadic) => match variadic.deref() {
            VariadicType::Base(base) => Some(vec![LuaMemberInfo {
                property_owner_id: None,
                key: LuaMemberKey::Integer(0),
                typ: base.clone(),
                feature: None,
                overload_index: None,
            }]),
            VariadicType::Multi(types) => {
                let mut members = Vec::new();
                for (idx, typ) in types.iter().enumerate() {
                    members.push(LuaMemberInfo {
                        property_owner_id: None,
                        key: LuaMemberKey::Integer(idx as i64),
                        typ: typ.clone(),
                        feature: None,
                        overload_index: None,
                    });
                }

                Some(members)
            }
        },
        _ => find_members(db, prefix_type),
    }
}
