# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from __future__ import annotations

from collections.abc import Iterable
from typing import Any, Union

from boltons.setutils import IndexedSet

from braket.registers.qubit import Qubit, QubitInput

QubitSetInput = Union[QubitInput, Iterable[QubitInput]]


def _flatten(other: Any) -> Any:
    if isinstance(other, Iterable) and not isinstance(other, str):
        for item in other:
            yield from _flatten(item)
    else:
        yield other


class QubitSet(IndexedSet):
    """An ordered, unique set of quantum bits.

    Note:
        QubitSet implements `__hash__()` but is a mutable object, therefore be careful when
        mutating this object.
    """

    def __init__(self, qubits: QubitSetInput | None = None):
        """Initializes a `QubitSet`.

        Args:
            qubits (QubitSetInput | None): Qubits to be included in the `QubitSet`.
                Default is `None`.

        Examples:
            >>> qubits = QubitSet([0, 1])
            >>> for qubit in qubits:
            ...     print(qubit)
            ...
            Qubit(0)
            Qubit(1)

            >>> qubits = QubitSet([0, 1, [2, 3]])
            >>> for qubit in qubits:
            ...     print(qubit)
            ...
            Qubit(0)
            Qubit(1)
            Qubit(2)
            Qubit(3)
        """
        _qubits = [Qubit.new(qubit) for qubit in _flatten(qubits)] if qubits is not None else None
        super().__init__(_qubits)

    def map(self, mapping: dict[QubitInput, QubitInput]) -> QubitSet:
        """Creates a new `QubitSet` where this instance's qubits are mapped to the values in `mapping`.
        If this instance contains a qubit that is not in the `mapping` that qubit is not modified.

        Args:
            mapping (dict[QubitInput, QubitInput]): A dictionary of qubit mappings to
                apply. Key is the qubit in this instance to target, and the value is what
                the key will be changed to.

        Returns:
            QubitSet: A new QubitSet with the `mapping` applied.

        Examples:
            >>> qubits = QubitSet([0, 1])
            >>> mapping = {0: 10, Qubit(1): Qubit(11)}
            >>> qubits.map(mapping)
            QubitSet([Qubit(10), Qubit(11)])
        """  # noqa E501
        new_qubits = [mapping.get(qubit, qubit) for qubit in self]

        return QubitSet(new_qubits)

    def __hash__(self):
        return hash(tuple(self))
