"""
Circuit object translation in different packages
"""

import logging
from copy import deepcopy
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union

import numpy as np

logger = logging.getLogger(__name__)


try:
    import qiskit.quantum_info as qi
    from qiskit import QuantumCircuit
    from qiskit.circuit import Parameter, ParameterExpression
    from qiskit.circuit.exceptions import CircuitError
    from qiskit.circuit.library import HamiltonianGate, UnitaryGate, XXPlusYYGate
    from qiskit.circuit.parametervector import ParameterVectorElement
    from qiskit.circuit.quantumcircuitdata import CircuitInstruction
except ImportError:
    logger.info(
        "Please first ``pip install -U qiskit`` to enable related functionality in translation module"
    )
    CircuitInstruction = Any
    QuantumCircuit = Any

try:
    import symengine
    import sympy
except ImportError:
    logger.info(
        "Please first ``pip install -U sympy symengine`` to enable `qiskit2tc` in translation module"
    )

try:
    import cirq
except ImportError:
    logger.info(
        "Please first ``pip install -U cirq`` to enable related functionality in translation module"
    )

from . import gates
from .circuit import Circuit
from .cons import backend
from .densitymatrix import DMCircuit2
from .interfaces.tensortrans import tensor_to_numpy

Tensor = Any


def get_qiskit_qasm(qc: Any) -> str:
    try:
        qasm_str = qc.qasm()  # type: ignore
    except AttributeError:  # qiskit 1.0
        from qiskit.qasm2 import dumps

        qasm_str = dumps(qc)
    return qasm_str  # type: ignore


def perm_matrix(n: int) -> Tensor:
    r"""
    Generate a permutation matrix P.
    Due to the different convention or qubits' order in qiskit and tensorcircuit,
    the unitary represented by the same circuit is different. They are related
    by this permutation matrix P:
    P @ U_qiskit @ P = U_tc

    :param n: # of qubits
    :type n: int
    :return: The permutation matrix P
    :rtype: Tensor
    """
    p_mat = np.zeros([2**n, 2**n])
    for i in range(2**n):
        bit = i
        revs_i = 0
        for j in range(n):
            if bit & 0b1:
                revs_i += 1 << (n - j - 1)
            bit = bit >> 1
        p_mat[i, revs_i] = 1
    return p_mat


def _get_float(parameters: Any, key: str, default: int = 0) -> float:
    return np.real(backend.numpy(gates.array_to_tensor(parameters.get(key, default)))).item()  # type: ignore


def _merge_extra_qir(
    qir: List[Dict[str, Any]], extra_qir: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
    nqir = []
    # caution on the same pos!
    inds: Dict[Union[int, float], List[Dict[str, Any]]] = {}
    for d in extra_qir:
        pos = d.get("pos", len(qir))
        if pos not in inds:
            inds[pos] = []
        inds[pos].append(d)
    for i, q in enumerate(qir):
        if i in inds:
            nqir += inds[i]
        nqir.append(q)
    for k in sorted(inds):
        if k >= len(qir):
            nqir += inds[k]
    return nqir


def qir2cirq(
    qir: List[Dict[str, Any]], n: int, extra_qir: Optional[List[Dict[str, Any]]] = None
) -> Any:
    r"""
    Generate a cirq circuit using the quantum intermediate
    representation (qir) in tensorcircuit.

    :Example:

    >>> c = tc.Circuit(2)
    >>> c.H(1)
    >>> c.X(1)
    >>> cisc = tc.translation.qir2cirq(c.to_qir(), 2)
    >>> print(cisc)
    1: ───H───X───

    :param qir: The quantum intermediate representation of a circuit.
    :type qir: List[Dict[str, Any]]
    :param n: # of qubits
    :type n: int
    :param extra_qir: The extra quantum IR of tc circuit including measure and reset on hardware,
        defaults to None
    :type extra_qir: Optional[List[Dict[str, Any]]]
    :return: qiskit cirq object
    :rtype: Any

    #TODO(@erertertet):
    add default theta to iswap gate
    add more cirq built-in gate instead of customized
    add unitary test with tolerance
    add support of cirq built-in ControlledGate for multiplecontroll
    support more element in qir, e.g. barrier, measure...
    """

    class CustomizedCirqGate(cirq.Gate):  # type: ignore
        def __init__(self, uMatrix: Any, name: str, nqubit: int):
            super(CustomizedCirqGate, self)
            self.uMatrix = uMatrix
            self.name = name
            self.nqubit = nqubit

        def _num_qubits_(self) -> int:
            return self.nqubit

        def _unitary_(self) -> Any:
            return self.uMatrix

        def _circuit_diagram_info_(
            self, *args: Optional[cirq.CircuitDiagramInfoArgs]
        ) -> List[str]:
            return [self.name] * self.nqubit

    if extra_qir is not None and len(extra_qir) > 0:
        qir = _merge_extra_qir(qir, extra_qir)
    qbits = cirq.LineQubit.range(n)
    cmd = []
    for gate_info in qir:
        if gate_info.get("is_channel", False):
            continue
        index = [qbits[i] for i in gate_info["index"]]
        gate_name = str(gate_info["gatef"])
        if "parameters" in gate_info:
            parameters = gate_info["parameters"]
        if gate_name in [
            "h",
            "i",
            "x",
            "y",
            "z",
            "s",
            "t",
            "fredkin",
            "toffoli",
            "cnot",
            "swap",
        ]:
            cmd.append(getattr(cirq, gate_name.upper())(*index))
        elif gate_name in ["rx", "ry", "rz"]:
            cmd.append(
                getattr(cirq, gate_name)(_get_float(parameters, "theta")).on(*index)
            )
        elif gate_name == "iswap":
            cmd.append(
                cirq.ISwapPowGate(
                    exponent=_get_float(parameters, "theta", default=1)
                ).on(*index)
            )
        elif gate_name in ["mpo", "multicontrol"]:
            gatem = np.reshape(
                backend.numpy(gate_info["gatef"](**parameters).eval_matrix()),
                [2 ** len(index), 2 ** len(index)],
            )
            ci_name = gate_info["name"]
            cgate = CustomizedCirqGate(gatem, ci_name, len(index))
            cmd.append(cgate.on(*index))
        else:
            # Add Customized Gate if there is no match
            gatem = np.reshape(
                gate_info["gate"].tensor,
                [2 ** len(index), 2 ** len(index)],
            )
            # Note: unitary test is not working for some of the generated matrix,
            # probably add tolerance unitary test later
            cgate = CustomizedCirqGate(gatem, gate_name, len(index))
            cmd.append(cgate.on(*index))
    cirq_circuit = cirq.Circuit(*cmd)
    return cirq_circuit


def qir2qiskit(
    qir: List[Dict[str, Any]],
    n: int,
    extra_qir: Optional[List[Dict[str, Any]]] = None,
    initialization: Optional[Tensor] = None,
) -> Any:
    r"""
    Generate a qiskit quantum circuit using the quantum intermediate
    representation (qir) in tensorcircuit.

    :Example:

    >>> c = tc.Circuit(2)
    >>> c.H(1)
    >>> c.X(1)
    >>> qisc = tc.translation.qir2qiskit(c.to_qir(), 2)
    >>> qisc.data
    [(Instruction(name='h', num_qubits=1, num_clbits=0, params=[]), [Qubit(QuantumRegister(2, 'q'), 1)], []),
     (Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), [Qubit(QuantumRegister(2, 'q'), 1)], [])]

    :param qir: The quantum intermediate representation of a circuit.
    :type qir: List[Dict[str, Any]]
    :param n: # of qubits
    :type n: int
    :param extra_qir: The extra quantum IR of tc circuit including measure and reset on hardware,
        defaults to None
    :type extra_qir: Optional[List[Dict[str, Any]]]
    :param initialization: Circuit initial state in qiskit format
    :type initialization: Optional[Tensor]
    :return: qiskit QuantumCircuit object
    :rtype: Any
    """
    if extra_qir is not None and len(extra_qir) > 0:
        qir = _merge_extra_qir(qir, extra_qir)
        qiskit_circ = QuantumCircuit(n, n)
    else:
        qiskit_circ = QuantumCircuit(n)
    if initialization is not None:
        qiskit_circ.initialize(initialization)
    measure_cbit = 0
    for gate_info in qir:
        if gate_info.get("is_channel", False):
            continue
        index = gate_info["index"]
        gate_name = str(gate_info["gatef"])
        qis_name = gate_name
        if gate_name in ["exp", "exp1", "any", "multicontrol"]:
            qis_name = gate_info["name"]
        parameters = {}
        if "parameters" in gate_info:
            parameters = gate_info["parameters"]
        if gate_name in [
            "h",
            "x",
            "y",
            "z",
            "i",
            "s",
            "t",
            "swap",
            "cnot",
            "toffoli",
            "fredkin",
            "cnot",
            "cx",
            "cy",
            "cz",
        ]:
            getattr(qiskit_circ, gate_name)(*index)
        elif gate_name in ["sd", "td"]:
            getattr(qiskit_circ, gate_name + "g")(*index)
        elif gate_name in ["ox", "oy", "oz"]:
            getattr(qiskit_circ, "c" + gate_name[1:])(*index, ctrl_state=0)
        elif gate_name == "u":
            getattr(qiskit_circ, "u")(
                _get_float(parameters, "theta"),
                _get_float(parameters, "phi"),
                _get_float(parameters, "lbd"),
                *index,
            )
        elif gate_name == "cu":
            getattr(qiskit_circ, "cu")(
                _get_float(parameters, "theta"),
                _get_float(parameters, "phi"),
                _get_float(parameters, "lbd"),
                0,  # gamma
                *index,
            )
        elif gate_name == "iswap":
            qiskit_circ.append(
                XXPlusYYGate(np.pi * _get_float(parameters, "theta", 1), np.pi),
                index,
            )
        elif gate_name == "wroot":
            getattr(qiskit_circ, "u")(np.pi / 2, -np.pi / 4, np.pi / 4, *index)
        elif gate_name in ["rx", "ry", "rz", "crx", "cry", "crz", "rxx", "ryy", "rzz"]:
            getattr(qiskit_circ, gate_name)(_get_float(parameters, "theta"), *index)
        elif gate_name in ["phase", "cphase"]:
            getattr(qiskit_circ, gate_name[:-4])(
                _get_float(parameters, "theta"), *index
            )
        elif gate_name in ["orx", "ory", "orz"]:
            getattr(qiskit_circ, "c" + gate_name[1:])(
                _get_float(parameters, "theta"), *index, ctrl_state=0
            )
        elif gate_name in ["exp", "exp1"]:
            unitary = backend.numpy(backend.convert_to_tensor(parameters["unitary"]))
            theta = backend.numpy(backend.convert_to_tensor(parameters["theta"]))
            theta = np.array(np.real(theta)).astype(np.float64)
            # cast theta to real, since qiskit only support unitary.
            # Error can be presented if theta is actually complex in this procedure.
            exp_op = qi.Operator(unitary)
            index_reversed = [x for x in index[::-1]]
            gate = HamiltonianGate(data=exp_op, time=theta, label=qis_name)
            qiskit_circ.append(gate, index_reversed)
        elif gate_name == "multicontrol":
            unitary = backend.numpy(backend.convert_to_tensor(parameters["unitary"]))
            k = int(np.log(unitary.shape[-1]) / np.log(2) + 1e-7)
            ctrl_str = "".join(map(str, parameters["ctrl"]))[::-1]
            unitary = perm_matrix(k) @ unitary @ perm_matrix(k)
            gate = UnitaryGate(unitary, label=qis_name).control(
                len(ctrl_str), ctrl_state=ctrl_str
            )
            qiskit_circ.append(gate, gate_info["index"])
        elif gate_name == "mpo":
            qop = qi.Operator(
                np.reshape(
                    backend.numpy(gate_info["gatef"](**parameters).eval_matrix()),
                    [2 ** len(index), 2 ** len(index)],
                )
            )
            qiskit_circ.unitary(qop, index[::-1], label=qis_name)
        elif gate_name == "measure":
            qiskit_circ.measure(index[0], measure_cbit)
            measure_cbit += 1
        elif gate_name == "reset":
            qiskit_circ.reset(index[0])
        elif gate_name == "barrier":
            qiskit_circ.barrier(*index)
        else:  # r cr any gate
            gatem = np.reshape(
                backend.numpy(gate_info["gatef"](**parameters).tensor),
                [2 ** len(index), 2 ** len(index)],
            )
            qop = qi.Operator(gatem)
            try:
                qiskit_circ.unitary(qop, index[::-1], label=qis_name)
            except (CircuitError, ValueError) as _:
                logger.warning(
                    "omit non unitary gate in tensorcircuit when transforming to qiskit: %s"
                    % gate_name
                )
                qiskit_circ.unitary(
                    np.eye(2 ** len(index)), index[::-1], label=qis_name
                )

    return qiskit_circ


def _translate_qiskit_params(
    gate_info: CircuitInstruction, binding_params: Any
) -> List[float]:
    parameters = []
    for p in gate_info.operation.params:
        if isinstance(p, ParameterVectorElement):
            parameters.append(binding_params[p.index])
        elif isinstance(p, Parameter):
            parameters.append(binding_params[p])
        elif isinstance(p, ParameterExpression):
            if len(p.parameters) == 0:
                parameters.append(float(p))
                continue

            # note "sym" != "sim"
            expr = p.sympify().simplify()
            if isinstance(expr, symengine.Expr):  # qiskit uses symengine if available
                expr = expr._sympy_()  # sympy.Expr

            if expr.is_algebraic_expr():  # numpy ufuncs are not used
                lambdify_module_name = "numpy"
            else:
                if backend.name == "pytorch":
                    lambdify_module_name = "math"
                else:
                    lambdify_module_name = backend.name

            for free_symbol in expr.free_symbols:
                # replace names: theta[0] -> theta_0
                # ParameterVector creates symbols with brackets like theta[0]
                # but sympy.lambdify does not allow brackets in symbol names
                free_symbol.name = free_symbol.name.replace("[", "_").replace("]", "")

            parameter_list = list(p.parameters)
            sympy_symbols = [param._symbol_expr for param in parameter_list]
            # replace names again: theta[0] -> theta_0
            sympy_symbols = [
                sympy.Symbol(str(symbol).replace("[", "_").replace("]", ""))
                for symbol in sympy_symbols
            ]
            lam_f = sympy.lambdify(sympy_symbols, expr, modules=lambdify_module_name)
            parameters.append(
                lam_f(*[binding_params[param.index] for param in parameter_list])
            )
        else:
            # numbers, arrays, etc.
            parameters.append(p)
    return parameters


def ctrl_str2ctrl_state(ctrl_str: str, nctrl: int) -> List[int]:
    ctrl_state_bin = int(ctrl_str)
    return [0x1 & (ctrl_state_bin >> (i)) for i in range(nctrl)]


def qiskit2tc(
    qc: QuantumCircuit,
    n: int,
    inputs: Optional[List[float]] = None,
    is_dm: bool = False,
    circuit_constructor: Any = None,
    circuit_params: Optional[Dict[str, Any]] = None,
    binding_params: Optional[Union[Sequence[float], Dict[Any, float]]] = None,
) -> Any:
    r"""
    Generate a tensorcircuit circuit from the qiskit circuit.

    :Example:

    >>> qisc = QuantumCircuit(2)
    >>> qisc.h(0)
    >>> qisc.x(1)
    >>> qc = tc.translation.qiskit2tc(qisc, 2)
    >>> qc.to_qir()[0]['gatef']

    :param qc: A quantum circuit in qiskit.
    :type qc: QuantumCircuit
    :param n: # of qubits
    :type n: int
    :param inputs: Input state of the circuit. Default is None.
    :type inputs: Optional[List[float]]
    :param circuit_constructor: ``Circuit``, ``DMCircuit`` or ``MPSCircuit``
    :type circuit_contructor: Any
    :param circuit_params: kwargs given in Circuit.__init__ construction function, default to None.
    :type circuit_params: Optional[Dict[str, Any]]
    :param binding_params: (variational) parameters for the circuit.
        Could be either a sequence or dictionary depending on the type of parameters in the Qiskit circuit.
        For ``ParameterVectorElement`` use sequence. For ``Parameter`` use dictionary.
    :type binding_params: Optional[Union[Sequence[float], Dict[Any, float]]]
    :return: A quantum circuit in tensorcircuit
    :rtype: Any
    """
    if circuit_constructor is not None:
        Circ = circuit_constructor
    elif is_dm:
        Circ = DMCircuit2
    else:
        Circ = Circuit
    if circuit_params is None:
        circuit_params = {}
    if "nqubits" not in circuit_params:
        circuit_params["nqubits"] = n
    if (
        len(qc.data) > 0
        and qc.data[0][0].name == "initialize"
        and "inputs" not in circuit_params
    ):
        circuit_params["inputs"] = perm_matrix(n) @ np.array(qc.data[0][0].params)
    if inputs is not None:
        circuit_params["inputs"] = inputs

    tc_circuit: Any = Circ(**circuit_params)
    for gate_info in qc.data:
        idx = [qc.find_bit(qb).index for qb in gate_info.qubits]
        gate_name = gate_info[0].name
        parameters = _translate_qiskit_params(gate_info, binding_params)
        if gate_name in [
            "h",
            "x",
            "y",
            "z",
            "i",
            "s",
            "t",
            "swap",
            "iswap",
            "cnot",
            "toffoli",
            "fredkin",
            "cx",
            "cy",
            "cz",
        ]:
            getattr(tc_circuit, gate_name)(*idx)
        elif gate_name in ["sdg", "tdg"]:
            getattr(tc_circuit, gate_name[:-1])(*idx)
        elif gate_name in ["cx_o0", "cy_o0", "cz_o0"]:
            getattr(tc_circuit, "o" + gate_name[1])(*idx)
        elif gate_name in ["rx", "ry", "rz", "crx", "cry", "crz", "rxx", "ryy", "rzz"]:
            getattr(tc_circuit, gate_name)(*idx, theta=parameters)
        elif gate_name in ["crx_o0", "cry_o0", "crz_o0"]:
            getattr(tc_circuit, "o" + gate_name[1:-3])(*idx, theta=parameters)
        elif gate_name in ["r"]:
            getattr(tc_circuit, "u")(
                *idx,
                theta=parameters[0],
                phi=parameters[1] - np.pi / 2,
                lbd=-parameters[1] + np.pi / 2,
            )
        elif gate_name in ["u3", "u"]:
            getattr(tc_circuit, "u")(
                *idx, theta=parameters[0], phi=parameters[1], lbd=parameters[2]
            )
        elif gate_name == "hamiltonian":
            tc_circuit.exp(*idx, theta=parameters[-1], unitary=parameters[0])
        elif gate_name == "cswap":
            tc_circuit.fredkin(*idx)
        elif gate_name[:3] == "ccx":
            if gate_name[3:] == "":
                tc_circuit.toffoli(*idx)
            else:
                ctrl_state = ctrl_str2ctrl_state(gate_name[5:], len(idx) - 1)
                tc_circuit.multicontrol(
                    *idx, ctrl=ctrl_state, unitary=gates._x_matrix, name="x"
                )
        elif gate_name[:3] == "mcx":
            if gate_name[3:] == "":
                tc_circuit.multicontrol(
                    *idx, ctrl=[1] * (len(idx) - 1), unitary=gates._x_matrix, name="x"
                )
            else:
                ctrl_state = ctrl_str2ctrl_state(gate_name[5:], len(idx) - 1)
                tc_circuit.multicontrol(
                    *idx, ctrl=ctrl_state, unitary=gates._x_matrix, name="x"
                )
        elif gate_name[0] == "c" and gate_name[:7] != "circuit" and gate_name != "cu":
            # qiskit cu bug, see https://github.com/tencent-quantum-lab/tensorcircuit/issues/199
            for i in range(1, len(gate_name)):
                if (gate_name[-i] == "o") & (gate_name[-i - 1] == "_"):
                    break
            if i == len(gate_name) - 1:
                base_gate = gate_info[0].base_gate
                ctrl_state = [1] * base_gate.num_qubits
                idx = idx[: -base_gate.num_qubits] + idx[-base_gate.num_qubits :][::-1]
                tc_circuit.multicontrol(
                    *idx, ctrl=ctrl_state, unitary=base_gate.to_matrix()
                )
            else:
                base_gate = gate_info[0].base_gate
                ctrl_state = ctrl_str2ctrl_state(
                    gate_name[-i + 1 :], len(idx) - base_gate.num_qubits
                )
                idx = idx[: -base_gate.num_qubits] + idx[-base_gate.num_qubits :][::-1]
                tc_circuit.multicontrol(
                    *idx, ctrl=ctrl_state, unitary=base_gate.to_matrix()
                )
        elif gate_name == "measure":
            tc_circuit.measure_instruction(*idx)
            # logger.warning(
            #     "qiskit to tc translation currently doesn't support measure instruction, just skipping"
            # )
        elif gate_name == "reset":
            tc_circuit.reset_instruction(*idx)
            # logger.warning(
            #     "qiskit to tc translation currently doesn't support reset instruction, just skipping"
            # )
        elif gate_name == "barrier":
            tc_circuit.barrier_instruction(*idx)
        elif gate_name == "initialize":
            # already taken care of when initializing
            continue
        elif not hasattr(gate_info[0], "__array__"):
            # an instruction containing a lot of gates.
            # the condition is based on
            # https://github.com/Qiskit/qiskit-terra/blob/2f5944d60140ceb6e30d5b891e8ffec735247ce9/qiskit/circuit/gate.py#L43
            qiskit_circuit = gate_info[0].definition
            if qiskit_circuit is None:
                logger.warning(
                    f"qiskit to tc translation doesn't support {gate_name} instruction, skipping"
                )
                continue
            c = Circuit.from_qiskit(qiskit_circuit, binding_params=binding_params)
            tc_circuit.append(c, idx)
        else:  # unitary gate
            idx_inverse = (x for x in idx[::-1])
            tc_circuit.any(*idx_inverse, unitary=gate_info[0].to_matrix())
    return tc_circuit


def tensor_to_json(a: Any) -> Any:
    if (
        isinstance(a, float)
        or isinstance(a, int)
        or isinstance(a, complex)
        or isinstance(a, list)
        or isinstance(a, tuple)
    ):
        a = np.array(a)
    a = tensor_to_numpy(a)

    ar = np.real(a)
    if np.iscomplexobj(a):
        ai = np.imag(a)
        return [ar.tolist(), ai.tolist()]
    else:
        return [ar.tolist()]


def json_to_tensor(a: Any) -> Any:
    ar = np.array(a[0])
    if len(a) == 1:
        return ar
    else:
        assert len(a) == 2
        ai = np.array(a[1])
        return ar + 1.0j * ai


def qir2json(
    qir: List[Dict[str, Any]], simplified: bool = False
) -> List[Dict[str, Any]]:
    """
    transform qir to json compatible list of dict where array is replaced by real and imaginary list

    :param qir: _description_
    :type qir: List[Dict[str, Any]]
    :param simplified: If False, keep all info for each gate, defaults to be False.
        If True, suitable for IO since less information is required
    :type simplified: bool
    :return: _description_
    :rtype: List[Dict[str, Any]]
    """
    logger.warning(
        "experimental feature subject to fast protocol and implementation change, try on your own risk"
    )
    qir = deepcopy(qir)
    tcqasm = []
    for r in qir:
        if r.get("is_channel", False):
            continue
        if r["mpo"] is True:
            nm = backend.reshapem(r["gate"].eval())
        else:
            nm = backend.reshapem(r["gate"].tensor)
        nmr, nmi = tensor_to_json(nm)
        if backend.shape_tuple(nm)[0] == backend.shape_tuple(nm)[1] == 2:
            uparams = [float(p) for p in gates.get_u_parameter(backend.numpy(nm))]
        else:
            uparams = []
        params = r.get("parameters", {})
        for k, v in params.items():
            params[k] = tensor_to_json(v)

        ditem = {
            "name": r["gatef"].n,
            "qubits": list(r["index"]),
        }
        unsupport_list = ["any", "unitary", "mpo", "exp", "exp1", "r", "cr"]
        if not simplified:
            ditem.update(
                {
                    "matrix": [nmr, nmi],
                    "uparams": uparams,
                    "parameters": params,
                    "mpo": r["mpo"],
                }
            )
        else:  # update only when necessary: simplified=True
            if ditem["name"] in unsupport_list and uparams:
                ditem.update({"uparams": uparams})
            elif ditem["name"] in unsupport_list:
                ditem.update({"matrix": [nmr, nmi]})
            if params:
                ditem.update({"parameters": params})
            if r["mpo"]:
                ditem.update({"mpo": r["mpo"]})
        tcqasm.append(ditem)
    return tcqasm


def json2qir(tcqasm: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    logger.warning(
        "experimental feature subject to fast protocol and implementation change, try on your own risk"
    )
    tcqasm = deepcopy(tcqasm)
    qir = []
    for d in tcqasm:
        param = d.get("parameters", {})
        for k, v in param.items():
            # tree_map doesn't work here since the end leaves are list
            param[k] = json_to_tensor(v)
        if "matrix" in d:
            gatem = json_to_tensor(d["matrix"])
        else:
            gatem = getattr(gates, d["name"])(**param)
        qir.append(
            {
                "gate": gatem,
                "index": tuple(d["qubits"]),
                "mpo": d.get("mpo", False),
                "split": {},
                "parameters": param,
                "gatef": getattr(gates, d["name"]),
                "name": d["name"],
            }
        )
    return qir


def eqasm2tc(
    eqasm: str, nqubits: Optional[int] = None, headers: Tuple[int, int] = (6, 1)
) -> Circuit:
    """
    Translation qexe/eqasm instruction to tensorcircuit Circuit object

    :param eqasm: _description_
    :type eqasm: str
    :param nqubits: _description_, defaults to None
    :type nqubits: Optional[int], optional
    :param headers: lines of ignored code at the head and the tail, defaults to (6, 1)
    :type headers: Tuple[int, int], optional
    :return: _description_
    :rtype: Circuit
    """
    eqasm_list = eqasm.split("\n")
    if nqubits is None:
        nqubits = len(eqasm_list[2].split(","))
    eqasm_list = eqasm_list[headers[0] : -headers[1]]
    c = Circuit(nqubits)
    # measure z not considered
    for inst in eqasm_list:
        if inst.startswith("bs"):
            inst_list = inst.split(" ")
            if inst_list[2].startswith("RZ"):
                exponent = int(inst_list[2][3:])
                index = (int(inst_list[3][1:]),)
                c.rz(*index, theta=2 * np.pi / 2**exponent)  # type: ignore
            elif inst_list[2] == "Z/2":
                c.rz(*index, theta=-np.pi / 2)  # type: ignore
            elif inst_list[2] == "-Z/2":
                c.rz(*index, theta=np.pi / 2)  # type: ignore
            # TODO(@refraction-ray): Z/2 convention to be double checked
            else:
                gate_name = inst_list[2].lower()
                if len(inst_list) == 4:
                    index = (int(inst_list[3][1:]),)
                elif len(inst_list) == 5:
                    index = (int(inst_list[3][2:-1]), int(inst_list[4][1:-1]))  # type: ignore
                else:
                    raise ValueError("Unknown format for eqasm: %s" % str(inst_list))
                getattr(c, gate_name)(*index)

    return c


def qiskit_from_qasm_str_ordered_measure(qasm_str: str) -> Any:
    """
    qiskit ``from_qasm_str`` method cannot keep the order of measure as the qasm file,
    we provide this alternative function in case the order of measure instruction matters

    :param qasm_str: open qasm str
    :type qasm_str: str
    :return: ``qiskit.circuit.QuantumCircuit``
    :rtype: Any
    """
    measure_sequence = []
    qasm_instruction = []
    for line in qasm_str.split("\n"):
        if line.startswith("measure"):
            index = int(line.split(" ")[1][2:-1])
            cindex = int(line.split(" ")[3].strip(";").split("[")[1][:-1])
            # sometimes we have qasm as "measure q[3] -> meas[0];"

            measure_sequence.append((index, cindex))
        else:
            qasm_instruction.append(line)

    qc = QuantumCircuit.from_qasm_str("\n".join(qasm_instruction))
    for qid, cid in measure_sequence:
        qc.measure(qid, cid)
    return qc


def stim2tc(
    stim_circuit: Any,
    n: Optional[int] = None,
    is_dm: bool = False,
    circuit_constructor: Any = None,
    circuit_params: Optional[Dict[str, Any]] = None,
) -> Any:
    """
    Generate a tensorcircuit circuit from the stim circuit.

    :param stim_circuit: A quantum circuit in stim
    :type stim_circuit: stim.Circuit
    :param n: # of qubits, defaults to None
    :type n: Optional[int], optional
    :param is_dm: whether to use DMCircuit, defaults to False
    :type is_dm: bool, optional
    :param circuit_constructor: _description_, defaults to None
    :type circuit_constructor: Any, optional
    :param circuit_params: _description_, defaults to None
    :type circuit_params: Optional[Dict[str, Any]], optional
    :return: _description|
    :rtype: Any
    """
    if circuit_constructor is not None:
        Circ = circuit_constructor
    elif is_dm:
        Circ = DMCircuit2
    else:
        Circ = Circuit

    if n is None:
        n = stim_circuit.num_qubits

    if circuit_params is None:
        circuit_params = {}
    if "nqubits" not in circuit_params:
        circuit_params["nqubits"] = n

    c = Circ(**circuit_params)

    for instruction in stim_circuit.flattened():
        name = instruction.name
        targets = [t.value for t in instruction.targets_copy()]
        args = instruction.gate_args_copy()

        # Metadata/Skip
        if name in [
            "QUBIT_COORDS",
            "SHIFT_COORDS",
            "I_ERROR",
            "TICK",
            "OBSERVABLE_INCLUDE",
        ]:
            continue
        if name == "DETECTOR":
            c.detector_instruction(targets, coords=args if args else None)
            continue

        # Standard Gates
        if name in ["I", "X", "Y", "Z", "H", "S", "T"]:
            for q in targets:
                getattr(c, name.lower())(q)
            continue
        if name in ["S_DAG", "T_DAG"]:
            for q in targets:
                getattr(c, name.lower().replace("_", ""))(q)
            continue
        if name in ["CX", "CNOT", "CY", "CZ", "SWAP"]:
            f = "cx" if name == "CNOT" else name.lower()
            for i in range(0, len(targets), 2):
                getattr(c, f)(targets[i], targets[i + 1])
            continue
        if name == "ISWAP":
            for i in range(0, len(targets), 2):
                c.iswap(targets[i], targets[i + 1])
            continue
        if name == "ISWAP_DAG":
            for i in range(0, len(targets), 2):
                c.iswap(targets[i], targets[i + 1], theta=-1.0)
            continue

        # SQRT Gates (X, Y, Z, XX, YY, ZZ)
        if name == "SQRT_X":
            for q in targets:
                c.rx(q, theta=np.pi / 2)
            continue
        if name == "SQRT_X_DAG":
            for q in targets:
                c.rx(q, theta=-np.pi / 2)
            continue
        if name == "SQRT_Y":
            for q in targets:
                c.ry(q, theta=np.pi / 2)
            continue
        if name == "SQRT_Y_DAG":
            for q in targets:
                c.ry(q, theta=-np.pi / 2)
            continue
        # SQRT_Z is S, SQRT_Z_DAG is S_DAG
        if name == "SQRT_XX":
            for i in range(0, len(targets), 2):
                c.rxx(targets[i], targets[i + 1], theta=np.pi / 2)
            continue
        if name == "SQRT_YY":
            for i in range(0, len(targets), 2):
                c.ryy(targets[i], targets[i + 1], theta=np.pi / 2)
            continue
        if name == "SQRT_ZZ":
            for i in range(0, len(targets), 2):
                c.rzz(targets[i], targets[i + 1], theta=np.pi / 2)
            continue

        # Measurement & Reset (Z-basis)
        if name in ["M", "MEASURE", "MZ"]:
            for q in targets:
                c.measure_instruction(q)
            continue
        if name in ["R", "RZ"]:
            for q in targets:
                c.reset_instruction(q)
            continue
        if name in ["MR", "MRZ"]:
            for q in targets:
                c.measure_instruction(q)
                c.reset_instruction(q)
            continue

        # X-basis M, R, MR
        if name == "MX":
            for q in targets:
                c.h(q)
                c.measure_instruction(q)
                c.h(q)
            continue
        if name == "RX":
            for q in targets:
                c.h(q)
                c.reset_instruction(q)
                c.h(q)
            continue
        if name == "MRX":
            for q in targets:
                c.h(q)
                c.measure_instruction(q)
                c.reset_instruction(q)
                c.h(q)
            continue

        # Y-basis M, R, MR
        if name == "MY":
            for q in targets:
                c.sdg(q)
                c.h(q)
                c.measure_instruction(q)
                c.h(q)
                c.s(q)
            continue
        if name == "RY":
            for q in targets:
                c.sdg(q)
                c.h(q)
                c.reset_instruction(q)
                c.h(q)
                c.s(q)
            continue
        if name == "MRY":
            for q in targets:
                c.sdg(q)
                c.h(q)
                c.measure_instruction(q)
                c.reset_instruction(q)
                c.h(q)
                c.s(q)
            continue

        # Noise Channels
        if name == "DEPOLARIZE1":
            p = args[0]
            for q in targets:
                c.depolarizing(q, px=p / 3.0, py=p / 3.0, pz=p / 3.0)
            continue
        if name == "DEPOLARIZE2":
            p = args[0]
            for i in range(0, len(targets), 2):
                c.isotropicdepolarizing(targets[i], targets[i + 1], p=p, num_qubits=2)
            continue
        if name == "X_ERROR":
            for q in targets:
                c.depolarizing(q, px=args[0], py=0.0, pz=0.0)
            continue
        if name == "Y_ERROR":
            for q in targets:
                c.depolarizing(q, px=0.0, py=args[0], pz=0.0)
            continue
        if name == "Z_ERROR":
            for q in targets:
                c.depolarizing(q, px=0.0, py=0.0, pz=args[0])
            continue
        if name == "PAULI_CHANNEL_1":
            for q in targets:
                c.depolarizing(q, px=args[0], py=args[1], pz=args[2])
            continue

        logger.warning(
            f"Stim instruction {name} not supported in translation, skipping"
        )

    return c


def cirq2tc(
    qc: Any,
    n: Optional[int] = None,
    inputs: Optional[List[float]] = None,
    is_dm: bool = False,
    circuit_constructor: Any = None,
    circuit_params: Optional[Dict[str, Any]] = None,
) -> Any:
    """
    Generate a tensorcircuit circuit from the cirq circuit.

    :param qc: A quantum circuit in cirq
    :type qc: cirq.Circuit
    :param n: # of qubits, defaults to None
    :type n: Optional[int], optional
    :param inputs: Input state of the circuit, defaults to None
    :type inputs: Optional[List[float]], optional
    :param is_dm: whether to use DMCircuit, defaults to False
    :type is_dm: bool, optional
    :param circuit_constructor: _description_, defaults to None
    :type circuit_constructor: Any, optional
    :param circuit_params: _description_, defaults to None
    :type circuit_params: Optional[Dict[str, Any]], optional
    :return: _description_
    :rtype: Any
    """

    if circuit_constructor is not None:
        Circ = circuit_constructor
    elif is_dm:
        Circ = DMCircuit2
    else:
        Circ = Circuit

    if n is None:
        n = len(sorted(qc.all_qubits()))

    if circuit_params is None:
        circuit_params = {}
    if "nqubits" not in circuit_params:
        circuit_params["nqubits"] = n

    if inputs is not None:
        circuit_params["inputs"] = inputs

    tc_circuit: Any = Circ(**circuit_params)

    qubits = sorted(qc.all_qubits())
    qubit_map = {q: i for i, q in enumerate(qubits)}

    for op in qc.all_operations():
        if isinstance(op.gate, cirq.MeasurementGate):
            for q in op.qubits:
                tc_circuit.measure_instruction(qubit_map[q])
            continue

        index = [qubit_map[q] for q in op.qubits]
        gate = op.gate
        # gate_name = str(gate)

        if isinstance(gate, cirq.IdentityGate):
            continue

        # Standard Gates (and PowGates with exp=1)
        if isinstance(gate, cirq.HPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.h(*index)
        elif isinstance(gate, cirq.XPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.x(*index)
        elif isinstance(gate, cirq.YPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.y(*index)
        elif isinstance(gate, cirq.ZPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.z(*index)
        elif isinstance(gate, cirq.SwapPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.swap(*index)
        elif isinstance(gate, cirq.ISwapPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.iswap(*index)
        elif isinstance(gate, cirq.CNotPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.cnot(*index)
        elif isinstance(gate, cirq.CZPowGate) and np.isclose(gate.exponent, 1):
            tc_circuit.cz(*index)

        # Variable Gates (X, Y, Z, ISWAP family)
        elif isinstance(gate, cirq.XPowGate):
            tc_circuit.rx(*index, theta=gate.exponent * np.pi)
        elif isinstance(gate, cirq.YPowGate):
            tc_circuit.ry(*index, theta=gate.exponent * np.pi)
        elif isinstance(gate, cirq.ZPowGate):
            if np.isclose(gate.exponent, 0.5):
                tc_circuit.s(*index)
            elif np.isclose(gate.exponent, 0.25):
                tc_circuit.t(*index)
            else:
                tc_circuit.rz(*index, theta=gate.exponent * np.pi)
        elif isinstance(gate, cirq.ISwapPowGate):
            tc_circuit.iswap(*index, theta=gate.exponent)

        elif isinstance(gate, cirq.FSimGate):
            tc_circuit.iswap(*index, theta=-gate.theta * 2 / np.pi)
            tc_circuit.cphase(*index, theta=-gate.phi)
        elif isinstance(gate, cirq.PhasedXPowGate):
            # Rz(phase_exponent * pi) Rx(exponent * pi) Rz(-phase_exponent * pi)
            tc_circuit.rz(*index, theta=-gate.phase_exponent * np.pi)
            tc_circuit.rx(*index, theta=gate.exponent * np.pi)
            tc_circuit.rz(*index, theta=gate.phase_exponent * np.pi)
        elif isinstance(gate, cirq.MatrixGate):
            # Arbitrary unitary
            m = cirq.unitary(gate)
            tc_circuit.any(*index, unitary=m)
        else:
            # try to get unitary
            try:
                m = cirq.unitary(gate)
                tc_circuit.any(*index, unitary=m)
            except (TypeError, ValueError):
                logger.warning(
                    f"Cirq gate {gate} not supported in translation, skipping"
                )

    return tc_circuit
