# This code is part of Qiskit.
#
# (C) Copyright IBM 2019.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# pylint: disable=unused-variable

"""
Multi controlled single-qubit unitary up to diagonal.
"""

# ToDo: This code should be merged wth the implementation of MCGs
# ToDo: (introducing a decomposition mode "up_to_diagonal").

import numpy as np

from qiskit.circuit import Gate
from qiskit.circuit.quantumcircuit import QuantumRegister, QuantumCircuit
from qiskit.quantum_info.operators.predicates import is_isometry
from qiskit.exceptions import QiskitError
from qiskit.circuit.exceptions import CircuitError
from qiskit.extensions.quantum_initializer.uc import UCGate

_EPS = 1e-10  # global variable used to chop very small numbers to zero


class MCGupDiag(Gate):
    """
    Decomposes a multi-controlled gate u up to a diagonal d acting on the control and target qubit
    (but not on the  ancilla qubits), i.e., it implements a circuit corresponding to a unitary u'
    such that u=d.u'.
    """

    def __init__(self, gate, num_controls, num_ancillas_zero, num_ancillas_dirty):
        """Initialize a multi controlled gate.

        Args:
            gate (ndarray): 2*2 unitary (given as a (complex) ndarray)
            num_controls (int): number of control qubits
            num_ancillas_zero (int): number of ancilla qubits that start in the state zero
            num_ancillas_dirty (int): number of ancilla qubits that are allowed to start in an
                arbitrary state
        Raises:
            QiskitError: if the input format is wrong; if the array gate is not unitary
        """

        self.num_controls = num_controls
        self.num_ancillas_zero = num_ancillas_zero
        self.num_ancillas_dirty = num_ancillas_dirty
        # Check if the gate has the right dimension
        if not gate.shape == (2, 2):
            raise QiskitError("The dimension of the controlled gate is not equal to (2,2).")
        # Check if the single-qubit gate is unitary
        if not is_isometry(gate, _EPS):
            raise QiskitError("The controlled gate is not unitary.")
        # Create new gate.
        num_qubits = 1 + num_controls + num_ancillas_zero + num_ancillas_dirty
        super().__init__("MCGupDiag", num_qubits, [gate])

    def _define(self):
        mcg_up_diag_circuit, _ = self._dec_mcg_up_diag()
        gate = mcg_up_diag_circuit.to_instruction()
        q = QuantumRegister(self.num_qubits)
        mcg_up_diag_circuit = QuantumCircuit(q)
        mcg_up_diag_circuit.append(gate, q[:])
        self.definition = mcg_up_diag_circuit

    def inverse(self):
        """Return the inverse.

        Note that the resulting Gate object has an empty ``params`` property.
        """
        inverse_gate = Gate(
            name=self.name + "_dg", num_qubits=self.num_qubits, params=[]
        )  # removing the params because arrays are deprecated

        definition = QuantumCircuit(*self.definition.qregs)
        for inst in reversed(self._definition):
            definition._append(inst.replace(operation=inst.operation.inverse()))
        inverse_gate.definition = definition
        return inverse_gate

    # Returns the diagonal up to which the gate is implemented.
    def _get_diagonal(self):
        # Important: for a control list q_controls = [q[0],...,q_[k-1]] the diagonal gate is
        # provided in the computational basis of the qubits q[k-1],...,q[0],q_target, decreasingly
        # ordered with respect to the significance of the qubit in the computational basis
        _, diag = self._dec_mcg_up_diag()
        return diag

    def _dec_mcg_up_diag(self):
        """
        Call to create a circuit with gates that implement the MCG up to a diagonal gate.
        Remark: The qubits the gate acts on are ordered in the following way:
            q=[q_target,q_controls,q_ancilla_zero,q_ancilla_dirty]
        """
        diag = np.ones(2 ** (self.num_controls + 1)).tolist()
        q = QuantumRegister(self.num_qubits)
        circuit = QuantumCircuit(q)
        (q_target, q_controls, q_ancillas_zero, q_ancillas_dirty) = self._define_qubit_role(q)
        # ToDo: Keep this threshold updated such that the lowest gate count is achieved:
        # ToDo: we implement the MCG with a UCGate up to diagonal if the number of controls is
        # ToDo: smaller than the threshold.
        threshold = float("inf")
        if self.num_controls < threshold:
            # Implement the MCG as a UCGate (up to diagonal)
            gate_list = [np.eye(2, 2) for i in range(2**self.num_controls)]
            gate_list[-1] = self.params[0]
            ucg = UCGate(gate_list, up_to_diagonal=True)
            circuit.append(ucg, [q_target] + q_controls)
            diag = ucg._get_diagonal()
            # else:
            # ToDo: Use the best decomposition for MCGs up to diagonal gates here
            # ToDo: (with all available ancillas)
        return circuit, diag

    def _define_qubit_role(self, q):
        # Define the role of the qubits
        q_target = q[0]
        q_controls = q[1 : self.num_controls + 1]
        q_ancillas_zero = q[self.num_controls + 1 : self.num_controls + 1 + self.num_ancillas_zero]
        q_ancillas_dirty = q[self.num_controls + 1 + self.num_ancillas_zero :]
        return q_target, q_controls, q_ancillas_zero, q_ancillas_dirty

    def validate_parameter(self, parameter):
        """Multi controlled single-qubit unitary gate parameter has to be an ndarray."""
        if isinstance(parameter, np.ndarray):
            return parameter
        else:
            raise CircuitError(f"invalid param type {type(parameter)} in gate {self.name}")
