#  Copyright (c) 2021, Manfred Moitzi
#  License: MIT License
from typing import Any, List, Dict, Optional
import textwrap
from ezdxf.lldxf.types import (
    render_tag,
    DXFVertex,
    GROUP_MARKERS,
    POINTER_CODES,
)
from ezdxf.addons.xqt import QModelIndex, QAbstractTableModel, Qt
from ezdxf.addons.xqt import QStandardItemModel, QStandardItem, QColor
from .tags import compile_tags, Tags

__all__ = [
    "DXFTagsModel",
    "DXFStructureModel",
    "EntityContainer",
    "Entity",
    "DXFTagsRole",
]

DXFTagsRole = Qt.UserRole + 1  # type: ignore


def name_fmt(handle, name: str) -> str:
    if handle is None:
        return name
    else:
        return f"<{handle}> {name}"


HEADER_LABELS = ["Group Code", "Data Type", "Content", "4", "5"]


def calc_line_numbers(start: int, tags: Tags) -> List[int]:
    numbers = [start]
    index = start
    for tag in tags:
        if isinstance(tag, DXFVertex):
            index += len(tag.value) * 2
        else:
            index += 2
        numbers.append(index)
    return numbers


class DXFTagsModel(QAbstractTableModel):
    def __init__(
        self, tags: Tags, start_line_number: int = 1, valid_handles=None
    ):
        super().__init__()
        self._tags = compile_tags(tags)
        self._line_numbers = calc_line_numbers(start_line_number, self._tags)
        self._valid_handles = valid_handles or set()

    def data(self, index: QModelIndex, role: int = ...) -> Any:  # type: ignore
        def is_invalid_handle(tag):
            if (
                tag.code in POINTER_CODES
                and not tag.value.upper() in self._valid_handles
            ):
                return True
            return False

        if role == Qt.DisplayRole:
            tag = self._tags[index.row()]
            return render_tag(tag, index.column())
        elif role == Qt.ForegroundRole:
            tag = self._tags[index.row()]
            if tag.code in GROUP_MARKERS:
                return QColor("blue")
            elif is_invalid_handle(tag):
                return QColor("red")
        elif role == DXFTagsRole:
            return self._tags[index.row()]
        elif role == Qt.ToolTipRole:
            code, value = self._tags[index.row()]
            if index.column() == 0:  # group code column
                return GROUP_CODE_TOOLTIPS_DICT.get(code)

            code, value = self._tags[index.row()]
            if code in POINTER_CODES:
                if value.upper() in self._valid_handles:
                    return f"Double click to go to the referenced entity"
                else:
                    return f"Handle does not exist"
            elif code == 0:
                return f"Double click to go to the DXF reference provided by Autodesk"

    def headerData(
        self, section: int, orientation: Qt.Orientation, role: int = ...  # type: ignore
    ) -> Any:
        if orientation == Qt.Horizontal:
            if role == Qt.DisplayRole:
                return HEADER_LABELS[section]
            elif role == Qt.TextAlignmentRole:
                return Qt.AlignLeft
        elif orientation == Qt.Vertical:
            if role == Qt.DisplayRole:
                return self._line_numbers[section]
            elif role == Qt.ToolTipRole:
                return "Line number in DXF file"

    def rowCount(self, parent: QModelIndex = ...) -> int:  # type: ignore
        return len(self._tags)

    def columnCount(self, parent: QModelIndex = ...) -> int:  # type: ignore
        return 3

    def compiled_tags(self) -> Tags:
        """Returns the compiled tags. Only points codes are compiled, group
        code 10, ...
        """
        return self._tags

    def line_number(self, row: int) -> int:
        """Return the DXF file line number of the widget-row."""
        try:
            return self._line_numbers[row]
        except IndexError:
            return 0


class EntityContainer(QStandardItem):
    def __init__(self, name: str, entities: List[Tags]):
        super().__init__()
        self.setEditable(False)
        self.setText(name + f" ({len(entities)})")
        self.setup_content(entities)

    def setup_content(self, entities):
        self.appendRows([Entity(e) for e in entities])


class Classes(EntityContainer):
    def setup_content(self, entities):
        self.appendRows([Class(e) for e in entities])


class AcDsData(EntityContainer):
    def setup_content(self, entities):
        self.appendRows([AcDsEntry(e) for e in entities])


class NamedEntityContainer(EntityContainer):
    def setup_content(self, entities):
        self.appendRows([NamedEntity(e) for e in entities])


class Tables(EntityContainer):
    def setup_content(self, entities):
        container = []
        name = ""
        for e in entities:
            container.append(e)
            dxftype = e.dxftype()
            if dxftype == "TABLE":
                try:
                    handle = e.get_handle()
                except ValueError:
                    handle = None
                name = e.get_first_value(2, default="UNDEFINED")
                name = name_fmt(handle, name)
            elif dxftype == "ENDTAB":
                if container:
                    container.pop()  # remove ENDTAB
                    self.appendRow(NamedEntityContainer(name, container))
                container.clear()


class Blocks(EntityContainer):
    def setup_content(self, entities):
        container = []
        name = "UNDEFINED"
        for e in entities:
            container.append(e)
            dxftype = e.dxftype()
            if dxftype == "BLOCK":
                try:
                    handle = e.get_handle()
                except ValueError:
                    handle = None
                name = e.get_first_value(2, default="UNDEFINED")
                name = name_fmt(handle, name)
            elif dxftype == "ENDBLK":
                if container:
                    self.appendRow(EntityContainer(name, container))
                container.clear()


def get_section_name(section: List[Tags]) -> str:
    if len(section) > 0:
        header = section[0]
        if len(header) > 1 and header[0].code == 0 and header[1].code == 2:
            return header[1].value
    return "INVALID SECTION HEADER!"


class Entity(QStandardItem):
    def __init__(self, tags: Tags):
        super().__init__()
        self.setEditable(False)
        self._tags = tags
        self._handle: Optional[str]
        try:
            self._handle = tags.get_handle()
        except ValueError:
            self._handle = None
        self.setText(self.entity_name())

    def entity_name(self):
        name = "INVALID ENTITY!"
        tags = self._tags
        if tags and tags[0].code == 0:
            name = name_fmt(self._handle, tags[0].value)
        return name

    def data(self, role: int = ...) -> Any:  # type: ignore
        if role == DXFTagsRole:
            return self._tags
        else:
            return super().data(role)


class Header(Entity):
    def entity_name(self):
        return "HEADER"


class ThumbnailImage(Entity):
    def entity_name(self):
        return "THUMBNAILIMAGE"


class NamedEntity(Entity):
    def entity_name(self):
        name = self._tags.get_first_value(2, "<noname>")
        return name_fmt(str(self._handle), name)


class Class(Entity):
    def entity_name(self):
        tags = self._tags
        name = "INVALID CLASS!"
        if len(tags) > 1 and tags[0].code == 0 and tags[1].code == 1:
            name = tags[1].value
        return name


class AcDsEntry(Entity):
    def entity_name(self):
        return self._tags[0].value


class DXFStructureModel(QStandardItemModel):
    def __init__(self, filename: str, doc):
        super().__init__()
        root = QStandardItem(filename)
        root.setEditable(False)
        self.appendRow(root)
        row: Any
        for section in doc.sections.values():
            name = get_section_name(section)
            if name == "HEADER":
                row = Header(section[0])
            elif name == "THUMBNAILIMAGE":
                row = ThumbnailImage(section[0])
            elif name == "CLASSES":
                row = Classes(name, section[1:])
            elif name == "TABLES":
                row = Tables(name, section[1:])
            elif name == "BLOCKS":
                row = Blocks(name, section[1:])
            elif name == "ACDSDATA":
                row = AcDsData(name, section[1:])
            else:
                row = EntityContainer(name, section[1:])
            root.appendRow(row)

    def index_of_entity(self, entity: Tags) -> QModelIndex:
        root = self.item(0, 0)
        index = find_index(root, entity)
        if index is None:
            return root.index()
        else:
            return index


def find_index(item: QStandardItem, entity: Tags) -> Optional[QModelIndex]:
    def _find(sub_item: QStandardItem):
        for index in range(sub_item.rowCount()):
            child = sub_item.child(index, 0)
            tags = child.data(DXFTagsRole)
            if tags and tags is entity:
                return child.index()
            if child.rowCount() > 0:
                index2 = _find(child)
                if index2 is not None:
                    return index2
        return None

    return _find(item)


GROUP_CODE_TOOLTIPS = [
    (0, "Text string indicating the entity type (fixed)"),
    (1, "Primary text value for an entity"),
    (2, "Name (attribute tag, block name, and so on)"),
    ((3, 4), "Other text or name values"),
    (5, "Entity handle; text string of up to 16 hexadecimal digits (fixed)"),
    (6, "Linetype name (fixed)"),
    (7, "Text style name (fixed)"),
    (8, "Layer name (fixed)"),
    (
        9,
        "DXF: variable name identifier (used only in HEADER section of the DXF file)",
    ),
    (
        10,
        "Primary point; this is the start point of a line or text entity, center "
        "of a circle, and so on DXF: X value of the primary point (followed by Y "
        "and Z value codes 20 and 30) APP: 3D point (list of three reals)",
    ),
    (
        (11, 18),
        "Other points DXF: X value of other points (followed by Y value codes "
        "21-28 and Z value codes 31-38) APP: 3D point (list of three reals)",
    ),
    (20, "DXF: Y value of the primary point"),
    (30, "DXF: Z value of the primary point"),
    ((21, 28), "DXF: Y values of other points"),
    ((31, 37), "DXF: Z values of other points"),
    (38, "DXF: entity's elevation if nonzero"),
    (39, "Entity's thickness if nonzero (fixed)"),
    (
        (40, 47),
        "Double-precision floating-point values (text height, scale factors, and so on)",
    ),
    (48, "Linetype scale; default value is defined for all entity types"),
    (
        49,
        "Multiple 49 groups may appear in one entity for variable-length tables "
        "(such as the dash lengths in the LTYPE table). A 7x group always appears "
        "before the first 49 group to specify the table length",
    ),
    (
        (50, 58),
        "Angles (output in degrees to DXF files and radians through AutoLISP and ObjectARX applications)",
    ),
    (
        60,
        "Entity visibility; absence or 0 indicates visibility; 1 indicates invisibility",
    ),
    (62, "Color number (fixed)"),
    (66, "Entities follow flag (fixed)"),
    (67, "0 for model space or 1 for paper space (fixed)"),
    (
        68,
        "APP: identifies whether viewport is on but fully off screen; is not active or is off",
    ),
    (69, "APP: viewport identification number"),
    ((70, 79), "Integer values, such as repeat counts, flag bits, or modes"),
    ((90, 99), "32-bit integer values"),
    (
        100,
        "Subclass data marker (with derived class name as a string). "
        "Required for all objects and entity classes that are derived from "
        "another concrete class. The subclass data marker segregates data defined by different "