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

# ***************************************************************************
# *   Copyright (c) 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 uuid
import pathlib
from typing import Mapping, Union, Optional, List, Dict, cast
import Path
from ...assets import Asset, AssetUri
from ...toolbit import ToolBit


class Library(Asset):
    asset_type: str = "toolbitlibrary"
    API_VERSION = 1

    def __init__(self, label, id=None):
        self.id = id if id is not None else str(uuid.uuid4())
        self._label = label
        self._bits: List[ToolBit] = []
        self._bit_nos: Dict[int, ToolBit] = {}
        self._bit_urls: Dict[AssetUri, ToolBit] = {}

    @property
    def label(self) -> str:
        return self._label

    def get_id(self) -> str:
        """Returns the unique identifier for the Library instance."""
        return self.id

    @classmethod
    def resolve_name(cls, identifier: Union[str, AssetUri, pathlib.Path]) -> AssetUri:
        """
        Resolves various forms of library identifiers to a canonical AssetUri string.
        Handles direct AssetUri objects, URI strings, asset IDs, or legacy filenames.
        Returns the canonical URI string or None if resolution fails.
        """
        if isinstance(identifier, AssetUri):
            return identifier

        if isinstance(identifier, str) and AssetUri.is_uri(identifier):
            return AssetUri(identifier)

        if isinstance(identifier, pathlib.Path):  # Handle direct Path objects (legacy filenames)
            identifier = identifier.stem  # Use the filename stem as potential ID

        if not isinstance(identifier, str):
            raise ValueError("Failed to resolve {identifier} to a Uri")

        return AssetUri.build(asset_type=Library.asset_type, asset_id=identifier)

    def to_dict(self) -> dict:
        """Returns a dictionary representation of the Library in the specified format."""
        tools_list = []
        for tool_no, tool in self._bit_nos.items():
            tools_list.append(
                {"nr": tool_no, "path": f"{tool.get_id()}.fctb"}  # Tool ID with .fctb extension
            )
        return {"label": self.label, "tools": tools_list, "version": self.API_VERSION}

    @classmethod
    def from_dict(
        cls,
        data_dict: dict,
        id: str,
        dependencies: Optional[Mapping[AssetUri, Asset]],
    ) -> "Library":
        """
        Creates a Library instance from a dictionary and resolved dependencies.
        If dependencies is None, it's a shallow load, and tools are not populated.
        """
        library = cls(data_dict.get("label", id or "Unnamed Library"), id=id)

        if dependencies is None:
            Path.Log.debug(
                f"Library.from_dict: Shallow load for library '{library.label}' (id: {id}). Tools not populated."
            )
            return library  # Only process tools if dependencies were resolved

        tools_list = data_dict.get("tools", [])
        for tool_data in tools_list:
            tool_no = tool_data["nr"]
            tool_id = pathlib.Path(tool_data["path"]).stem  # Extract tool ID
            tool_uri = AssetUri(f"toolbit://{tool_id}")
            bit = cast(ToolBit, dependencies.get(tool_uri))
            if bit:
                library.add_bit(bit, bit_no=tool_no)
            else:
                raise ValueError(f"Tool with id {tool_id} not found in dependencies")
        return library

    def __str__(self):
        return '{} "{}"'.format(self.id, self.label)

    def __eq__(self, other):
        return self.id == other.id

    def __iter__(self):
        return self._bits.__iter__()

    def get_next_bit_no(self):
        bit_nolist = sorted(self._bit_nos, reverse=True)
        return bit_nolist[0] + 1 if bit_nolist else 1

    def get_bit_no_from_bit(self, bit: ToolBit) -> Optional[int]:
        for bit_no, thebit in self._bit_nos.items():
            if bit == thebit:
                return bit_no
        return None

    def get_tool_by_uri(self, uri: AssetUri) -> Optional[ToolBit]:
        for tool in self._bit_nos.values():
            if tool.get_uri() == uri:
                return tool
        return None

    def assign_new_bit_no(self, bit: ToolBit, bit_no: Optional[int] = None) -> int:
        if bit not in self._bits:
            raise ValueError(f"given bit {bit} not in library; cannot assign tool number")

        # If no specific bit_no was requested, assign a new one.
        if bit_no is None:
            bit_no = self.get_next_bit_no()
        elif self._bit_nos.get(bit_no) == bit:
            return bit_no

        # Otherwise, add the bit. Since the requested bit_no may already
        # be in use, we need to account for that. In this case, we will
        # add the removed bit into a new bit_no.
        old_bit = self._bit_nos.pop(bit_no, None)
        old_bit_no = self.get_bit_no_from_bit(bit)
        if old_bit_no:
            del self._bit_nos[old_bit_no]
        self._bit_nos[bit_no] = bit
        if old_bit:
            self.assign_new_bit_no(old_bit)
        return bit_no

    def add_bit(self, bit: ToolBit, bit_no: Optional[int] = None) -> int:
        if bit not in self._bits:
            self._bits.append(bit)
        return self.assign_new_bit_no(bit, bit_no)

    def get_bits(self) -> List[ToolBit]:
        return self._bits

    def has_bit(self, bit: ToolBit) -> bool:
        for t in self._bits:
            if bit.id == t.id:
                return True
        return False

    def remove_bit(self, bit: ToolBit):
        self._bits = [t for t in self._bits if t.id != bit.id]
        self._bit_nos = {k: v for (k, v) in self._bit_nos.items() if v.id != bit.id}

    def remove_bit_by_uri(self, uri: AssetUri | str):
        if isinstance(uri, str):
            uri = AssetUri(uri)
        self._bits = [t for t in self._bits if t.get_uri() != uri]
        self._bit_nos = {k: v for (k, v) in self._bit_nos.items() if v.get_uri() != uri}

    def dump(self, summarize: bool = False):
        title = 'Library "{}" ({}) (instance {})'.format(self.label, self.id, id(self))
        print("-" * len(title))
        print(title)
        print("-" * len(title))
        for bit in self._bits:
            print(f"- {bit.label} ({bit.get_id()})")
        print()
