from typing import override

from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, QMimeData, Signal

from orcalab.actor import BaseActor, GroupActor
from orcalab.local_scene import LocalScene
from orcalab.path import Path
from orcalab.scene_edit_bus import SceneEditNotification, SceneEditNotificationBus


class ReparentData:
    def __init__(self):
        # actor to be reparented
        self.actor: BaseActor = None
        self.actor_path: Path = None

        # new parent
        self.parent: BaseActor = None
        self.parent_path: Path = None


class ActorOutlineModel(QAbstractItemModel, SceneEditNotification):
    # actor path, new parent path, index to insert at (-1 means append to the end)
    request_reparent = Signal(Path, Path, int)
    add_item = Signal(str, BaseActor)

    def __init__(self, local_scene: LocalScene, parent=None):
        super().__init__(parent)
        self.column_count = 1
        self.m_root_group: GroupActor | None = None
        self.reparent_mime = "application/x-orca-actor-reparent"
        self.local_scene = local_scene

    def connect_bus(self):
        SceneEditNotificationBus.connect(self)

    def disconnect_bus(self):
        SceneEditNotificationBus.disconnect(self)

    def get_actor(self, index: QModelIndex) -> BaseActor:
        if not index.isValid():
            return self.m_root_group

        if index.model() != self:
            raise ValueError("Index does not belong to this model.")

        actor = index.internalPointer()
        if not isinstance(actor, BaseActor):
            raise ValueError("Invalid actor.")

        return actor

    def get_index_from_actor(self, actor: BaseActor) -> QModelIndex:
        if not isinstance(actor, BaseActor):
            raise ValueError("Invalid actor.")

        if actor == self.m_root_group:
            return QModelIndex()

        parent_actor = actor.parent
        if parent_actor is None:
            raise Exception("Actor that is not pseudo root should always has a parent.")

        index = -1
        children = parent_actor.children
        for i, child in enumerate(children):
            if child == actor:
                index = i
                break

        if index == -1:
            raise Exception("Child not found from it's parent.")

        return self.createIndex(index, 0, actor)

    @override
    def index(self, row, column, /, parent=...):
        if row < 0 or column < 0 or column >= self.column_count:
            return QModelIndex()

        if not parent.isValid():
            if row < len(self.m_root_group.children):
                child = self.m_root_group.children[row]
                if child is not None:
                    return self.createIndex(row, column, child)
        else:
            if parent.column() == 0:
                parent_actor = self.get_actor(parent)
                if isinstance(parent_actor, GroupActor):
                    if row < len(parent_actor.children):
                        child = parent_actor.children[row]
                        return self.createIndex(row, column, child)

        return QModelIndex()

    @override
    def parent(self, child):
        super().parent()
        if not child.isValid():
            return QModelIndex()

        actor = self.get_actor(child)

        parent_actor = actor.parent
        if parent_actor == self.m_root_group:
            return QModelIndex()

        return self.get_index_from_actor(parent_actor)

    @override
    def rowCount(self, /, parent=...):
        if not parent.isValid():
            if self.m_root_group is not None:
                return len(self.m_root_group.children)
        else:
            if parent.column() == 0:
                actor = self.get_actor(parent)
                if isinstance(actor, GroupActor):
                    return len(actor.children)
        return 0

    @override
    def columnCount(self, /, parent=...):
        return 1

    @override
    def data(self, index, /, role=...):
        if not index.isValid():
            return None

        actor = self.get_actor(index)

        if index.column() == 0:
            if role == Qt.DisplayRole:
                return actor.name

        return None

    @override
    def flags(self, index, /):
        ItemFlag = Qt.ItemFlag

        if not index.isValid():
            return ItemFlag.ItemIsDropEnabled

        f = (
            ItemFlag.ItemIsEnabled
            | ItemFlag.ItemIsSelectable
            # | ItemFlag.ItemIsDragEnabled
            | ItemFlag.ItemIsDropEnabled
            # | ItemFlag.ItemIsEditable
        )

        return f

    def set_root_group(self, group: GroupActor):
        if not isinstance(group, GroupActor):
            raise TypeError("Root group must be an instance of GroupActor.")

        self.beginResetModel()
        self.m_root_group = group
        self.endResetModel()

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction

    def supportedDragActions(self):
        return Qt.CopyAction

    def dropMimeData(self, data, action, row, column, parent):
        print("drop")
        if data.hasFormat(self.reparent_mime):
            reparent_data = ReparentData()
            if not self.prepare_reparent_data(
                reparent_data, data, action, row, column, parent
            ):
                return False
            self.request_reparent.emit(
                reparent_data.actor_path, reparent_data.parent_path, row
            )
            return True

        if data.hasFormat("application/x-orca-asset"):
            # decode asset name robustly
            ba = data.data("application/x-orca-asset")
            try:
                asset_name = bytes(ba).decode("utf-8")
            except Exception:
                try:
                    asset_name = ba.data().decode("utf-8")
                except Exception:
                    asset_name = str(ba)

            parent_actor = self.get_actor(parent)

            self.add_item.emit(asset_name, parent_actor)
            return True

        return False

    def canDropMimeData(self, data, action, row, column, parent):
        if data.hasFormat("application/x-orca-asset"):
            parent_actor = self.get_actor(parent)
            return parent_actor is not None
        if data.hasFormat(self.reparent_mime):
            reparent_data = ReparentData()
            return self.prepare_reparent_data(
                reparent_data, data, action, row, column, parent
            )
        return False

    def mimeData(self, indexes):
        if len(indexes) == 0:
            return None

        actor = self.get_actor(indexes[0])

        actor_path = self.local_scene.get_actor_path(actor)
        if actor_path is None:
            return None

        mime_data = QMimeData()
        mime_data.setData(self.reparent_mime, actor_path.string().encode("utf-8"))
        return mime_data

    def mimeTypes(self):
        return [self.reparent_mime, "application/x-orca-asset"]

    def prepare_reparent_data(
        self,
        reparent_data: ReparentData,
        mime_data: QMimeData,
        action,
        row,
        column,
        parent: QModelIndex,
    ) -> bool:
        if not mime_data.hasFormat(self.reparent_mime):
            return False

        if action not in [Qt.CopyAction, Qt.MoveAction]:
            return False

        if column > 0:
            return False

        parent_actor = self.get_actor(parent)

        parent_actor_path = self.local_scene.get_actor_path(parent_actor)
        if parent_actor_path is None:
            return False

        actor_path_bytes = mime_data.data(self.reparent_mime)
        actor_path_str = actor_path_bytes.data().decode("utf-8")

        actor_path = Path(actor_path_str)
        actor = self.local_scene.find_actor_by_path(actor_path)

        # print(f"drop {parent_actor_path}, row: {row}, col:{column}")

        ok, err = self.local_scene.can_reparent_actor(actor, parent_actor)
        if not ok:
            return False

        reparent_data.actor = actor
        reparent_data.actor_path = actor_path
        reparent_data.parent = parent_actor
        reparent_data.parent_path = parent_actor_path

        return True

    @override
    async def before_actor_added(
        self,
        actor: BaseActor,
        parent_actor_path: Path,
        source: str,
    ):
        parent_actor, _ = self.local_scene.get_actor_and_path(parent_actor_path)
        parent_index = self.get_index_from_actor(parent_actor)
        child_count = len(parent_actor.children)

        self.beginInsertRows(parent_index, child_count, child_count)

    @override
    async def on_actor_added(
        self,
        actor: BaseActor,
        parent_actor_path: Path,
        source: str,
    ):
        self.endInsertRows()

    @override
    async def before_actor_deleted(
        self,
        actor_path: Path,
        source: str,
    ):
        actor, _ = self.local_scene.get_actor_and_path(actor_path)
        index = self.get_index_from_actor(actor)
        parent_index = index.parent()

        self.beginRemoveRows(parent_index, index.row(), index.row())

    @override
    async def on_actor_deleted(
        self,
        actor_path: Path,
        source: str,
    ):
        self.endRemoveRows()

    @override
    async def before_actor_renamed(
        self,
        actor_path: Path,
        new_name: str,
        source: str,
    ):
        pass

    @override
    async def on_actor_renamed(
        self,
        actor_path: Path,
        new_name: str,
        source: str,
    ):
        new_path = actor_path.parent() / new_name
        actor, _ = self.local_scene.get_actor_and_path(new_path)
        index = self.get_index_from_actor(actor)
        self.dataChanged.emit(index, index)

    @override
    async def before_actor_reparented(
        self,
        actor_path: Path,
        new_parent_path: Path,
        row: int,
        source: str,
    ):
        print("before reparent")
        self.beginResetModel()

    @override
    async def on_actor_reparented(
        self,
        actor_path: Path,
        new_parent_path: Path,
        row: int,
        source: str,
    ):
        print("after reparent")
        self.endResetModel()


if __name__ == "__main__":
    import unittest
    from orcalab.actor import AssetActor
    from PySide6.QtTest import QAbstractItemModelTester
    from PySide6.QtCore import QModelIndex, Qt

    class TestAddFunction(unittest.TestCase):
        def test_empty_path_is_invalid(self):
            local_scene = LocalScene()

            local_scene.add_actor(GroupActor("g1"), Path("/"))
            local_scene.add_actor(GroupActor("g2"), Path("/"))
            local_scene.add_actor(GroupActor("g3"), Path("/"))
            local_scene.add_actor(GroupActor("g4"), Path("/g2"))
            local_scene.add_actor(AssetActor("a1", "spw_name"), Path("/g3"))

            model = ActorOutlineModel(local_scene)
            model.set_root_group(local_scene.root_actor)

            self.assertEqual(model.rowCount(QModelIndex()), 3)
            index1 = model.index(0, 0, QModelIndex())
            self.assertEqual(index1.isValid(), True)
            self.assertEqual(index1.data(Qt.DisplayRole), "g1")
            self.assertEqual(
                model.parent(model.index(0, 0, QModelIndex())).isValid(), False
            )
            self.assertEqual(
                model.parent(model.index(1, 0, QModelIndex())).isValid(), False
            )
            self.assertEqual(
                model.parent(model.index(2, 0, QModelIndex())).isValid(), False
            )

            mode = QAbstractItemModelTester.FailureReportingMode.Fatal
            tester = QAbstractItemModelTester(model, mode)

    unittest.main()
