# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2022.
#
# 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.

"""Utility functions for handling linear reversible circuits."""

import copy
from typing import Callable
import numpy as np
from qiskit import QuantumCircuit
from qiskit.exceptions import QiskitError
from qiskit.circuit.exceptions import CircuitError
from . import calc_inverse_matrix, check_invertible_binary_matrix


def transpose_cx_circ(qc: QuantumCircuit):
    """Takes a circuit having only CX gates, and calculates its transpose.
    This is done by recursively replacing CX(i, j) with CX(j, i) in all instructions.

    Args:
        qc: a QuantumCircuit containing only CX gates.

    Returns:
        QuantumCircuit: the transposed circuit.

    Raises:
        CircuitError: if qc has a non-CX gate.
    """
    transposed_circ = QuantumCircuit(qc.qubits, qc.clbits, name=qc.name + "_transpose")
    for instruction in reversed(qc.data):
        if instruction.operation.name != "cx":
            raise CircuitError("The circuit contains non-CX gates.")
        transposed_circ._append(instruction.replace(qubits=reversed(instruction.qubits)))
    return transposed_circ


def optimize_cx_4_options(function: Callable, mat: np.ndarray, optimize_count: bool = True):
    """Get the best implementation of a circuit implementing a binary invertible matrix M,
    by considering all four options: M,M^(-1),M^T,M^(-1)^T.
    Optimizing either the CX count or the depth.

    Args:
        function: the synthesis function.
        mat: a binary invertible matrix.
        optimize_count: True if the number of CX gates in optimize, False if the depth is optimized.

    Returns:
        QuantumCircuit: an optimized QuantumCircuit, has the best depth or CX count of the four options.

    Raises:
        QiskitError: if mat is not an invertible matrix.
    """
    if not check_invertible_binary_matrix(mat):
        raise QiskitError("The matrix is not invertible.")

    qc = function(mat)
    best_qc = qc
    best_depth = qc.depth()
    best_count = qc.count_ops()["cx"]

    for i in range(1, 4):
        mat_cpy = copy.deepcopy(mat)
        # i=1 inverse, i=2 transpose, i=3 transpose and inverse
        if i == 1:
            mat_cpy = calc_inverse_matrix(mat_cpy)
            qc = function(mat_cpy)
            qc = qc.inverse()
        elif i == 2:
            mat_cpy = np.transpose(mat_cpy)
            qc = function(mat_cpy)
            qc = transpose_cx_circ(qc)
        elif i == 3:
            mat_cpy = calc_inverse_matrix(np.transpose(mat_cpy))
            qc = function(mat_cpy)
            qc = transpose_cx_circ(qc)
            qc = qc.inverse()

        new_depth = qc.depth()
        new_count = qc.count_ops()["cx"]
        # Prioritize count, and if it has the same count, then also consider depth
        better_count = (optimize_count and best_count > new_count) or (
            not optimize_count and best_depth == new_depth and best_count > new_count
        )
        # Prioritize depth, and if it has the same depth, then also consider count
        better_depth = (not optimize_count and best_depth > new_depth) or (
            optimize_count and best_count == new_count and best_depth > new_depth
        )

        if better_count or better_depth:
            best_count = new_count
            best_depth = new_depth
            best_qc = qc

    return best_qc


def check_lnn_connectivity(qc: QuantumCircuit) -> bool:
    """Check that the synthesized circuit qc fits linear nearest neighbor connectivity.

    Args:
        qc: a QuantumCircuit containing only CX and single qubit gates.

    Returns:
        bool: True if the circuit has linear nearest neighbor connectivity.

    Raises:
        CircuitError: if qc has a non-CX two-qubit gate.
    """
    for instruction in qc.data:
        if instruction.operation.num_qubits > 1:
            if instruction.operation.name == "cx":
                q0 = qc.find_bit(instruction.qubits[0]).index
                q1 = qc.find_bit(instruction.qubits[1]).index
                dist = abs(q0 - q1)
                if dist != 1:
                    return False
            else:
                raise CircuitError("The circuit has two-qubits gates different than CX.")
    return True
