# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com>               *
# *                 2020 Schildkroet                                        *
# *                 2025 Samuel Abels <knipknap@gmail.com>                  *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   This program is distributed in the hope that it will be useful,       *
# *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
# *   GNU Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************
import yaml
import pathlib
import FreeCAD
import FreeCADGui
import Path
from PySide.QtGui import (
    QStandardItem,
    QStandardItemModel,
    QPixmap,
    QDialog,
    QMessageBox,
    QWidget,
)
from PySide.QtCore import Qt, QEvent
from typing import List, cast, Tuple, Optional
from ...assets import AssetUri
from ...assets.ui import AssetOpenDialog, AssetSaveDialog
from ...camassets import cam_assets, ensure_assets_initialized
from ...shape.ui.shapeselector import ShapeSelector
from ...toolbit import ToolBit
from ...toolbit.serializers import all_serializers as toolbit_serializers
from ...toolbit.ui import ToolBitEditor
from ...toolbit.ui.toollist import ToolBitUriListMimeType
from ...toolbit.ui.util import natural_sort_key
from ..serializers import all_serializers as library_serializers
from ..models import Library
from .browser import LibraryBrowserWidget
from .properties import LibraryPropertyDialog


if False:
    Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
    Path.Log.trackModule(Path.Log.thisModule())
else:
    Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())


_LibraryRole = Qt.UserRole + 1
translate = FreeCAD.Qt.translate


class LibraryEditor(QWidget):
    """LibraryEditor is the controller for
    displaying/selecting/creating/editing a collection of ToolBits."""

    def __init__(self, parent=None):
        super().__init__(parent=parent)
        Path.Log.track()
        ensure_assets_initialized(cam_assets)
        self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitLibraryEdit.ui")
        self.form.installEventFilter(self)  # to forward keypress events
        self._base_title = self.form.windowTitle()

        # Create the library list.
        self.listModel = QStandardItemModel()
        self.form.TableList.setModel(self.listModel)
        self.form.TableList.clicked.connect(self._on_library_selected)

        # Enable drop support for the library list
        self.form.TableList.viewport().installEventFilter(self)  # Also on viewport

        # Create the LibraryBrowserWidget
        self.browser = LibraryBrowserWidget(
            asset_manager=cam_assets,
            parent=self,
        )
        self.browser.setDragEnabled(True)
        self.form.verticalLayout_2.layout().replaceWidget(self.form.toolTable, self.browser)
        self.form.toolTable.hide()

        # Connect signals.
        self.browser.itemDoubleClicked.connect(self.browser._on_edit_requested)

        self.form.addLibraryButton.clicked.connect(self._on_add_library_requested)
        self.form.removeLibraryButton.clicked.connect(self._on_remove_library_requested)
        self.form.renameLibraryButton.clicked.connect(self._on_rename_library_requested)
        self.form.importLibraryButton.clicked.connect(self._on_import_library_requested)
        self.form.exportLibraryButton.clicked.connect(self._on_export_library_requested)

        self.form.addToolBitButton.clicked.connect(self._on_add_toolbit_requested)
        self.form.importToolBitButton.clicked.connect(self._on_import_toolbit_requested)
        self.form.exportToolBitButton.clicked.connect(self._on_export_toolbit_requested)

        # Populate the UI.
        self._refresh_library_list()
        self._select_last_library()
        self._update_button_states()

    def _highlight_row(self, index):
        """Highlights the row at the given index using the selection model."""
        if not index.isValid():
            return
        self.form.TableList.setCurrentIndex(index)

    def _clear_highlight(self):
        """Clears the highlighting from the previously highlighted row."""
        self.form.TableList.selectionModel().clear()

    def eventFilter(self, obj, event):
        if event.type() == QEvent.KeyPress and self.form.TableList.hasFocus():
            if event.key() == Qt.Key_F2:
                Path.Log.debug("F2 pressed on library list.")
                self._on_rename_library_requested()
                return True
            elif event.key() == Qt.Key_Delete:
                Path.Log.debug("Del pressed on library list.")
                self._on_remove_library_requested()
                return True
        if obj == self.form.TableList.viewport():
            if event.type() == QEvent.DragEnter or event.type() == QEvent.DragMove:
                return self._handle_drag_enter(event)
            elif event.type() == QEvent.DragLeave:
                self._handle_drag_leave(event)
                return True
            elif event.type() == QEvent.Drop:
                return self._handle_drop(event)
        return super().eventFilter(obj, event)

    def _handle_drag_enter(self, event):
        """Handle drag enter and move events for the library list."""
        mime_data = event.mimeData()
        Path.Log.debug(f"_handle_drag_enter: MIME formats: {mime_data.formats()}")
        if not mime_data.hasFormat(ToolBitUriListMimeType):
            Path.Log.debug("_handle_drag_enter: Invalid MIME type, ignoring")
            return True

        # Get the row being hovered.
        pos = event.pos()
        event.acceptProposedAction()
        index = self.form.TableList.indexAt(pos)
        if not index.isValid():
            self._clear_highlight()
            return True

        # Prevent drop into "All Toolbits"
        item = self.listModel.itemFromIndex(index)
        if not item or item.data(_LibraryRole) == "all_tools":
            self._clear_highlight()
            return True

        self._highlight_row(index)
        return True

    def _handle_drag_leave(self, event):
        """Handle drag leave event for the library list."""
        self._clear_highlight()

    def _handle_drop(self, event):
        """Handle drop events to move or copy toolbits to the target library."""
        mime_data = event.mimeData()
        if not (mime_data.hasFormat(ToolBitUriListMimeType)):
            event.ignore()
            return True

        self._clear_highlight()
        pos = event.pos()
        index = self.form.TableList.indexAt(pos)
        if not index.isValid():
            event.ignore()
            return True

        item = self.listModel.itemFromIndex(index)
        if not item or item.data(_LibraryRole) == "all_tools":
            event.ignore()
            return True

        target_library_id = item.data(_LibraryRole)
        target_library_uri = f"toolbitlibrary://{target_library_id}"
        target_library = cast(Library, cam_assets.get(target_library_uri, depth=1))

        try:
            clipboard_content_yaml = mime_data.data(ToolBitUriListMimeType).data().decode("utf-8")
            clipboard_data_dict = yaml.safe_load(clipboard_content_yaml)

            if not isinstance(clipboard_data_dict, dict) or "toolbits" not in clipboard_data_dict:
                event.ignore()
                return True

            uris = clipboard_data_dict["toolbits"]
            new_uris = set()

            # Get the current library from the browser
            current_library = self.browser.get_current_library()

            for uri in uris:
                try:
                    toolbit = cast(ToolBit, cam_assets.get(AssetUri(uri), depth=0))
                    if toolbit:
                        added_toolbit = target_library.add_bit(toolbit)
                        if added_toolbit:
                            new_uris.add(str(toolbit.get_uri()))

                            # Remove the toolbit from the current library if it exists and
                            # it's not "all_tools"
                            if current_library and current_library.get_id() != "all_tools":
                                current_library.remove_bit(toolbit)
                except Exception as e:
                    Path.Log.error(f"Failed to load toolbit from URI {uri}: {e}")
                    continue

            if new_uris:
                cam_assets.add(target_library)
                # Save the current library if it was modified
                if current_library and current_library.get_id() != "all_tools":
                    cam_assets.add(current_library)
                self.browser.refresh()
                self.browser.select_by_uri(list(new_uris))
                self._update_button_states()

            event.acceptProposedAction()
        except Exception as e:
            Path.Log.error(f"Failed to process drop event: {e}")
            event.ignore()
        return True

    def get_selected_library_id(self) -> Optional[str]:
        index = self.form.TableList.currentIndex()
        if not index.isValid():
            return None
        item = self.listModel.itemFromIndex(index)
        if not item:
            return None
        return item.data(_LibraryRole)

    def get_selected_library(self, depth=1) -> Optional[Library]:
        library_id = self.get_selected_library_id()
        if not library_id:
            return None
        uri = f"toolbitlibrary://{library_id}"
        return cast(Library, cam_assets.get(uri, depth=depth))

    def select_library_by_uri(self, uri: AssetUri):
        # Find it in the list.
        index = 0
        for i in range(self.listModel.rowCount()):
            item = self.listModel.item(i)
            if item and item.data(_LibraryRole) == uri.asset_id:
                index = i
                break
        else:
            return

        # Select it.
        if index <= self.listModel.rowCount():
            item = self.listModel.item(index)
            if item:  # Should always be true, but...
                self.form.TableList.setCurrentIndex(self.listModel.index(index, 0))
                self._on_library_selected()

    def _select_last_library(self):
        # Find the last used library.
        last_used_lib_identifier = Path.Preferences.getLastToolLibrary()
        if last_used_lib_identifier:
            uri = Library.resolve_name(last_used_lib_identifier)
            self.select_library_by_uri(uri)

    def open(self):
        Path.Log.track()
        return self.form.exec_()

    def _refresh_library_list(self):
        """Clears and repopulates the self.listModel with available libraries."""
        Path.Log.track()
        self.listModel.clear()

        # Add "All Toolbits" item
        all_tools_item = QStandardItem(translate("CAM", "All Toolbits"))
        all_tools_item.setData("all_tools", _LibraryRole)
        # all_tools_item.setIcon(QPixmap(":/icons/CAM_ToolTable.svg"))
        # Make the "All Toolbits" item bold and italic
        font = all_tools_item.font()
        font.setBold(True)
        font.setItalic(True)
        all_tools_item.setFont(font)
        self.listModel.appendRow(all_tools_item)

        # Use AssetManager to fetch library assets (depth=0 for shallow fetch)
        try:
            # Fetch library assets themselves, not their deep dependencies (toolbits).
            # depth=0 means "fetch this asset, but not its dependencies"
            # The 'fetch' method returns actual Asset objects.
            libraries = cast(List[Library], cam_assets.fetch(asset_type="toolbitlibrary", depth=0))
        except Exception as e:
            Path.Log.error(f"Failed to fetch toolbit libraries: {e}")
            return

        # Sort by label for consistent ordering, falling back to asset_id if label is missing
        for library in sorted(
            libraries,
            key=lambda library: natural_sort_key(library.label or library.get_id()),
        ):
            lib_uri_str = str(library.get_uri())
            libItem = QStandardItem(library.label or library.get_id())
            libItem.setToolTip(f"ID: {library.get_id()}\nURI: {lib_uri_str}")
            libItem.setData(library.get_id(), _LibraryRole)  # Store the library ID
            libItem.setIcon(QPixmap(":/icons/CAM_ToolTable.svg"))
            self.listModel.appendRow(libItem)

        Path.Log.debug("model rows: {}".format(self.listModel.rowCount()))

        self.listModel.setHorizontalHeaderLabels(["Library"])

    def _on_library_selected(self):
        """Sets the current library in the browser when a library is selected."""
        Path.Log.debug("_on_library_selected: Called.")
        index = self.form.TableList.currentIndex()
        item = self.listModel.itemFromIndex(index)
        if not item:
            return
        if item.data(_LibraryRole) == "all_tools":
            selected_library = None
        else:
            selected_library = self.get_selected_library()
        self.browser.set_current_library(selected_library)
        self._update_window_title()
        self._update_button_states()

    def _update_window_title(self):
        """Updates the window title with the current library name."""
        current_library = self.browser.get_current_library()
        if current_library:
            title = f"{self._base_title} - {current_library.label}"
        else:
            title = self._base_title
        self.form.setWindowTitle(title)

    def _update_button_states(self):
        """Updates the enabled state of library management buttons."""
        library_selected = self.browser.get_current_library() is not None
        self.form.addLibraryButton.setEnabled(True)
        self.form.removeLibraryButton.setEnabled(library_selected)
        self.form.renameLibraryButton.setEnabled(library_selected)
        self.form.exportLibraryButton.setEnabled(library_selected)
        self.form.importLibraryButton.setEnabled(True)
        self.form.addToolBitButton.setEnabled(
            True
        )  # Always enabled - can create standalone toolbits
        # TODO: self.form.exportToolBitButton.setEnabled(toolbit_selected)

    def _save_library(self):
        """Internal method to save the current tool library asset"""
        Path.Log.track()
        library = self.browser.get_current_library()
        if not library:
            return

        # Save the modified library asset.
        try:
            cam_assets.add(library)
            Path.Log.debug(f"Library {library.get_uri()} saved")
        except Exception as e:
            Path.Log.error(f"Failed to save library {library.get_uri()}: {e}")
            QMessageBox.critical(
                self.form,
                translate("CAM_ToolBit", "Error Saving Library"),
                str(e),
            )
            raise

    def _on_add_library_requested(self):
        Path.Log.debug("_on_add_library_requested: Called.")
        new_library = Library(FreeCAD.Qt.translate("CAM", "New Library"))
        dialog = LibraryPropertyDialog(new_library, new=True, parent=self)
        if dialog.exec_() != QDialog.Accepted:
            return

        uri = cam_assets.add(new_library)
        Path.Log.debug(f"_on_add_library_requested: New library URI = {uri}")
        self._refresh_library_list()
        self.select_library_by_uri(uri)
        self._update_button_states()

    def _on_remove_library_requested(self):
        """Handles request to remove the selected library."""
        Path.Log.debug("_on_remove_library_requested: Called.")
        current_library = self.browser.get_current_library()
        if not current_library:
            return

        reply = QMessageBox.question(
            self,
            FreeCAD.Qt.translate("CAM", "Confirm Library Removal"),
            FreeCAD.Qt.translate(
                "CAM",
                "Are you sure you want to remove the library '{0}'?\n"
                "This will not delete the toolbits contained within it.",
            ).format(current_library.label),
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No,
        )

        if reply != QMessageBox.Yes:
            return

        try:
            library_uri = current_library.get_uri()
            cam_assets.delete(library_uri)
            Path.Log.info(f"Library {current_library.label} deleted.")
            self._refresh_library_list()
            self.browser.refresh()
            self._update_button_states()
        except FileNotFoundError as e:
            Path.Log.error(f"Failed to delete library {current_library.label}: {e}")
            QMessageBox.critical(
                self,
                FreeCAD.Qt.translate("CAM", "Error"),
                FreeCAD.Qt.translate("CAM", "Failed to delete library '{0}': {1}").format(
                    current_library.label, str(e)
                ),
            )

    def _on_rename_library_requested(self):
        """Handles request to rename the selected library."""
        Path.Log.debug("_on_rename_library_requested: Called.")
        current_library = self.browser.get_current_library()
        if not current_library:
            return

        dialog = LibraryPropertyDialog(current_library, new=False, parent=self)
        if dialog.exec_() != QDialog.Accepted:
            return

        cam_assets.add(current_library)
        self._refresh_library_list()
        self._update_button_states()

    def _on_import_library_requested(self):
        """Handles request to import a library."""
        Path.Log.debug("_on_import_library_requested: Called.")
        dialog = AssetOpenDialog(
            cam_assets, asset_class=Library, serializers=library_serializers, parent=self
        )
        response = dialog.exec_()
        if not response:
            return
        file_path, library = cast(Tuple[pathlib.Path, Library], response)

        try:
            cam_assets.add(library)
            self._refresh_library_list()
            self._update_button_states()
        except Exception as e:
            Path.Log.error(f"Failed to import library: {file_path} {e}")
            QMessageBox.critical(
                self,
                FreeCAD.Qt.translate("CAM", "Error"),
                FreeCAD.Qt.translate("CAM", f"Failed to import library: {file_path} {e}"),
            )

    def _on_export_library_requested(self):
        """Handles request to export the selected library."""
        Path.Log.debug("_on_export_library_requested: Called.")
        current_library = self.browser.get_current_library()
        if not current_library:
            return

        dialog = AssetSaveDialog(asset_class=Library, serializers=library_serializers, parent=self)
        dialog.exec_(current_library)
        self._update_button_states()

    def _on_add_toolbit_requested(self):
        """Handles request to add a new toolbit to the current library or create standalone."""
        Path.Log.debug("_on_add_toolbit_requested: Called.")
        current_library = self.browser.get_current_library()

        # Select the shape for the new toolbit
        selector = ShapeSelector()
        shape = selector.show()
        if shape is None:  # user canceled
            return

        try:
            # Find the appropriate ToolBit subclass based on the shape name
            tool_bit_classes = {b.SHAPE_CLASS.name: b for b in ToolBit.__subclasses__()}
            tool_bit_class = tool_bit_classes.get(shape.name)
            if not tool_bit_class:
                raise ValueError(f"No ToolBit subclass found for shape '{shape.name}'")

            # Create a new ToolBit instance
            new_toolbit = tool_bit_class(shape)
            new_toolbit.label = FreeCAD.Qt.translate("CAM", "New Toolbit")

            # Save the individual toolbit asset first
            tool_asset_uri = cam_assets.add(new_toolbit)
            Path.Log.debug(f"_on_add_toolbit_requested: Saved tool with URI: {tool_asset_uri}")

            # Add the toolbit to the current library if one is selected
            if current_library:
                toolno = current_library.add_bit(new_toolbit)
                Path.Log.debug(
                    f"_on_add_toolbit_requested: Added toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()}) "
                    f"to current_library with number {toolno}."
                )
                # Save the library
                cam_assets.add(current_library)
            else:
                Path.Log.debug(
                    f"_on_add_toolbit_requested: Created standalone toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()})"
                )

        except Exception as e:
            Path.Log.error(f"Failed to create or add new toolbit: {e}")
            QMessageBox.critical(
                self,
                FreeCAD.Qt.translate("CAM", "Error Creating Toolbit"),
                str(e),
            )
            raise

        self.browser.refresh()
        self.browser.select_by_uri([str(new_toolbit.get_uri())])
        self._update_button_states()

    def _on_import_toolbit_requested(self):
        """Handles request to import a toolbit."""
        Path.Log.debug("_on_import_toolbit_requested: Called.")
        current_library = self.browser.get_current_library()
        if not current_library:
            Path.Log.warning("Cannot import toolbit: No library selected.")
            QMessageBox.warning(
                self,
                FreeCAD.Qt.translate("CAM", "Warning"),
                FreeCAD.Qt.translate("CAM", "Please select a library first."),
            )
            return

        dialog = AssetOpenDialog(
            cam_assets, asset_class=ToolBit, serializers=toolbit_serializers, parent=self
        )
        response = dialog.exec_()
        if not response:
            return
        file_path, toolbit = cast(Tuple[pathlib.Path, ToolBit], response)

        # Debug logging for imported toolbit
        Path.Log.info(
            f"IMPORT TOOLBIT: file_path={file_path}, toolbit.id={toolbit.id}, toolbit.label={toolbit.label}"
        )
        import traceback

        stack = traceback.format_stack()
        caller_info = "".join(stack[-3:-1])
        Path.Log.info(f"IMPORT TOOLBIT CALLER:\n{caller_info}")

        # Check if toolbit already exists in asset manager
        toolbit_uri = toolbit.get_uri()
        Path.Log.info(f"IMPORT CHECK: toolbit_uri={toolbit_uri}")
        existing_toolbit = None
        try:
            existing_toolbit = cam_assets.get(toolbit_uri, store=["local", "builtin"], depth=0)
            Path.Log.info(
                f"IMPORT CHECK: Toolbit {toolbit.id} already exists, using existing reference"
            )
            Path.Log.info(
                f"IMPORT CHECK: existing_toolbit.id={existing_toolbit.id}, existing_toolbit.label={existing_toolbit.label}"
            )
        except FileNotFoundError:
            # Toolbit doesn't exist, save it as new
            Path.Log.info(f"IMPORT CHECK: Toolbit {toolbit.id} is new, saving to disk")
            new_uri = cam_assets.add(toolbit)
            Path.Log.info(f"IMPORT CHECK: Toolbit saved with new URI: {new_uri}")
            existing_toolbit = toolbit

        # Add the toolbit (existing or new) to the current library
        Path.Log.info(
            f"IMPORT ADD: Adding toolbit {existing_toolbit.id} to library {current_library.label}"
        )
        added_toolbit = current_library.add_bit(existing_toolbit)
        if added_toolbit:
            Path.Log.info(f"IMPORT ADD: Successfully added toolbit to library")
            cam_assets.add(current_library)  # Save the modified library
            self.browser.refresh()
            self.browser.select_by_uri([str(existing_toolbit.get_uri())])
            self._update_button_states()
        else:
            Path.Log.warning(f"IMPORT ADD: Failed to add toolbit {existing_toolbit.id} to library")
            Path.Log.warning(
                f"IMPORT FAILED: Failed to import toolbit from {file_path} to library {current_library.label}."
            )
            QMessageBox.warning(
                self,
                FreeCAD.Qt.translate("CAM", "Warning"),
                FreeCAD.Qt.translate(
                    "CAM",
                    f"Failed to import toolbit from '{file_path}' to library '{current_library.label}'.",
                ),
            )

    def _on_export_toolbit_requested(self):
        """Handles request to export the selected toolbit."""
        Path.Log.debug("_on_export_toolbit_requested: Called.")
        selected_toolbits = self.browser.get_selected_bits()
        if not selected_toolbits:
            Path.Log.warning("Cannot export toolbit: No toolbit selected.")
            QMessageBox.warning(
                self,
                FreeCAD.Qt.translate("CAM", "Warning"),
                FreeCAD.Qt.translate("CAM", "Please select a toolbit to export."),
            )
            return

        if len(selected_toolbits) > 1:
            Path.Log.warning("Cannot export multiple toolbits: Please select only one.")
            QMessageBox.warning(
                self,
                FreeCAD.Qt.translate("CAM", "Warning"),
                FreeCAD.Qt.translate("CAM", "Please select only one toolbit to export."),
            )
            return

        toolbit_to_export = selected_toolbits[0]
        dialog = AssetSaveDialog(asset_class=ToolBit, serializers=toolbit_serializers, parent=self)
        dialog.exec_(toolbit_to_export)  # This will open the save dialog and handle the export
        self._update_button_states()
