# -*- coding: utf-8 -*-

# Copyright 2017, IBM.
#
# This source code is licensed under the Apache License, Version 2.0 found in
# the LICENSE.txt file in the root directory of this source tree.

"""
The eval_hamiltonian function has been borrowed from QISKit's tools/apps/optimization.py
and slightly modified by dividiti to fit our benchmarking needs.
"""

import uuid
import copy
import numpy as np

from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.extensions.standard import h, x, y, z
from qiskit.tools.apps.optimization import make_Hamiltonian, group_paulis, measure_pauli_z


def eval_hamiltonian(Q_program, hamiltonian, input_circuit, shots, device, timeout=60):
    """Calculates the average value of a Hamiltonian on a state created by the
     input circuit

    Args:
        Q_program (QuantumProgram): QuantumProgram object used to run the
            input circuit.
        hamiltonian (array or matrix or list): a representation of the
            Hamiltonian or observables to be measured. If it is a list, it is
            a list of Pauli operators grouped into tpb sets.
        input_circuit (QuantumCircuit): input circuit.
        shots (int): number of shots considered in the averaging. If 1 the
            averaging is exact.
        device (str): the backend used to run the simulation.
    Returns:
        float: Average value of the Hamiltonian or observable.
    """
    energy = 0

    q_execution_times = []

    if 'statevector' in device:
        # Hamiltonian is not a pauli_list grouped into tpb sets
        if not isinstance(hamiltonian, list):
            circuit = ['c' + str(uuid.uuid4())]    # unique random circuit for no collision
            Q_program.add_circuit(circuit[0], input_circuit)
            result = Q_program.execute(circuit, device, shots=shots, timeout=timeout,
                                       config={"data": ["statevector"]})
            statevector = result.get_data(circuit[0]).get('statevector')
            if statevector is None:
                statevector = result.get_data(
                    circuit[0]).get('statevector')
                if statevector:
                    statevector = statevector[0]
            # Diagonal Hamiltonian represented by 1D array
            if (hamiltonian.shape[0] == 1 or
                    np.shape(np.shape(np.array(hamiltonian))) == (1,)):
                energy = np.sum(hamiltonian * np.absolute(statevector) ** 2)
            # Hamiltonian represented by square matrix
            elif hamiltonian.shape[0] == hamiltonian.shape[1]:
                energy = np.inner(np.conjugate(statevector),
                                  np.dot(hamiltonian, statevector))
        # Hamiltonian represented by a Pauli list
        else:
            circuits = []
            circuits_labels = []
            circuits.append(input_circuit)
            # Trial circuit w/o the final rotations
            circuits_labels.append('circuit_label0' + str(uuid.uuid4()))
            Q_program.add_circuit(circuits_labels[0], circuits[0])
            # Execute trial circuit with final rotations for each Pauli in
            # hamiltonian and store from circuits[1] on
            n_qubits = input_circuit.regs['q'].size
            q = QuantumRegister(n_qubits, "q")
            i = 1
            for p in hamiltonian:
                circuits.append(copy.deepcopy(input_circuit))
                for j in range(n_qubits):
                    if p[1].v[j] == 0 and p[1].w[j] == 1:
                        circuits[i].x(q[j])
                    elif p[1].v[j] == 1 and p[1].w[j] == 0:
                        circuits[i].z(q[j])
                    elif p[1].v[j] == 1 and p[1].w[j] == 1:
                        circuits[i].y(q[j])

                circuits_labels.append('circuit_label' + str(i) + str(uuid.uuid4()))
                Q_program.add_circuit(circuits_labels[i], circuits[i])
                i += 1
            result = Q_program.execute(circuits_labels, device, shots=shots, timeout=timeout)
            # no Pauli final rotations
            statevector_0 = result.get_data(
                circuits_labels[0])['statevector']
            i = 1
            for p in hamiltonian:
                statevector_i = result.get_data(
                    circuits_labels[i])['statevector']
                # inner product with final rotations of (i-1)-th Pauli
                energy += p[0] * np.inner(np.conjugate(statevector_0),
                                          statevector_i)
                i += 1
    # finite number of shots and hamiltonian grouped in tpb sets
    else:
        circuits = []
        circuits_labels = []
        n = int(len(hamiltonian[0][0][1].v))
        q = QuantumRegister(n, "q")
        c = ClassicalRegister(n, "c")
        i = 0
        for tpb_set in hamiltonian:
            circuits.append(copy.deepcopy(input_circuit))
            circuits_labels.append('tpb_circuit_' + str(i) + str(uuid.uuid4()))
            for j in range(n):
                # Measure X
                if tpb_set[0][1].v[j] == 0 and tpb_set[0][1].w[j] == 1:
                    circuits[i].h(q[j])
                # Measure Y
                elif tpb_set[0][1].v[j] == 1 and tpb_set[0][1].w[j] == 1:
                    circuits[i].s(q[j]).inverse()
                    circuits[i].h(q[j])
                circuits[i].measure(q[j], c[j])
            Q_program.add_circuit(circuits_labels[i], circuits[i])
            i += 1

        result = Q_program.execute(circuits_labels, device, shots=shots, timeout=timeout)

        for j, _ in enumerate(hamiltonian):
#            print( "Q execution data [{}] = {}".format(j, result.get_data(circuits_labels[j])))

            q_execution_time = result.get_data(circuits_labels[j]).get('time')
            if q_execution_time:
                q_execution_times.append( q_execution_time )

            for k, _ in enumerate(hamiltonian[j]):
                energy += hamiltonian[j][k][0] *\
                    measure_pauli_z(result.get_counts(
                        circuits_labels[j]), hamiltonian[j][k][1])

    return energy, q_execution_times
