#
# A class which knows how to parse objects in the object tree.
# Implements section 12 of Z-code specification.
#
# For the license of this file, please consult the LICENSE file in the
# root directory of this distribution.
#

# This part of of the z-machine is where it becomes really clear that
# the original authoris were MIT Lisp-heads.  :-) They've got a tree
# of objects going, where each object is basically a linked list of
# siblings.  Specifically, each object contains a pointer to a parent,
# a pointer to its "next sibling" in the list, and a pointer to the
# head of its own children-list.

from .bitfield import BitField
from .zlogging import log
from .zstring import ZStringFactory


class ZObjectError(Exception):
    "General exception for ZObject class"

    pass


class ZObjectIllegalObjectNumber(ZObjectError):
    "Illegal object number given."

    pass


class ZObjectIllegalAttributeNumber(ZObjectError):
    "Illegal attribute number given."

    pass


class ZObjectIllegalPropertyNumber(ZObjectError):
    "Illegal property number given."

    pass


class ZObjectIllegalPropertySet(ZObjectError):
    "Illegal set of a property whose size is not 1 or 2."

    pass


class ZObjectIllegalVersion(ZObjectError):
    "Unsupported z-machine version."

    pass


class ZObjectIllegalPropLength(ZObjectError):
    "Illegal property length."

    pass


class ZObjectMalformedTree(ZObjectError):
    "Object tree is malformed."

    pass


# The interpreter should only need exactly one instance of this class.


class ZObjectParser:
    def __init__(self, zmem):
        self._memory = zmem
        self._propdefaults_addr = zmem.read_word(0x0A)
        self._stringfactory = ZStringFactory(self._memory)

        if 1 <= self._memory.version <= 3:
            self._objecttree_addr = self._propdefaults_addr + 62
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            self._objecttree_addr = self._propdefaults_addr + 126
        else:
            raise ZObjectIllegalVersion

    def _get_object_addr(self, objectnum):
        """Return address of object number OBJECTNUM."""

        result = 0
        if 1 <= self._memory.version <= 3:
            if not (1 <= objectnum <= 255):
                raise ZObjectIllegalObjectNumber
            result = self._objecttree_addr + (9 * (objectnum - 1))
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            if not (1 <= objectnum <= 65535):
                log(f"error:  there is no object {objectnum}")
                raise ZObjectIllegalObjectNumber
            result = self._objecttree_addr + (14 * (objectnum - 1))
        else:
            raise ZObjectIllegalVersion

        log(f"address of object {objectnum} is {result}")
        return result

    def _get_parent_sibling_child(self, objectnum):
        """Return [parent, sibling, child] object numbers of object OBJECTNUM."""

        addr = self._get_object_addr(objectnum)

        result = 0
        if 1 <= self._memory.version <= 3:
            addr += 4  # skip past attributes
            result = self._memory[addr : addr + 3]

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            addr += 6  # skip past attributes
            result = [
                self._memory.read_word(addr),
                self._memory.read_word(addr + 2),
                self._memory.read_word(addr + 4),
            ]
        else:
            raise ZObjectIllegalVersion

        log(
            f"parent/sibling/child of object {objectnum} is "
            f"{result[0]}, {result[1]}, {result[2]}"
        )
        return result

    def _get_proptable_addr(self, objectnum):
        """Return address of property table of object OBJECTNUM."""

        addr = self._get_object_addr(objectnum)

        # skip past attributes and relatives
        if 1 <= self._memory.version <= 3:
            addr += 7
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            addr += 12
        else:
            raise ZObjectIllegalVersion

        return self._memory.read_word(addr)

    def _get_default_property_addr(self, propnum):
        """Return address of default value for property PROPNUM."""

        addr = self._propdefaults_addr

        if 1 <= self._memory.version <= 3:
            if not (1 <= propnum <= 31):
                raise ZObjectIllegalPropertyNumber
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            if not (1 <= propnum <= 63):
                raise ZObjectIllegalPropertyNumber
        else:
            raise ZObjectIllegalVersion

        return addr + (2 * (propnum - 1))

    # --------- Public APIs -----------

    def get_attribute(self, objectnum, attrnum):
        """Return value (0 or 1) of attribute number ATTRNUM of object
        number OBJECTNUM."""
        if objectnum == 0:
            return 0

        object_addr = self._get_object_addr(objectnum)

        if 1 <= self._memory.version <= 3:
            if not (0 <= attrnum <= 31):
                raise ZObjectIllegalAttributeNumber
            bf = BitField(self._memory[object_addr + (attrnum // 8)])

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            if not (0 <= attrnum <= 47):
                raise ZObjectIllegalAttributeNumber
            bf = BitField(self._memory[object_addr + (attrnum // 8)])

        else:
            raise ZObjectIllegalVersion

        return bf[7 - (attrnum % 8)]

    def set_attribute(self, objectnum, attrnum):
        """Set attribute number ATTRNUM of object number OBJECTNUM to 1."""
        if objectnum == 0:
            return

        object_addr = self._get_object_addr(objectnum)

        if 1 <= self._memory.version <= 3:
            if not (0 <= attrnum <= 31):
                raise ZObjectIllegalAttributeNumber
            byte_offset = attrnum // 8
            bf = BitField(self._memory[object_addr + byte_offset])

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            if not (0 <= attrnum <= 47):
                raise ZObjectIllegalAttributeNumber
            byte_offset = attrnum // 8
            bf = BitField(self._memory[object_addr + byte_offset])

        else:
            raise ZObjectIllegalVersion

        bf[7 - (attrnum % 8)] = 1
        self._memory[object_addr + byte_offset] = int(bf)

    def clear_attribute(self, objectnum, attrnum):
        """Clear attribute number ATTRNUM of object number OBJECTNUM to 0."""
        if objectnum == 0:
            return

        object_addr = self._get_object_addr(objectnum)

        if 1 <= self._memory.version <= 3:
            if not (0 <= attrnum <= 31):
                raise ZObjectIllegalAttributeNumber
            byte_offset = attrnum // 8
            bf = BitField(self._memory[object_addr + byte_offset])

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            if not (0 <= attrnum <= 47):
                raise ZObjectIllegalAttributeNumber
            byte_offset = attrnum // 8
            bf = BitField(self._memory[object_addr + byte_offset])

        else:
            raise ZObjectIllegalVersion

        bf[7 - (attrnum % 8)] = 0
        self._memory[object_addr + byte_offset] = int(bf)

    def get_all_attributes(self, objectnum):
        """Return a list of all attribute numbers that are set on object
        OBJECTNUM"""

        if 1 <= self._memory.version <= 3:
            max = 32
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            max = 48
        else:
            raise ZObjectIllegalVersion

        # really inefficient, but who cares?
        attrs = []
        for i in range(0, max):
            if self.get_attribute(objectnum, i):
                attrs.append(i)
        return attrs

    def get_parent(self, objectnum):
        """Return object number of parent of object number OBJECTNUM."""
        if objectnum == 0:
            return 0

        [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
        return parent

    def get_child(self, objectnum):
        """Return object number of child of object number OBJECTNUM."""
        if objectnum == 0:
            return 0

        [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
        return child

    def get_sibling(self, objectnum):
        """Return object number of sibling of object number OBJECTNUM."""
        if objectnum == 0:
            return 0

        [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
        return sibling

    def set_parent(self, objectnum, new_parent_num):
        """Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""

        addr = self._get_object_addr(objectnum)
        if 1 <= self._memory.version <= 3:
            self._memory[addr + 4] = new_parent_num
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            self._memory.write_word(addr + 6, new_parent_num)
        else:
            raise ZObjectIllegalVersion

    def set_child(self, objectnum, new_child_num):
        """Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""

        addr = self._get_object_addr(objectnum)
        if 1 <= self._memory.version <= 3:
            self._memory[addr + 6] = new_child_num
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            self._memory.write_word(addr + 10, new_child_num)
        else:
            raise ZObjectIllegalVersion

    def set_sibling(self, objectnum, new_sibling_num):
        """Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""

        addr = self._get_object_addr(objectnum)
        if 1 <= self._memory.version <= 3:
            self._memory[addr + 5] = new_sibling_num
        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            self._memory.write_word(addr + 8, new_sibling_num)
        else:
            raise ZObjectIllegalVersion

    def remove_object(self, objectnum):
        """Detach object OBJECTNUM from its parent (unlink from sibling chain)."""
        if objectnum == 0:
            return

        parent = self.get_parent(objectnum)
        if parent == 0:
            # Object has no parent, nothing to remove
            return

        sibling = self.get_sibling(objectnum)

        # Check if this object is the first child
        if self.get_child(parent) == objectnum:
            # Make sibling the new first child
            self.set_child(parent, sibling)
        else:
            # Walk the sibling chain to find the object before this one
            prev = self.get_child(parent)
            current = self.get_sibling(prev)
            while current != 0:
                if current == objectnum:
                    # Link prev to our sibling, removing us from chain
                    self.set_sibling(prev, sibling)
                    break
                prev = current
                current = self.get_sibling(current)
            else:
                # Shouldn't happen - object claimed parent but not in chain
                raise ZObjectMalformedTree

        # Clear this object's parent
        self.set_parent(objectnum, 0)
        self.set_sibling(objectnum, 0)

    def insert_object(self, parent_object, new_child):
        """Prepend object NEW_CHILD to the list of PARENT_OBJECT's children.

        Per the Z-spec: if new_child already has a parent, it is first
        removed from that parent's child list, then made the first child
        of parent_object."""
        if parent_object == 0 or new_child == 0:
            return

        # Remove from old parent first (spec says "first removed")
        self.remove_object(new_child)

        # Now insert as first child of parent_object
        original_child = self.get_child(parent_object)
        self.set_sibling(new_child, original_child)
        self.set_parent(new_child, parent_object)
        self.set_child(parent_object, new_child)

    def get_shortname(self, objectnum):
        """Return 'short name' of object number OBJECTNUM as ascii string."""
        if objectnum == 0:
            return ""

        addr = self._get_proptable_addr(objectnum)
        return self._stringfactory.get(addr + 1)

    def get_prop(self, objectnum, propnum):
        """Return either a byte or word value of property PROPNUM of
        object OBJECTNUM."""
        if objectnum == 0:
            return 0
        (addr, size) = self.get_prop_addr_len(objectnum, propnum)
        if size == 1:
            return self._memory[addr]
        elif size == 2:
            return self._memory.read_word(addr)
        else:
            raise ZObjectIllegalPropLength

    def get_prop_addr_len(self, objectnum, propnum):
        """Return address & length of value for property number PROPNUM of
        object number OBJECTNUM.  If object has no such property, then
        return the address & length of the 'default' value for the property."""

        # start at the beginning of the object's proptable
        addr = self._get_proptable_addr(objectnum)
        # skip past the shortname of the object
        addr += 1 + 2 * self._memory[addr]
        pnum = 0

        if 1 <= self._memory.version <= 3:
            while self._memory[addr] != 0:
                bf = BitField(self._memory[addr])
                addr += 1
                pnum = bf[0:5]
                size = bf[5:8] + 1
                if pnum == propnum:
                    return (addr, size)
                addr += size

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            while self._memory[addr] != 0:
                bf = BitField(self._memory[addr])
                addr += 1
                pnum = bf[0:6]
                if bf[7]:
                    bf2 = BitField(self._memory[addr])
                    addr += 1
                    size = bf2[0:6]
                else:
                    size = 2 if bf[6] else 1
                if pnum == propnum:
                    return (addr, size)
                addr += size

        else:
            raise ZObjectIllegalVersion

        # property list ran out, so return default propval instead.
        default_value_addr = self._get_default_property_addr(propnum)
        return (default_value_addr, 2)

    def get_all_properties(self, objectnum):
        """Return a dictionary of all properties listed in the property
        table of object OBJECTNUM.  (Obviously, this discounts 'default'
        property values.).  The dictionary maps property numbers to (addr,
        len) propval tuples."""

        proplist = {}

        # start at the beginning of the object's proptable
        addr = self._get_proptable_addr(objectnum)
        # skip past the shortname of the object
        shortname_length = self._memory[addr]
        addr += 1
        addr += 2 * shortname_length

        if 1 <= self._memory.version <= 3:
            while self._memory[addr] != 0:
                bf = BitField(self._memory[addr])
                addr += 1
                pnum = bf[0:5]
                size = bf[5:8] + 1
                proplist[pnum] = (addr, size)
                addr += size

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            while self._memory[addr] != 0:
                bf = BitField(self._memory[addr])
                addr += 1
                pnum = bf[0:6]
                if bf[7]:
                    bf2 = BitField(self._memory[addr])
                    addr += 1
                    size = bf2[0:6]
                    if size == 0:
                        size = 64
                else:
                    size = 2 if bf[6] else 1
                proplist[pnum] = (addr, size)
                addr += size

        else:
            raise ZObjectIllegalVersion

        return proplist

    def set_property(self, objectnum, propnum, value):
        """Set a property on an object."""
        proplist = self.get_all_properties(objectnum)
        if propnum not in proplist:
            raise ZObjectIllegalPropertyNumber

        addr, size = proplist[propnum]
        if size == 1:
            self._memory[addr] = value & 0xFF
        elif size == 2:
            self._memory.write_word(addr, value)
        else:
            raise ZObjectIllegalPropertySet

    def get_property_data_address(self, objectnum, propnum):
        """Return the address of property PROPNUM's data bytes for object
        OBJECTNUM. Return 0 if the object doesn't have that property."""
        if objectnum == 0:
            return 0

        try:
            addr, size = self.get_prop_addr_len(objectnum, propnum)
            # get_prop_addr_len returns default property addr if not found
            # We need to check if this is the actual property or default
            proplist = self.get_all_properties(objectnum)
            if propnum in proplist:
                return addr
            else:
                return 0
        except ZObjectIllegalPropLength:
            return 0

    def get_next_property(self, objectnum, propnum):
        """If PROPNUM is 0, return the first property number of object OBJECTNUM.
        Otherwise, return the property number after PROPNUM in the property list.
        Return 0 if there are no more properties."""
        if objectnum == 0:
            return 0

        if propnum == 0:
            # Return first property number
            addr = self._get_proptable_addr(objectnum)
            # Skip past the shortname
            addr += 1 + 2 * self._memory[addr]
            # Read first property number
            if self._memory[addr] == 0:
                return 0

            if 1 <= self._memory.version <= 3:
                bf = BitField(self._memory[addr])
                return bf[0:5]
            elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
                bf = BitField(self._memory[addr])
                return bf[0:6]
            else:
                raise ZObjectIllegalVersion

        else:
            # Find the property after propnum
            proplist = self.get_all_properties(objectnum)
            if propnum not in proplist:
                raise ZObjectIllegalPropertyNumber

            # Properties are stored in descending order
            # Find the next lower property number
            sorted_props = sorted(proplist.keys(), reverse=True)
            try:
                idx = sorted_props.index(propnum)
                if idx + 1 < len(sorted_props):
                    return sorted_props[idx + 1]
                else:
                    return 0
            except ValueError:
                return 0

    def get_property_length(self, data_address):
        """Given a property DATA address, return the length of that property's data.
        Return 0 if data_address is 0."""

        if data_address == 0:
            return 0

        # The size byte is just before the data address
        size_addr = data_address - 1

        if 1 <= self._memory.version <= 3:
            bf = BitField(self._memory[size_addr])
            size = bf[5:8] + 1
            return size

        elif 4 <= self._memory.version <= 5 or self._memory.version == 8:
            bf = BitField(self._memory[size_addr])
            if bf[7]:
                # Two-byte header: size is in bits 0-5 of this byte
                size = bf[0:6]
                if size == 0:
                    size = 64
                return size
            else:
                # One size byte
                return 2 if bf[6] else 1

        else:
            raise ZObjectIllegalVersion

    def describe_object(self, objectnum):
        """For debugging purposes, pretty-print everything known about
        object OBJECTNUM."""

        print("Object number:", objectnum)
        print("    Short name:", self.get_shortname(objectnum))
        print("    Parent:", self.get_parent(objectnum), end=" ")
        print(" Sibling:", self.get_sibling(objectnum), end=" ")
        print(" Child:", self.get_child(objectnum))
        print("    Attributes:", self.get_all_attributes(objectnum))
        print("    Properties:")

        proplist = self.get_all_properties(objectnum)
        for key in list(proplist.keys()):
            (addr, len) = proplist[key]
            print(f"       [{key:2d}] :", end=" ")
            for i in range(0, len):
                print(f"{self._memory[addr + i]:02X}", end=" ")
            print()
