from abc import ABC, abstractmethod
import numpy as np


class AbstractBackend(ABC):

    def __init__(self):
        self.name = None
        self.__version__ = None

    @abstractmethod
    def from_qasm(self, qasm):
        raise NotImplementedError

    @abstractmethod
    def __call__(self, circuit):
        raise NotImplementedError

    def transpose_state(self, x):
        """Switch order of qubits in state vector to be compatible to Qibo."""
        shape = tuple(x.shape)
        nqubits = int(np.log2(shape[0]))
        x = np.reshape(x, nqubits * (2,))
        x = np.transpose(x, range(nqubits - 1, -1, -1))
        return np.reshape(x, shape)

    @abstractmethod
    def get_precision(self):
        raise NotImplementedError

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

    @abstractmethod
    def get_device(self):
        raise NotImplementedError


class ParserBackend(AbstractBackend):

    QASM_GATES = {"h": "H", "x": "X", "y": "Y", "z": "Z",
                  "rx": "RX", "ry": "RY", "rz": "RZ",
                  "u1": "U1", "u2": "U2", "u3": "U3",
                  "cx": "CNOT", "swap": "SWAP", "cz": "CZ",
                  "crx": "CRX", "cry": "CRY", "crz": "CRZ",
                  "cu1": "CU1", "cu3": "CU3", "rzz": "RZZ",
                  "ccx": "TOFFOLI", "id": "I"}
    PARAMETRIZED_GATES = {"rx", "ry", "rz", "u1", "u2", "u3",
                          "crx", "cry", "crz", "cu1", "cu3", "rzz"}

    def parse(self, qasm_code):
        """Extracts circuit information from QASM script.

        Args:
            qasm_code: String with the QASM code to parse.

        Returns:
            nqubits: The total number of qubits in the circuit.
            gate_list: List that specifies the gates of the circuit.
                Contains tuples of the form
                (Qibo gate name, qubit IDs, optional additional parameter).
                The additional parameter is the ``register_name`` for
                measurement gates or ``theta`` for parametrized gates.
        """
        import re
        def read_args(args):
            _args = iter(re.split(r"[\[\],]", args))
            for name in _args:
                if name:
                    index = next(_args)
                    if not index.isdigit():
                        raise ValueError("Invalid QASM qubit arguments: {}".format(args))
                    yield name, int(index)

        # Remove comment lines
        lines = "".join(line for line in qasm_code.split("\n")
                        if line and line[:2] != "//")
        lines = (line for line in lines.split(";") if line)

        if next(lines) != "OPENQASM 2.0":
            raise ValueError("QASM code should start with 'OPENQASM 2.0'.")

        qubits = {} # Dict[Tuple[str, int], int]: map from qubit tuple to qubit id
        cregs_size = {} # Dict[str, int]: map from `creg` name to its size
        registers = {} # Dict[str, List[int]]: map from register names to target qubit ids
        gate_list = [] # List[Tuple[str, List[int]]]: List of (gate name, list of target qubit ids)
        for line in lines:
            command, args = line.split(None, 1)
            # remove spaces
            command = command.replace(" ", "")
            args = args.replace(" ", "")

            if command == "include":
                pass

            elif command == "qreg":
                for name, nqubits in read_args(args):
                    for i in range(nqubits):
                        qubits[(name, i)] = len(qubits)

            elif command == "creg":
                for name, nqubits in read_args(args):
                    cregs_size[name] = nqubits

            elif command == "measure":
                args = args.split("->")
                if len(args) != 2:
                    raise ValueError("Invalid QASM measurement: {}".format(line))
                qubit = next(read_args(args[0]))
                if qubit not in qubits:
                    raise ValueError("Qubit {} is not defined in QASM code."
                                     "".format(qubit))

                register, idx = next(read_args(args[1]))
                if register not in cregs_size:
                    raise ValueError("Classical register name {} is not defined "
                                     "in QASM code.".format(register))
                if idx >= cregs_size[register]:
                    raise ValueError("Cannot access index {} of register {} "
                                     "with {} qubits."
                                     "".format(idx, register, cregs_size[register]))
                if register in registers:
                    if idx in registers[register]:
                        raise KeyError("Key {} of register {} has already "
                                       "been used.".format(idx, register))
                    registers[register][idx] = qubits[qubit]
                else:
                    registers[register] = {idx: qubits[qubit]}
                    gate_list.append(("M", register))

            else:
                pieces = [x for x in re.split("[()]", command) if x]
                if len(pieces) == 1:
                    gatename, params = pieces[0], None
                    if gatename not in self.QASM_GATES:
                        raise ValueError("QASM command {} is not recognized."
                                         "".format(command))
                    if gatename in self.PARAMETRIZED_GATES:
                        raise ValueError("Missing parameters for QASM "
                                         "gate {}.".format(gatename))

                elif len(pieces) == 2:
                    gatename, params = pieces
                    if gatename not in self.PARAMETRIZED_GATES:
                        raise ValueError("Invalid QASM command {}."
                                         "".format(command))
                    params = params.replace(" ", "").split(",")
                    try:
                        for i, p in enumerate(params):
                            if 'pi' in p:
                                import math
                                from operator import mul
                                from functools import reduce
                                s = p.replace('pi', str(math.pi)).split('*')
                                p = reduce(mul, [float(j) for j in s], 1)
                            params[i] = float(p)
                    except ValueError:
                        raise ValueError("Invalid value {} for gate parameters."
                                         "".format(params))

                else:
                    raise ValueError("QASM command {} is not recognized."
                                     "".format(command))

                # Add gate to gate list
                qubit_list = []
                for qubit in read_args(args):
                    if qubit not in qubits:
                        raise ValueError("Qubit {} is not defined in QASM "
                                         "code.".format(qubit))
                    qubit_list.append(qubits[qubit])
                gate_list.append((self.QASM_GATES[gatename], list(qubit_list), params))
        return len(qubits), gate_list
