import numpy as np
from benchmarks.libraries import abstract


class Qulacs(abstract.ParserBackend):

    def __init__(self):
        import qulacs
        self.name = "qulacs"
        self.qulacs = qulacs
        self.__version__ = None
        self.QuantumState = self.qulacs.QuantumState

    def RX(self, target, theta):
        return self.qulacs.gate.RX(target, -theta)

    def RY(self, target, theta):
        return self.qulacs.gate.RY(target, -theta)

    def RZ(self, target, theta):
        return self.qulacs.gate.RZ(target, -theta)

    def CU1(self, control, target, theta):
        # See `https://github.com/qulacs/qulacs/issues/278` for CU1 on Qulacs
        matrix = np.diag([1, np.exp(1j * theta)])
        gate = self.qulacs.gate.DenseMatrix([target], matrix)
        gate.add_control_qubit(control, 1)
        return gate

    def CU3(self, control, target, theta, phi, lam):
        cost, sint = np.cos(theta / 2.0), np.sin(theta / 2.0)
        pplus, pminus = np.exp(0.5j * (phi + lam)), np.exp(0.5j * (phi - lam))
        matrix = np.array([[np.conj(pplus) * cost, -np.conj(pminus) * sint],
                           [pminus * sint, pplus * cost]])
        gate = self.qulacs.gate.DenseMatrix([target], matrix)
        gate.add_control_qubit(control, 1)
        return gate

    def RZZ(self, target1, target2, theta):
        phase = np.exp(0.5j * theta)
        phasec = np.conj(phase)
        matrix = np.diag([phasec, phase, phase, phasec])
        gate = self.qulacs.gate.DenseMatrix([target1, target2], matrix)
        return gate

    def __getattr__(self, x):
        return getattr(self.qulacs.gate, x)

    def __getitem__(self, x):
        return getattr(self.qulacs.gate, x)

    def from_qasm(self, qasm):
        nqubits, gatelist = self.parse(qasm)
        circuit = self.qulacs.QuantumCircuit(nqubits)
        for gatename, qubits, params in gatelist:
            gate = getattr(self, gatename)
            args = list(qubits)
            if params is not None:
                args.extend(params)
            circuit.add_gate(gate(*args))
        return circuit

    def __call__(self, circuit):
        nqubits = circuit.get_qubit_count()
        state = self.QuantumState(nqubits)
        circuit.update_quantum_state(state)
        return state.get_vector()

    def set_precision(self, precision):
        if precision != "double":
            raise NotImplementedError(f"Cannot set {precision} precision for {self.name} backend.")

    def get_precision(self):
        return "double"

    def get_device(self):
        return None


class QulacsGpu(Qulacs):

    def __init__(self):
        super().__init__()
        self.name = "qulacs-gpu"
        self.QuantumState = self.qulacs.QuantumStateGpu
