"""
AbuLang Null Handler - JavaScript-like null/undefined behavior

Features:
- Unassigned variables are None (like undefined in JS)
- null can have properties
- Error messages for using unassigned variables with properties
- Must explicitly assign null to use properties
"""


class NullObject:
    """
    A special null object that can have properties
    Similar to JavaScript's null but with properties
    """
    
    def __init__(self):
        self._properties = {}
        self._is_null = True
    
    def __setattr__(self, name, value):
        if name.startswith('_'):
            # Internal attributes
            object.__setattr__(self, name, value)
        else:
            # User properties
            if not hasattr(self, '_properties'):
                object.__setattr__(self, '_properties', {})
            props = object.__getattribute__(self, '_properties')
            props[name] = value
    
    def __getattribute__(self, name):
        if name.startswith('_'):
            return object.__getattribute__(self, name)
        # Get properties dict
        props = object.__getattribute__(self, '_properties')
        return props.get(name, None)
    
    def __repr__(self):
        if self._properties:
            props = ', '.join(f"{k}={v}" for k, v in self._properties.items())
            return f"null({props})"
        return "null"
    
    def __str__(self):
        return "null"
    
    def __bool__(self):
        return False
    
    def __eq__(self, other):
        if isinstance(other, NullObject):
            return True
        if other is None:
            return True
        return False


class UndefinedVariable:
    """
    Represents an undefined/unassigned variable
    Similar to JavaScript's undefined
    """
    
    def __init__(self, name):
        object.__setattr__(self, '_name', name)
    
    def __getattribute__(self, attr):
        # Allow access to internal _name attribute
        if attr == '_name':
            return object.__getattribute__(self, attr)
        
        # Get the variable name for error message
        var_name = object.__getattribute__(self, '_name')
        
        raise AttributeError(
            f"[AbuLang Error] Cannot access property '{attr}' of undefined variable '{var_name}'\n"
            f"Hint: Assign a value first, or use 'null' if you want a null object with properties:\n"
            f"  {var_name} = null  # Then you can use {var_name}.{attr}"
        )
    
    def __setattr__(self, name, value):
        var_name = object.__getattribute__(self, '_name')
        raise AttributeError(
            f"[AbuLang Error] Cannot set property '{name}' on undefined variable '{var_name}'\n"
            f"Hint: Assign a value first:\n"
            f"  {var_name} = null  # Then you can use {var_name}.{name} = {value}"
        )
    
    def __repr__(self):
        var_name = object.__getattribute__(self, '_name')
        return f"undefined({var_name})"
    
    def __str__(self):
        return "undefined"
    
    def __bool__(self):
        return False


class NullAwareContext(dict):
    """
    A context dictionary that returns UndefinedVariable for missing keys
    and provides null object support
    """
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Add null as a built-in
        self['null'] = NullObject()
    
    def __getitem__(self, key):
        if key in self:
            return super().__getitem__(key)
        # Return undefined variable instead of raising KeyError
        return UndefinedVariable(key)
    
    def get(self, key, default=None):
        if key in self:
            return super().__getitem__(key)
        if default is None:
            return UndefinedVariable(key)
        return default


def create_null():
    """Create a new null object"""
    return NullObject()


def is_null(value):
    """Check if value is null"""
    return isinstance(value, NullObject)


def is_undefined(value):
    """Check if value is undefined"""
    return isinstance(value, UndefinedVariable)


def is_nullish(value):
    """Check if value is null or undefined (nullish)"""
    return is_null(value) or is_undefined(value) or value is None


# Example usage and tests
if __name__ == "__main__":
    print("=== Testing Null Handler ===\n")
    
    # Test 1: Create null object
    print("Test 1: Null Object with Properties")
    obj = NullObject()
    obj.name = "Abu"
    obj.age = 25
    print(f"obj = {obj}")
    print(f"obj.name = {obj.name}")
    print(f"obj.age = {obj.age}")
    
    # Test 2: Undefined variable
    print("\nTest 2: Undefined Variable")
    context = NullAwareContext()
    x = context['x']  # Undefined
    print(f"x = {x}")
    print(f"is_undefined(x) = {is_undefined(x)}")
    
    # Test 3: Try to access property of undefined
    print("\nTest 3: Access Property of Undefined (should error)")
    try:
        # This should raise an error
        value = x.name
        print(f"ERROR: Should have raised AttributeError, got: {value}")
    except AttributeError as e:
        print(str(e))
    
    # Test 4: Assign null and use properties
    print("\nTest 4: Assign Null and Use Properties")
    context['y'] = NullObject()
    context['y'].name = "Test"
    print(f"y = {context['y']}")
    print(f"y.name = {context['y'].name}")
    
    # Test 5: Check nullish values
    print("\nTest 5: Nullish Checks")
    print(f"is_null(NullObject()) = {is_null(NullObject())}")
    print(f"is_undefined(UndefinedVariable('x')) = {is_undefined(UndefinedVariable('x'))}")
    print(f"is_nullish(None) = {is_nullish(None)}")
    print(f"is_nullish(NullObject()) = {is_nullish(NullObject())}")
    
    print("\n=== All Tests Complete! ===")
