#  Copyright (c) 2022, Manfred Moitzi
#  License: MIT License
from __future__ import annotations
from typing import Iterator, Iterable, Optional
import ezdxf
from ezdxf.addons.xqt import (
    QtWidgets,
    QtGui,
    QAction,
    QMessageBox,
    QFileDialog,
    Qt,
    QModelIndex,
)
from ezdxf.document import Drawing
from ezdxf.entities import Body
from ezdxf.lldxf.const import DXFStructureError
from .data import AcisData, BinaryAcisData, TextAcisData


APP_NAME = "ACIS Structure Browser"
BROWSER_WIDTH = 1024
BROWSER_HEIGHT = 768
SELECTOR_WIDTH_FACTOR = 0.20
FONT_FAMILY = "monospaced"


def make_font():
    font = QtGui.QFont(FONT_FAMILY)
    font.setStyleHint(QtGui.QFont.Monospace)
    return font


class AcisStructureBrowser(QtWidgets.QMainWindow):
    def __init__(
        self,
        filename: str = "",
        handle: str = "",
    ):
        super().__init__()
        self.doc: Optional[Drawing] = None
        self.acis_entities: list[AcisData] = []
        self.current_acis_entity = AcisData()
        self.entity_selector = self.make_entity_selector()
        self.acis_content_viewer = self.make_content_viewer()
        self.statusbar = QtWidgets.QStatusBar(self)
        self.setup_actions()
        self.setup_menu()

        if filename:
            self.load_dxf(filename)
        else:
            self.setWindowTitle(APP_NAME)

        self.setStatusBar(self.statusbar)
        self.setCentralWidget(self.make_central_widget())
        self.resize(BROWSER_WIDTH, BROWSER_HEIGHT)
        self.connect_slots()
        if handle:
            try:
                int(handle, 16)
            except ValueError:
                msg = f"Given handle is not a hex value: '{handle}'"
                self.statusbar.showMessage(msg)
                print(msg)
            else:
                if not self.goto_handle(handle):
                    msg = f"Handle '{handle}' not found."
                    self.statusbar.showMessage(msg)
                    print(msg)

    def make_entity_selector(self):
        return QtWidgets.QListWidget(self)

    def make_content_viewer(self):
        viewer = QtWidgets.QPlainTextEdit(self)
        viewer.setReadOnly(True)
        viewer.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
        return viewer

    def make_central_widget(self):
        container = QtWidgets.QSplitter(Qt.Horizontal)
        container.addWidget(self.entity_selector)
        container.addWidget(self.acis_content_viewer)
        selector_width = int(BROWSER_WIDTH * SELECTOR_WIDTH_FACTOR)
        entity_view_width = BROWSER_WIDTH - selector_width
        container.setSizes([selector_width, entity_view_width])
        container.setCollapsible(0, False)
        container.setCollapsible(1, False)
        return container

    def connect_slots(self):
        self.entity_selector.clicked.connect(self.acis_entity_activated)
        self.entity_selector.activated.connect(self.acis_entity_activated)

    # noinspection PyAttributeOutsideInit
    def setup_actions(self):
        self._open_action = self.make_action(
            "&Open DXF File...", self.open_dxf, shortcut="Ctrl+O"
        )
        self._reload_action = self.make_action(
            "Reload DXF File",
            self.reload_dxf,
            shortcut="Ctrl+R",
        )
        self._export_entity_action = self.make_action(
            "&Export Current Entity View...",
            self.export_entity,
            shortcut="Ctrl+E",
        )
        self._export_raw_data_action = self.make_action(
            "&Export Raw SAT/SAB Data...",
            self.export_raw_entity,
            shortcut="Ctrl+W",
        )
        self._quit_action = self.make_action(
            "&Quit", self.close, shortcut="Ctrl+Q"
        )

    def make_action(
        self,
        name,
        slot,
        *,
        shortcut: str = "",
        tip: str = "",
    ) -> QAction:
        action = QAction(name, self)
        if shortcut:
            action.setShortcut(shortcut)
        if tip:
            action.setToolTip(tip)
        action.triggered.connect(slot)  # type: ignore
        return action

    def setup_menu(self):
        menu = self.menuBar()
        file_menu = menu.addMenu("&File")
        file_menu.addAction(self._open_action)
        file_menu.addAction(self._reload_action)
        file_menu.addSeparator()
        file_menu.addAction(self._export_entity_action)
        file_menu.addAction(self._export_raw_data_action)
        file_menu.addSeparator()
        file_menu.addAction(self._quit_action)

    def open_dxf(self):
        path, _ = QtWidgets.QFileDialog.getOpenFileName(
            self,
            caption="Select DXF file",
            filter="DXF Documents (*.dxf *.DXF)",
        )
        if path:
            self.load_dxf(path)

    def load_dxf(self, path: str):
        try:
            doc = ezdxf.readfile(path)
        except IOError as e:
            QMessageBox.critical(self, "Loading Error", str(e))
            return
        except DXFStructureError as e:
            QMessageBox.critical(
                self,
                "DXF Structure Error",
                f'Invalid DXF file "{path}": {str(e)}',
            )
            return
        entities = list(get_acis_entities(doc))
        if len(entities):
            self.doc = doc
            self.set_acis_entities(entities)
            self.update_title(path)
            self.statusbar.showMessage(self.make_loading_message())
        else:
            msg = f"DXF file '{path}' contains no ACIS data"
            QMessageBox.information(self, "Loading Error", msg)

    def make_loading_message(self) -> str:
        assert self.doc is not None
        dxfversion = self.doc.dxfversion
        acis_type = "SAB" if dxfversion >= "AC1027" else "SAT"
        return f"Loaded DXF file has version {self.doc.acad_release}/{dxfversion}" \
               f" and contains {acis_type} data"

    def set_acis_entities(self, entities: list[AcisData]):
        self.acis_entities = entities
        self.update_entity_selector(entities)
        self.set_current_acis_entity(entities[0])

    def reload_dxf(self):
        try:
            index = self.acis_entities.index(self.current_acis_entity)
        except IndexError:
            index = -1
        self.load_dxf(self.doc.filename)
        if index > 0:
            self.set_current_acis_entity(self.acis_entities[index])

    def export_entity(self):
        dxf_entity = self.get_current_dxf_entity()
        if dxf_entity is None:
            return
        path, _ = QFileDialog.getSaveFileName(
            self,  # type: ignore
            caption="Export Current Entity View",
            dir=f"{dxf_entity.dxftype()}-{dxf_entity.dxf.handle}.txt",
            filter="Text Files (*.txt *.TXT)",
        )
        if path:
            write_data(self.current_acis_entity, path)

    def export_raw_entity(self):
        dxf_entity = self.get_current_dxf_entity()
        if dxf_entity is None:
            return
        filename = f"{dxf_entity.dxftype()}-{dxf_entity.dxf.handle}"
        sab = dxf_entity.has_binary_data
        if sab:
            filter_ = "Standard ACIS Binary Files (*.sab *.SAB)"
            filename += ".sab"
        else:
            filter_ = "Standard ACIS Text Files (*.sat *.SAT)"
            filename += ".sat"

        path, _ = QFileDialog.getSaveFileName(
            self,  # type: ignore
            caption="Export ACIS Raw Data",
            dir=filename,
            filter=filter_,
        )
        if path:
            if sab:
                with open(path, "wb") as fp:
                    fp.write(dxf_entity.sab)
            else:
                with open(path, "wt") as fp:
                    fp.write("\n".join(dxf_entity.sat))

    def get_current_dxf_entity(self) -> Optional[Body]:
        current = self.current_acis_entity
        if not current.handle or self.doc is None:
            return None
        return self.doc.entitydb.get(current.handle)  # type: ignore

    def update_title(self, path: str):
        self.setWindowTitle(f"{APP_NAME} - {path}")

    def acis_entity_activated(self, index: QModelIndex):
        if len(self.acis_entities) == 0:
            return
        try:
            self.set_current_acis_entity(self.acis_entities[index.row()])
        except IndexError:
            self.set_current_acis_entity(self.acis_entities[0])

    def set_current_acis_entity(self, entity: AcisData):
        if entity:
            self.current_acis_entity = entity
            self.update_acis_content_viewer(entity)

    def update_acis_content_viewer(self, entity: AcisData):
        viewer = self.acis_content_viewer
        viewer.clear()
        viewer.setPlainText("\n".join(entity.lines))

    def update_entity_selector(self, entities: Iterable[AcisData]):
        viewer = self.entity_selector
        viewer.clear()
        viewer.addItems([e.name for e in entities])

    def goto_handle(self, handle: str) -> bool:
        for entity in self.acis_entities:
            if entity.handle == handle:
                self.set_current_acis_entity(entity)
                return True
        return False


def get_acis_entities(doc: Drawing) -> Iterator[AcisData]:
    for e in doc.entitydb.values():
        if isinstance(e, Body):
            handle = e.dxf.handle
            name = f"<{handle}> {e.dxftype()}"
            if e.has_binary_data:
                yield BinaryAcisData(e.sab, name, handle)
            else:
                yield TextAcisData(e.sat, name, handle)


def write_data(entity: AcisData, path: str):
    try:
        with open(path, "wt") as fp:
            fp.write("\n".join(entity.lines))
    except IOError:
        pass

