# Copyright 2019 The Blueqat Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License 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.
"""
`gate` module implements quantum gate operations.
This module is internally used.
"""

import copy
from typing import Any, Callable, Dict, List, Optional, Tuple

from ..gate import Operation


class Backend:
    """Abstract quantum gate processor backend class."""
    def copy(self) -> "Backend":
        """Returns (deep)copy of Backend.

        Backend developer must support `copy` method.
        If required, the developer can override this method.
        """
        return copy.deepcopy(self)

    def _preprocess_run(self, gates: List[Operation], n_qubits: int,
                        args: Tuple[Any], kwargs: Dict[Any, Any]) -> Any:
        """Preprocess of backend run.
        Backend developer can override this function.
        """
        return gates, None

    def _postprocess_run(self, ctx: Any) -> Any:
        """Postprocess of backend run
        Backend developer can override this function.
        """
        return None

    def _run_gates(self, gates: List[Operation], n_qubits: int,
                   ctx: Any) -> Any:
        """Iterate gates and call backend's action for each gates"""
        for gate in gates:
            action = self._get_action(gate)
            if action is not None:
                ctx = action(gate, ctx)
            else:
                ctx = self._run_gates(gate.fallback(n_qubits), n_qubits, ctx)
        return ctx

    def _run(self, gates: List[Operation], n_qubits: int, args: Tuple[Any],
             kwargs: Dict[Any, Any]) -> Any:
        """Default implementation of `Backend.run`.
        Backend developer shouldn't override this function, but override `run` instead of this.

        The default flow of running is:
            1. preprocessing
            2. call the gate action which defined in backend
            3. postprocessing

        Backend developer can:
            1. Define preprocessing process. Override `_preprocess_run`
            2. Define the gate action. Define methods `gate_{gate.lowername}`,
               for example, `gate_x` for X gate, `gate_cx` for CX gate.
            3. Define postprocessing process (and make return value). Override `_postprocess_run`
        Otherwise, the developer can override `run` method if they want to change the flow of run.
        """
        gates, ctx = self._preprocess_run(gates, n_qubits, args, kwargs)
        self._run_gates(gates, n_qubits, ctx)
        return self._postprocess_run(ctx)

    def make_cache(self, gates: List[Operation], n_qubits: int):
        """Make internal cache to reduce the time of running the circuit.

        Some backends may implement this method. Otherwise, this method do nothing.
        This is a temporary API and may changed or deprecated in future."""
        return None

    def run(self, gates: List[Operation], n_qubits: int, *args, **kwargs):
        """Run the backend."""
        return self._run(gates, n_qubits, args, kwargs)

    def _get_action(self, gate: Operation) -> Optional[Callable]:
        try:
            return getattr(self, "gate_" + gate.lowername)
        except AttributeError:
            return None

    def _has_action(self, gate: Operation) -> bool:
        return hasattr(self, "gate_" + gate.lowername)

    def _resolve_fallback(self, gates: Operation,
                          n_qubits: int) -> List[Operation]:
        """Resolve fallbacks and flatten gates."""
        flattened = []
        for g in gates:
            if self._has_action(g):
                flattened.append(g)
            else:
                flattened += self._resolve_fallback(g.fallback(n_qubits),
                                                    n_qubits)
        return flattened
