#  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 PyQt5.QtCore import QModelIndex, QAbstractTableModel, Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem, QColor
from .tags import compile_tags, Tags

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

DXFTagsRole = Qt.UserRole + 1


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:
        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 = ...
    ) -> 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:
        return len(self._tags)

    def columnCount(self, parent: QModelIndex = ...) -> int:
        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
        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:
        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)
        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 "
        "classes in the inheritance chain for the same object. This is in addition "
        "to the requirement for DXF names for each distinct concrete class derived "
        "from ObjectARX (see Subclass Markers)",
    ),
    (101, "Embedded object marker"),
    (
        102,
        "Control string, followed by '{arbitrary name' or '}'. Similar to the "
        "xdata 1002 group code, except that when the string begins with '{', it "
        "can be followed by an arbitrary string whose interpretation is up to the "
        "application. The only other control string allowed is '}' as a group "
        "terminator. AutoCAD does not interpret these strings except during d"
        "rawing audit operations. They are for application use.",
    ),
    (105, "Object handle for DIMVAR symbol table entry"),
    (
        110,
        "UCS origin (appears only if code 72 is set to 1); DXF: X value; APP: 3D point",
    ),
    (
        111,
        "UCS Y-axis (appears only if code 72 is set to 1); DXF: Y value; APP: 3D vector",
    ),
    (
        112,
        "UCS Z-axis (appears only if code 72 is set to 1); DXF: Z value; APP: 3D vector",
    ),
    ((120, 122), "DXF: Y value of UCS origin, UCS X-axis, and UCS Y-axis"),
    ((130, 132), "DXF: Z value of UCS origin, UCS X-axis, and UCS Y-axis"),
    (
        (140, 149),
        "Double-precision floating-point values (points, elevation, and DIMSTYLE settings, for example)",
    ),
    (
        (170, 179),
        "16-bit integer values, such as flag bits representing DIMSTYLE settings",
    ),
    (
        210,
        "Extrusion direction (fixed) "
        + "DXF: X value of extrusion direction "
        + "APP: 3D extrusion direction vector",
    ),
    (220, "DXF: Y value of the extrusion direction"),
    (230, "DXF: Z value of the extrusion direction"),
    ((270, 279), "16-bit integer values"),
    ((280, 289), "16-bit integer value"),
    ((290, 299), "Boolean flag value; 0 = False; 1 = True"),
    ((300, 309), "Arbitrary text strings"),
    (
        (310, 319),
        "Arbitrary binary chunks with same representation and limits as 1004 "
        "group codes: hexadecimal strings of up to 254 characters represent data "
        "chunks of up to 127 bytes",
    ),
    (
        (320, 329),
        "Arbitrary object handles; handle values that are taken 'as is'. They "
        "are not translated during INSERT and XREF operations",
    ),
    (
        (330, 339),
        "Soft-pointer handle; arbitrary soft pointers to other objects within "
        "same DXF file or drawing. Translated during INSERT and XREF operations",
    ),
    (
        (340, 349),
        "Hard-pointer handle; arbitrary hard pointers to other objects within "
        "same DXF file or drawing. Translated during INSERT and XREF operations",
    ),
    (
        (350, 359),
        "Soft-owner handle; arbitrary soft ownership links to other objects "
        "within same DXF file or drawing. Translated during INSERT and XREF "
        "operations",
    ),
    (
        (360, 369),
        "Hard-owner handle; arbitrary hard ownership links to other objects within "
        "same DXF file or drawing. Translated during INSERT and XREF operations",
    ),
    (
        (370, 379),
        "Lineweight enum value (AcDb::LineWeight). Stored and moved around as a 16-bit integer. "
        "Custom non-entity objects may use the full range, but entity classes only use 371-379 DXF "
        "group codes in their representation, because AutoCAD and AutoLISP both always assume a 370 "
        "group code is the entity's lineweight. This allows 370 to behave like other 'common' entity fields",
    ),
    (
        (380, 389),
        "PlotStyleName type enum (AcDb::PlotStyleNameType). Stored and moved around as a 16-bit integer. "
        "Custom non-entity objects may use the full range, but entity classes only use 381-389 "
        "DXF group codes in their representation, for the same reason as the lineweight range",
    ),
    (
        (390, 399),
        "String representing handle value of the PlotStyleName object, basically a hard pointer, but has "
        "a different range to make backward compatibility easier to deal with. Stored and moved around "
        "as an object ID (a handle in DXF files) and a special type in AutoLISP. Custom non-entity objects "
        "may use the full range, but entity classes only use 391-399 DXF group codes in their representation, "
        "for the same reason as the lineweight range",
    ),
    ((400, 409), "16-bit integers"),
    ((410, 419), "String"),
    (
        (420, 427),
        "32-bit integer value. When used with True Color; a 32-bit integer representing a 24-bit color value. "
        "The high-order byte (8 bits) is 0, the low-order byte an unsigned char holding the Blue value (0-255), "
        "then the Green value, and the next-to-high order byte is the Red Value. Converting this integer value to "
        "hexadecimal yields the following bit mask: 0x00RRGGBB. "
        "For example, a true color with Red==200, Green==100 and Blue==50 is 0x00C86432, and in DXF, in decimal, 13132850",
    ),
    (
        (430, 437),
        "String; when used for True Color, a string representing the name of the color",
    ),
    (
        (440, 447),
        "32-bit integer value. When used for True Color, the transparency value",
    ),
    ((450, 459), "Long"),
    ((460, 469), "Double-precision floating-point value"),
    ((470, 479), "String"),
    (
        (480, 481),
        "Hard-pointer handle; arbitrary hard pointers to other objects within same DXF file or drawing. "
        "Translated during INSERT and XREF operations",
    ),
    (
        999,
        "DXF: The 999 group code indicates that the line following it is a comment string. SAVEAS does "
        "not include such groups in a DXF output file, but OPEN honors them and ignores the comments. "
        "You can use the 999 group to include comments in a DXF file that you have edited",
    ),
    (1000, "ASCII string (up to 255 bytes long) in extended data"),
    (
        1001,
        "Registered application name (ASCII string up to 31 bytes long) for extended data",
    ),
    (1002, "Extended data control string ('{' or '}')"),
    (1003, "Extended data layer name"),
    (1004, "Chunk of bytes (up to 127 bytes long) in extended data"),
    (
        1005,
        "Entity handle in extended data; text string of up to 16 hexadecimal digits",
    ),
    (
        1010,
        "A point in extended data; DXF: X value (followed by 1020 and 1030 groups); APP: 3D point",
    ),
    (1020, "DXF: Y values of a point"),
    (1030, "DXF: Z values of a point"),
    (
        1011,
        "A 3D world space position in extended data "
        "DXF: X value (followed by 1021 and 1031 groups) "
        "APP: 3D point",
    ),
    (1021, "DXF: Y value of a world space position"),
    (1031, "DXF: Z value of a world space position"),
    (
        1012,
        "A 3D world space displacement in extended data "
        "DXF: X value (followed by 1022 and 1032 groups) "
        "APP: 3D vector",
    ),
    (1022, "DXF: Y value of a world space displacement"),
    (1032, "DXF: Z value of a world space displacement"),
    (
        1013,
        "A 3D world space direction in extended data "
        "DXF: X value (followed by 1022 and 1032 groups) "
        "APP: 3D vector",
    ),
    (1023, "DXF: Y value of a world space direction"),
    (1033, "DXF: Z value of a world space direction"),
    (1040, "Extended data double-precision floating-point value"),
    (1041, "Extended data distance value"),
    (1042, "Extended data scale factor"),
    (1070, "Extended data 16-bit signed integer"),
    (1071, "Extended data 32-bit signed long"),
]


def build_group_code_tooltip_dict() -> Dict[int, str]:
    tooltips = dict()
    for code, tooltip in GROUP_CODE_TOOLTIPS:
        tooltip = "\n".join(textwrap.wrap(tooltip, width=80))
        if isinstance(code, int):
            tooltips[code] = tooltip
        elif isinstance(code, tuple):
            s, e = code
            for group_code in range(s, e + 1):
                tooltips[group_code] = tooltip
        else:
            raise ValueError(type(code))

    return tooltips


GROUP_CODE_TOOLTIPS_DICT = build_group_code_tooltip_dict()
