#!/usr/bin/env python
# cspell:ignore Bambu
"""Common supporting classes and constants for slicer tools."""

from enum import StrEnum
from typing import NamedTuple, Sequence, Any

# create some type aliases
type SettingValue = str | list[str]
type SettingsDict = dict[str, SettingValue]

# Navigation and file name components
DEFAULT_ENCODING = "utf-8"

# json key elements
FROM = "from"
INHERITS = "inherits"
NAME = "name"


class PresetType(StrEnum):
    """Preset types used by Bambu Studio."""

    FILAMENT = "filament"
    MACHINE = "machine"
    PROCESS = "process"


class PresetGroup(StrEnum):
    """Preset groups used by Bambu Studio.

    System refers to presets from the system folder.
    User refers to presets from the user folder.
    Project refers to a filament, process or machine preset in a 3mf file.
    Override refers to the project settings from a 3mf file that override any of
    of the above (this is strictly also project preset, but as it is handled somewhat
    differently to the other presets by BBS, I've created a special category).
    """

    OVERRIDE = "override"
    PROJECT = "project"
    USER = "user"
    SYSTEM = "system"


class NodeMetadata(NamedTuple):
    """Summary metadata relating to Preset Nodes.

    Used for initialising PresetNodes and in generating complete settings lists from
    tree walk.
    """

    # Unique name for preset in project file.
    name: str
    # Source filename for the preset. Source file may contain multiple presets.
    filename: str
    group: PresetGroup
    preset_type: PresetType

    # The override_inherits field deals with an irritating quirk of project
    # overrides - they are defined as differences to a system preset. This system
    # preset is specified as the "inherits_group" value for the project override (i.e.
    # the parent for the differences is relative to the system preset named in
    # "inherits_group").
    #
    # However, in the Bambu Studio interface, orange highlighted values show differences
    # relative to the project specified immediate parent of the override, which may not
    # be the same as the system preset above! The immediate parent is named in:
    # "<printer|print|filament>_settings_id". (i.e. the project settings includes two
    # inheritance paths for overrides!)
    #
    # To capture this information in this package, we need to know both the system
    # preset and the immediate parent preset.
    #
    # I could have handled this by introducing a dummy key/value pair into the
    # the settings dictionary, but this would probably cause bigger problems with
    # end users trying to create a preset containing this dummy parameter.
    #
    # So instead we do a special here to handle the unicorn sparkly tears case
    # of project settings.
    #
    # override_inherits is the name of the immediate parent preset for the override. It
    # may be different from the "inherits" value used to generate
    # "differences_to_system" values.
    #
    # Defaults to empty string and should only be set for group = Preset.OVERRIDE.
    override_inherits: str = ""


class AllNodeSettings(NamedTuple):
    """Container for all of the settings for a preset node (from tree roll up)."""

    # metadata for the source/target node
    metadata: NodeMetadata
    # metadata for the reference node if defined.
    ref_metadata: NodeMetadata | None
    # Settings generated by walking the inheritance tree from the source/target node
    # up to but not including the reference node if defined, or all settings if
    # reference node is not defined).
    # In effect, these are the source node settings that are different to the reference
    # node settings.
    # (If reference settings exist, these settings either override or augment the
    # reference settings.)
    source_subtree: SettingsDict
    # Settings generated by walking the inheritance tree from the reference node to
    # the root of the inheritance tree.
    reference_subtree: SettingsDict


def choose(
    options: Sequence[str | tuple[str, Any]],
    header: str | None = None,
    allow_multi: bool = False,
    no_action: str | None = None,
    help_text: str | None = None,
) -> list[Any] | None:
    """Return user selection or multi-selection from a list of options.

    Arguments:
        options {Sequence[Union[str, Tuple[str, Any]]]} -- A list of options, where
            each may be either:
            * a string, where string is returned if the option is selected; or
            * a of tuple (option_description, return_value), where the return_value
              is returned if the option is selected.

    Keyword Arguments:
        header {str} -- Header/description text for the selection (default: {None})
        allow_multi {bool} -- Allow multi-selection if True (default: {False})
        no_action {str} -- Description value for no selection. If this argument is not
            set or is None, the user must select from the options list.
            (default: {None})
        help_text {str} -- Detailed help text. (default: {None})

    Returns:
        Optional[List[Any]] -- List of return values. If the allow_multi is False, the
        list will contain only one element. If the user has selected no_action, the
        return value is None.

    To select multiple options, enter a comma separated list of values.

    """
    opt_tuple_list = list()
    for this_opt in options:
        if isinstance(this_opt, tuple):
            opt_tuple_list.append(this_opt)
        else:
            opt_tuple_list.append((this_opt, this_opt))

    while True:
        print()
        if header is not None:
            print(header)
            print()

        for i, (key, value) in enumerate(opt_tuple_list):
            print(f"{i + 1: 3d}) {key}")
        if no_action is not None:
            print("  0) " + no_action)
        if help_text is not None:
            print("  h) Help.")

        values: Any = ""
        try:
            # splits on commas, strips white space from values, converts to int
            print()
            values = input("Choose> ").strip()
            values = list(map(lambda x: int(x.strip()) - 1, values.split(",")))

            if no_action is not None and -1 in values:
                return None

            values = [
                opt_tuple_list[value][1]
                for value in values
                if 0 <= value < len(opt_tuple_list)
            ]

            if not values:
                continue

            if allow_multi:
                return values

            if len(values) == 1:
                return values[0:1]

        except (ValueError, IndexError):
            if help_text is not None and values == "h":
                print("-" * 80)
                print()
                print(help_text)
                print()
                print("-" * 80)
                continue
            else:
                print("Invalid input, please try again")
