use crate::enums::table_state::TableState;
use crate::enums::value_context::ValueContext;
use crate::functions::follow_type::follow_type_id;
use crate::functions::get_mutable_type::getMutable;
use crate::functions::get_type_alt_j::get_type_id;
use crate::functions::lookup_extern_type_prop::lookup_extern_type_prop;
use crate::functions::maybe_string::maybe_string;
use crate::records::assign_prop_constraint::AssignPropConstraint;
use crate::records::blocked_type::BlockedType;
use crate::records::constraint::Constraint;
use crate::records::constraint_solver::ConstraintSolver;
use crate::records::extern_type::ExternType;
use crate::records::metatable_type::MetatableType;
use crate::records::property_type::Property;
use crate::records::table_type::TableType;
use crate::records::union_type::UnionType;
use luaur_common::macros::luau_assert::LUAU_ASSERT;
use luaur_common::FFlag;

impl ConstraintSolver {
    pub fn try_dispatch_assign_prop_constraint_not_null_constraint(
        &mut self,
        c: &AssignPropConstraint,
        constraint: *const Constraint,
    ) -> bool {
        let mut lhs_type = unsafe { follow_type_id(c.lhs_type) };
        let rhs_type = unsafe { follow_type_id(c.rhs_type) };

        if self.is_blocked_type_id(lhs_type) {
            return self.block_type_id_not_null_constraint(lhs_type, constraint);
        }

        let lhs_extern_type = unsafe { get_type_id::<ExternType>(lhs_type) };
        if !lhs_extern_type.is_null() {
            let prop = unsafe { lookup_extern_type_prop(&*lhs_extern_type, &c.prop_name) };
            if prop.is_null() {
                self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, unsafe {
                    (*self.builtin_types).anyType
                });
                return true;
            }

            if let Some(write_ty) = unsafe { (*prop).write_ty } {
                self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, write_ty);
                self.constraint_solver_unify(constraint, rhs_type, write_ty);
            } else {
                self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, unsafe {
                    (*self.builtin_types).anyType
                });
            }

            return true;
        }

        let lookup = self
            .lookup_table_prop_not_null_constraint_type_id_string_value_context_bool_bool(
                constraint,
                lhs_type,
                &c.prop_name,
                ValueContext::LValue,
                false,
                false,
            );
        if !lookup.blocked_types.is_empty() {
            for blocked in lookup.blocked_types {
                self.block_type_id_not_null_constraint(blocked, constraint);
            }
            return false;
        }

        if let Some(prop_ty) = lookup.prop_type {
            let bound_prop_ty = if lookup.is_index {
                unsafe {
                    (*self.arena).add_type(UnionType {
                        options: alloc::vec![prop_ty, (*self.builtin_types).nilType],
                    })
                }
            } else {
                prop_ty
            };

            self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, bound_prop_ty);
            self.constraint_solver_unify(constraint, rhs_type, prop_ty);
            return true;
        }

        let mut lhs_table = unsafe { getMutable::<TableType>(lhs_type) };
        if lhs_table.is_null() {
            let lhs_meta = unsafe { get_type_id::<MetatableType>(lhs_type) };
            if !lhs_meta.is_null() {
                lhs_type = unsafe { follow_type_id((*lhs_meta).table) };
                lhs_table = unsafe { getMutable::<TableType>(lhs_type) };
            }
        }

        if !lhs_table.is_null() {
            let table = unsafe { &mut *lhs_table };

            if let Some(prop) = table.props.get_mut(&c.prop_name) {
                if let Some(write_ty) = prop.write_ty {
                    self.bind_not_null_constraint_type_id_type_id(
                        constraint,
                        c.prop_type,
                        write_ty,
                    );
                    self.constraint_solver_unify(constraint, rhs_type, write_ty);
                    return true;
                }

                if (table.state == TableState::Unsealed || table.state == TableState::Free)
                    && prop.read_ty.is_some()
                {
                    prop.write_ty = prop.read_ty;
                    let write_ty = prop.write_ty.unwrap();
                    self.bind_not_null_constraint_type_id_type_id(
                        constraint,
                        c.prop_type,
                        write_ty,
                    );
                    self.constraint_solver_unify(constraint, rhs_type, write_ty);
                    return true;
                }

                self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, unsafe {
                    (*self.builtin_types).errorType
                });
                return true;
            }

            if let Some(indexer) = &table.indexer {
                if maybe_string(indexer.index_type) {
                    let prop_ty = indexer.index_result_type;
                    let union = unsafe {
                        (*self.arena).add_type(UnionType {
                            options: alloc::vec![prop_ty, (*self.builtin_types).nilType],
                        })
                    };
                    self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, union);
                    self.constraint_solver_unify(constraint, rhs_type, prop_ty);
                    return true;
                }
            }

            if table.state == TableState::Unsealed || table.state == TableState::Free {
                if FFlag::LuauConstraintGraph.get() {
                    LUAU_ASSERT!(!self.cgraph.is_null());
                    unsafe { (*self.cgraph).copy_dependencies_of_type_id(lhs_type, rhs_type) };
                } else {
                    self.deprecate_d_shift_references(lhs_type, rhs_type);
                }

                self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, rhs_type);

                let mut prop = Property::rw_type_id(rhs_type);
                prop.location = c.prop_location;
                table.props.insert(c.prop_name.clone(), prop);

                if table.state == TableState::Unsealed && c.decrement_prop_count {
                    LUAU_ASSERT!(table.remaining_props > 0);
                    table.remaining_props -= 1;
                    self.unblock_type_id_location(lhs_type, unsafe { (*constraint).location });
                }

                return true;
            }
        }

        let prop_type_is_blocked = unsafe { !get_type_id::<BlockedType>(c.prop_type).is_null() };
        if prop_type_is_blocked {
            self.bind_not_null_constraint_type_id_type_id(constraint, c.prop_type, unsafe {
                (*self.builtin_types).errorType
            });
        }

        true
    }
}
