import numpy as np
import pickle

from ase.units import Hartree, Bohr

from gpaw.kpt_descriptor import to1bz
from gpaw.new.ase_interface import GPAW
from gpaw.spinorbit import soc_eigenstates
from gpaw.pw.descriptor import PWDescriptor
import gpaw.mpi as mpi


class Unfold:
    """This Class is used to Unfold the Bands of a supercell (SC) calculations
    into a the primitive cell (PC). As a convention (when possible) capital
    letters variables are related to the SC while lowercase ones to the
    PC """

    def __init__(self,
                 name=None,
                 calc=None,
                 M=None,
                 spin=0,
                 spinorbit=None,
                 theta=90,
                 scale=1.0,
                 phi=90):

        self.name = name
        self.calc = GPAW(calc, txt=None, communicator=mpi.serial_comm)
        self.M = np.array(M, dtype=float)
        self.spinorbit = spinorbit
        self.spin = spin

        self.gd = self.calc.wfs.gd.new_descriptor()

        self.kd = self.calc.wfs.kd
        if self.calc.wfs.mode == 'pw':
            self.pd = self.calc.wfs.pd
        else:
            self.pd = PWDescriptor(ecut=None, gd=self.gd, kd=self.kd,
                                   dtype=complex)

        self.acell_cv = self.gd.cell_cv
        self.bcell_cv = 2 * np.pi * self.gd.icell_cv

        self.nb = self.calc.get_number_of_bands()

        self.v_Kmn = None
        if spinorbit:
            assert self.calc.density.collinear
            self.nb *= 2
            if mpi.world.rank == 0:
                print('Calculating spinorbit Corrections')
            soc = soc_eigenstates(self.calc,
                                  scale=scale, theta=theta, phi=phi)
            self.e_mK = soc.eigenvalues().T
            self.v_Kmn = soc.eigenvectors()
            if mpi.world.rank == 0:
                print('Done with the spinorbit Corrections')

    def get_K_index(self, K):
        """Find the index of a given K."""

        K = np.array([K])
        bzKG = to1bz(K, self.acell_cv)[0]
        iK = self.kd.where_is_q(bzKG, self.kd.ibzk_kc)
        return iK

    def get_g(self, iK):
        """Not all the G vectors are relevant for the bands unfolding,
        but only the ones that match the PC reciprocal vectors.
        This function finds the relevant ones."""

        G_Gv_temp = self.pd.get_reciprocal_vectors(q=iK, add_q=False)
        G_Gc_temp = np.dot(G_Gv_temp, np.linalg.inv(self.bcell_cv))

        iG_list = []
        g_list = []
        for iG, G in enumerate(G_Gc_temp):
            a = np.dot(G, np.linalg.inv(self.M).T)
            check = np.abs(a) % 1 < 1e-5
            check2 = np.abs((np.abs(a[np.where(~check)]) % 1) - 1) < 1e-5
            if all(check) or all(check2):
                iG_list.append(iG)
                g_list.append(G)

        return np.array(iG_list), np.array(g_list)

    def get_G_index(self, iK, G, G_list):
        """Find the index of a given G."""

        G_list -= G
        sumG = np.sum(abs(G_list), axis=1)
        iG = np.where(sumG < 1e-5)[0]
        return iG

    def get_eigenvalues(self, iK):
        """Get the list of eigenvalues for a given iK."""

        if not self.spinorbit:
            e_m = self.calc.get_eigenvalues(kpt=iK, spin=self.spin)
        else:
            e_m = self.e_mK[:, iK]
        return np.array(e_m) / Hartree

    def get_pw_wavefunctions_k(self, iK):
        """Get the list of Fourier coefficients of the WaveFunction for a
        given iK. For spinors the number of bands is doubled and a spin
        dimension is added."""

        psi_mgrid = get_rs_wavefunctions_k(self.calc, iK, self.spinorbit,
                                           self.v_Kmn, spin=self.spin)
        if not self.spinorbit and self.calc.density.collinear:
            psi_list_mG = []
            for i in range(len(psi_mgrid)):
                psi_list_mG.append(self.pd.fft(psi_mgrid[i], iK))

            psi_mG = np.array(psi_list_mG)
            return psi_mG
        else:
            u0_list_mG = []
            u1_list_mG = []
            for i in range(psi_mgrid.shape[0]):
                u0_list_mG.append(self.pd.fft(psi_mgrid[i, 0], iK))
                u1_list_mG.append(self.pd.fft(psi_mgrid[i, 1], iK))

            u0_mG = np.array(u0_list_mG)
            u1_mG = np.array(u1_list_mG)

            u_mG = np.zeros((len(u0_mG),
                             2,
                             u0_mG.shape[1]), complex)
            u_mG[:, 0] = u0_mG
            u_mG[:, 1] = u1_mG

            return u_mG

    def get_spectral_weights_k(self, k_t):
        r"""Returns the spectral weights for a given k in the PC:

            P_mK(k_t) = \sum_n |<Km|k_t n>|**2

        which can be shown to be equivalent to:

            P_mK(k_t) = \sum_g |C_Km(g+k_t-K)|**2
        """

        K_c, G_t = find_K_from_k(k_t, self.M)
        iK = self.get_K_index(K_c)
        iG_list, g_list = self.get_g(iK)
        gG_t_list = g_list + G_t

        G_Gv = self.pd.get_reciprocal_vectors(q=iK, add_q=False)
        G_Gc = np.dot(G_Gv, np.linalg.inv(self.bcell_cv))

        igG_t_list = []
        for g in gG_t_list:
            igG_t_list.append(self.get_G_index(iK, g.copy(), G_Gc.copy()))

        C_mG = self.get_pw_wavefunctions_k(iK)
        P_m = []
        if not self.spinorbit and self.calc.density.collinear:
            for m in range(self.nb):
                P = 0.
                norm = np.sum(np.linalg.norm(C_mG[m, :])**2)
                for iG in igG_t_list:
                    P += np.linalg.norm(C_mG[m, iG])**2
                P_m.append(P / norm)
        else:
            for m in range(self.nb):
                P = 0.
                norm = np.sum(np.linalg.norm(C_mG[m, 0, :])**2 +
                              np.linalg.norm(C_mG[m, 1, :])**2)
                for iG in igG_t_list:
                    P += (np.linalg.norm(C_mG[m, 0, iG])**2 +
                          np.linalg.norm(C_mG[m, 1, iG])**2)
                P_m.append(P / norm)

        return np.array(P_m)

    def get_spectral_weights(self, kpoints, filename=None):
        """Collect the spectral weights for the k points in the kpoints list.

        This function is parallelized over k's."""

        Nk = len(kpoints)
        Nb = self.nb

        world = mpi.world
        if filename is None:
            try:
                e_mK, P_mK = pickle.load(open('weights_' + self.name +
                                              '.pckl', 'rb'))
            except OSError:
                e_Km = []
                P_Km = []
                if world.rank == 0:
                    print('Getting EigenValues and Weights')

                e_Km = np.zeros((Nk, Nb))
                P_Km = np.zeros((Nk, Nb))
                myk = range(0, Nk)[world.rank::world.size]
                for ik in myk:
                    k = kpoints[ik]
                    print('kpoint: %s' % k)
                    K_c, G_c = find_K_from_k(k, self.M)
                    iK = self.get_K_index(K_c)
                    e_Km[ik] = self.get_eigenvalues(iK)
                    P_Km[ik] = self.get_spectral_weights_k(k)

                world.barrier()
                world.sum(e_Km)
                world.sum(P_Km)

                e_mK = np.array(e_Km).T
                P_mK = np.array(P_Km).T
                if world.rank == 0:
                    pickle.dump((e_mK, P_mK),
                                open('weights_' + self.name + '.pckl', 'wb'))
        else:
            e_mK, P_mK = pickle.load(open(filename, 'rb'))

        return e_mK, P_mK

    def spectral_function(self, kpts, x, X, points_name, width=0.002,
                          npts=10000, filename=None):
        r"""Returns the spectral function for all the ks in kpoints:

                                              eta / pi

            A_k(e) = \sum_m  P_mK(k) x  ---------------------

                                        (e - e_mk)**2 + eta**2


        at each k-points defined on npts energy points in the range
        [emin, emax]. The width keyword is FWHM = 2 * eta."""

        Nk = len(kpts)
        A_ke = np.zeros((Nk, npts), float)

        world = mpi.world
        e_mK, P_mK = self.get_spectral_weights(kpts, filename)
        if world.rank == 0:
            print('Calculating the Spectral Function')
        emin = np.min(e_mK) - 5 * width
        emax = np.max(e_mK) + 5 * width
        e = np.linspace(emin, emax, npts)

        for ik in range(Nk):
            for ie in range(len(e_mK[:, ik])):
                e0 = e_mK[ie, ik]
                D = (width / 2 / np.pi) / ((e - e0)**2 + (width / 2)**2)
                A_ke[ik] += P_mK[ie, ik] * D
        if world.rank == 0:
            pickle.dump((e * Hartree, A_ke, x, X, points_name),
                        open('sf_' + self.name + '.pckl', 'wb'))
            print('Spectral Function calculation completed!')
        return


def find_K_from_k(k, M):
    """Gets a k vector in scaled coordinates and returns a K vector and the
    unfolding G in scaled Coordinates."""

    KG = np.dot(M, k)
    G = np.zeros(3, dtype=int)

    for i in range(3):
        if KG[i] > 0.5000001:
            G[i] = int(np.round(KG[i]))
            KG[i] -= np.round(KG[i])
        elif KG[i] < -0.4999999:
            G[i] = int(np.round(KG[i]))
            KG[i] += abs(np.round(KG[i]))

    return KG, G


def get_rs_wavefunctions_k(calc, iK, spinorbit=False, v_Kmn=None, spin=0):
    """Get the list of WaveFunction for a given iK. For spinors the number of
    bands is doubled and a spin dimension is added."""

    N_c = calc.wfs.gd.N_c
    k_c = calc.wfs.kd.ibzk_kc[iK]
    Nb = calc.wfs.bd.nbands
    Ns = calc.wfs.nspins
    eikr_R = np.exp(-2j * np.pi * np.dot(np.indices(N_c).T,
                                         k_c / N_c).T)

    if calc.wfs.mode == 'lcao' and not calc.wfs.positions_set:
        calc.initialize_positions()

    if not spinorbit:
        psit_mgrid = np.array(
            [calc.get_pseudo_wave_function(m, iK, spin, periodic=True)
             for m in range(Nb)]) * Bohr**1.5
        return psit_mgrid
    else:
        v_mn = v_Kmn[iK]
        v0_mn = v_mn[:, ::2]
        v1_mn = v_mn[:, 1::2]

        u0_ngrid = np.array(
            [calc.wfs.get_wave_function_array(n, iK, 0) * eikr_R
             for n in range(Nb)])
        u1_ngrid = np.array(
            [calc.wfs.get_wave_function_array(n, iK, (Ns - 1)) * eikr_R
             for n in range(Nb)])
        u0_mG = np.swapaxes(np.dot(v0_mn, np.swapaxes(u0_ngrid, 0, 2)), 1, 2)
        u1_mG = np.swapaxes(np.dot(v1_mn, np.swapaxes(u1_ngrid, 0, 2)), 1, 2)
        ut_mgrid = np.zeros((len(u0_mG),
                             2,
                             len(u0_mG[0]),
                             len(u0_mG[0, 0]),
                             len(u0_mG[0, 0, 0])), complex)
        ut_mgrid[:, 0] = u0_mG
        ut_mgrid[:, 1] = u1_mG
        return ut_mgrid


def plot_spectral_function(filename, color='blue', eref=None,
                           emin=None, emax=None, scale=1):
    """Function to plot spectral function corresponding to the bandstructure
    along the kpoints path."""

    try:
        e, A_ke, x, X, points_name = pickle.load(open(filename + '.pckl',
                                                      'rb'))
    except OSError:
        print('You Need to Calculate the SF first!')
        raise SystemExit()

    import matplotlib.pyplot as plt
    print('Plotting Spectral Function')

    if eref is not None:
        e -= eref
    if emin is None:
        emin = e.min()
    if emax is None:
        emax = e.max()

    A_ke /= np.max(A_ke)
    A_ek = A_ke.T * scale
    A_ekc = np.reshape(A_ek, (A_ek.shape[0], A_ek.shape[1]))

    mycmap = make_colormap(color)

    plt.figure()

    plt.plot([0, x[-1]], 2 * [0.0], '--', c='0.5')
    plt.imshow(A_ekc + 0.23,
               cmap=mycmap,
               aspect='auto',
               origin='lower',
               vmin=0.,
               vmax=1,
               extent=[0, x[-1], e.min(), e.max()])

    for k in X[1:-1]:
        plt.plot([k, k], [emin, emax], lw=0.5, c='0.5')
    plt.xticks(X, points_name, size=20)
    plt.yticks(size=20)
    plt.ylabel('E(eV)', size=20)
    plt.axis([0, x[-1], emin, emax])
    plt.savefig(filename + '_spec.png')
    plt.show()


def plot_band_structure(e_mK, P_mK, x, X, points_name,
                        weights_mK=None, color='red', fit=True, nfit=200):
    """Function to plot the bandstructure using the P_mK weights directly.
    each point is represented with a filled circle, whose size and color
    vary with as a function of P_mK."""

    import matplotlib.pyplot as plt
    print('Plotting Bands Structure')
    emin = e_mK.min()
    emax = e_mK.max()

    new_cmap = make_colormap(color)

    if weights_mK is None:
        weights_mK = P_mK.copy()
    else:
        weights_mK *= P_mK.copy()

    plt.figure()
    plt.plot([0, x[-1]], 2 * [0.0], '--', c='0.5')
    plt.scatter(np.tile(x, len(e_mK)), e_mK.reshape(-1),
                c=P_mK.reshape(-1),
                cmap=new_cmap,
                vmin=0.,
                vmax=1.,
                s=20. * weights_mK.reshape(-1),
                marker='o',
                edgecolor='none')

    for k in X[1:-1]:
        plt.plot([k, k], [emin, emax], lw=0.5, c='0.5')
    plt.xticks(X, points_name, size=20)
    plt.yticks(size=20)
    plt.ylabel('E(eV)', size=20)
    plt.axis([0, x[-1], emin, emax])
    plt.show()


def make_colormap(main_color):
    """Custom colormaps used in plot_spectral function and
    plot_band_structure."""

    from matplotlib.colors import LinearSegmentedColormap
    if main_color == 'blue':
        cdict = {'red': ((0.0, 1.0, 1.0),
                         (0.25, 1.0, 1.0),
                         (0.5, 0.0, 0.0),
                         (1.0, 0.0, 0.0)),

                 'green': ((0.0, 1.0, 1.0),
                           (0.25, 1.0, 1.0),
                           (0.5, 0.0, 0.0),
                           (1.0, 0.0, 0.0)),

                 'blue': ((0.0, 1.0, 1.0),
                          (0.25, 1.0, 1.0),
                          (0.5, 1.0, 1.0),
                          (1.0, 0.75, 0.75))}

    elif main_color == 'red':
        cdict = {'red': ((0.0, 1.0, 1.0),
                         (0.25, 1.0, 1.0),
                         (0.5, 1.0, 1.0),
                         (1.0, 0.75, 0.75)),

                 'green': ((0.0, 1.0, 1.0),
                           (0.25, 1.0, 1.0),
                           (0.5, 0.0, 0.0),
                           (1.0, 0.0, 0.0)),

                 'blue': ((0.0, 1.0, 1.0),
                          (0.25, 1.0, 1.0),
                          (0.5, 0.0, 0.0),
                          (1.0, 0.0, 0.0))}

    elif main_color == 'green':
        cdict = {'red': ((0.0, 1.0, 1.0),
                         (0.25, 1.0, 1.0),
                         (0.5, 0.0, 0.0),
                         (1.0, 0.0, 0.0)),

                 'green': ((0.0, 1.0, 1.0),
                           (0.25, 1.0, 1.0),
                           (0.5, 1.0, 1.0),
                           (1.0, 0.75, 0.75)),

                 'blue': ((0.0, 1.0, 1.0),
                          (0.25, 1.0, 1.0),
                          (0.5, 0.0, 0.0),
                          (1.0, 0.0, 0.0)),

                 'alpha': ((0.0, 0.0, 0.0),
                           (0.25, 0.1, 0.1),
                           (0.5, 1.0, 1.0),
                           (1.0, 1.0, 1.0))}

    cmap = LinearSegmentedColormap('mymap', cdict)
    return cmap


def get_vacuum_level(calc, plot_pot=False):
    """Get the vacuum energy level from a given calculator."""

    calc.restore_state()
    if calc.wfs.mode == 'pw':
        vHt_g = calc.hamiltonian.pd3.ifft(calc.hamiltonian.vHt_q) * Hartree
    else:
        vHt_g = calc.hamiltonian.vHt_g * Hartree
    vHt_z = np.mean(np.mean(vHt_g, axis=0), axis=0)

    if plot_pot:
        import matplotlib.pyplot as plt
        plt.plot(vHt_z)
        plt.show()
    return vHt_z[0]
