"""
/*********************************************************************
* Copyright (c) 2023 the Qrisp Authors
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
"""



import numpy as np
from qrisp.circuit.operation import PTControlledOperation, ControlledOperation
from qrisp.circuit.instruction import Instruction
from qrisp.circuit.operation import Operation
from qrisp.circuit.quantum_circuit import fast_append

#This function dissolves any Operation objects that have a definition circuit such that the result only consists of elementary gates
def transpile(qc, transpilation_level = np.inf, transpile_predicate = None, **kwargs):
    
    from qrisp.circuit import Operation, QuantumCircuit, Qubit, Clbit

    with fast_append():
        from qrisp.circuit import Qubit, Clbit
        transpiled_qc = QuantumCircuit()
        
        # [transpiled_qc.add_qubit(Qubit(qb.identifier)) for qb in qc.qubits]
        # [transpiled_qc.add_clbit(Clbit(cb.identifier)) for cb in qc.clbits]
        
        for qb in qc.qubits:
            if isinstance(qb, Qubit):
                transpiled_qc.add_qubit(qb)
            else:
                transpiled_qc.add_qubit(Qubit(qb.identifier))
                
        for cb in qc.clbits:
            if isinstance(qb, Qubit):
                transpiled_qc.add_clbit(cb)
            else:
                transpiled_qc.add_clbit(Clbit(cb.identifier))
    
        translation_dic = {qc.qubits[i].identifier : transpiled_qc.qubits[i] for i in range(len(qc.qubits))}
        translation_dic.update({qc.clbits[i].identifier : transpiled_qc.clbits[i] for i in range(len(qc.clbits))})
        
        if transpile_predicate is None:
            transpile_predicate_ = lambda i, op : i < transpilation_level
        else:
            transpile_predicate_ = lambda i, op : i < transpilation_level and transpile_predicate(op)
        
        transpile_inner(qc, transpiled_qc, translation_dic, transpile_predicate_)
        
        QuantumCircuit.fast_append = False
        
        if not kwargs or not hasattr(qc, "to_qiskit"):
            return transpiled_qc
        else:
            from qrisp import QuantumCircuit
            qiskit_qc = transpiled_qc.to_qiskit()
            
            from qiskit import transpile as qiskit_transpile
            
            transpiled_qiskit_qc = qiskit_transpile(qiskit_qc, **kwargs)
            
            qrisp_qc = QuantumCircuit.from_qiskit(transpiled_qiskit_qc)
            
            return qrisp_qc


def transpile_inner(transpilation_qc, target_qc, translation_dic, transpile_predicate, transpilation_level = 0):
    
    for i in range(len(transpilation_qc.data)):
        instr = transpilation_qc.data[i]
        if instr.op.definition:
            
            if transpile_predicate(transpilation_level, instr.op):
                
                definition = instr.op.definition
                
                new_translation_dic = {definition.qubits[j].identifier : translation_dic[instr.qubits[j].identifier] for j in range(len(instr.qubits))}
                
                new_translation_dic.update({definition.clbits[j].identifier : translation_dic[instr.clbits[j].identifier] for j in range(len(instr.clbits))})
                
                transpile_inner(instr.op.definition, 
                                target_qc, 
                                new_translation_dic, 
                                transpile_predicate, 
                                transpilation_level + 1)
                continue
        
        if not isinstance(instr.op, Operation):
            op = Operation(init_op = instr.op)
        else:
            op = instr.op
        
        target_qc.data.append(Instruction(op, [translation_dic[qb.identifier] for qb in instr.qubits], [translation_dic[cb.identifier] for cb in instr.clbits]))
        # target_qc.append()
        
    


def extend(qc_0, qc_1, translation_dic = "id"):
    
    if translation_dic == "id":
        
        translation_dic = {}
        for qb in qc_1.qubits:
            translation_dic[qb] = qb
            if not qb in qc_0.qubits:
                qc_0.add_qubit(qb)
            
        for cb in qc_1.clbits:
            translation_dic[qb] = qb
            if not cb in qc_0.clbits:
                qc_0.add_clbit(cb)
            

    #Copy in order to prevent modification
    translation_dic = dict(translation_dic)

    for key in list(translation_dic.keys()):
        
        if not isinstance(key, str):
            translation_dic[key.identifier] = translation_dic[key]


    for i in range(len(qc_1.data)):
        
        instruction_other = qc_1.data[i]
        qubits = []
        for qb in instruction_other.qubits:
            qubits.append(translation_dic[qb.identifier])
        
        clbits = []
        
        for cb in instruction_other.clbits:
            clbits.append(translation_dic[cb.identifier])
        
        instr_type = type(instruction_other)
        
        qc_0.data.append(instr_type(instruction_other.op, qubits, clbits))
        