# -*- coding: utf-8 -*-
"""
Created on Mon Aug 20 10:03:37 2018

@author: USER 
"""


# Codes are free to use. Do whatever you want

from __future__ import absolute_import 

"""Read raw data"""

####################### LIBRARY #############################

# exceptions library
from exceptions import (Excel_Load_Exception,
                        Data_Load_Exception,
                        Data_Format_Exception)

# Python stdlib imports
import os
import pickle

# data processing library
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# pyrod library

from read.read_parameters import initialization_parameters
from read.read_raw_data import initialization_rhkl

####################### CONSTANT ############################

# constant 

R0 = 2.82e-5 # Thompson scattering length, the radi of electron in Angs
NA = 6.022e23 # Avogadros number corresponding to g/cm3 e23

# atomic_mass.xlsx and atomic_scattering_factor.pickle

ATOMIC_MASS = os.path.abspath(os.path.dirname('atomic_mass.xlsx')) +\
                     '/base/atomic_mass.xlsx'
ASF = os.path.abspath(os.path.dirname('atomic_scattering_factor.pickle')) +\
                     '\\base\\atomic_scattering_factor.pickle'

try:
    ATOM_TABLE = pd.read_excel(ATOMIC_MASS,
                               sheet_name = 'Sheet1',
                               index_col = 0)
    ATOM_LIST = ATOM_TABLE.index.values.tolist()
    
    f = open(ASF,'rb')
    ASF_TABLE = pickle.load(f)
    f.close()
    
except Excel_Load_Exception:
    print('load atomic form factor data base load fail')

# load ctr optimised data

CTR_PATH = os.path.abspath(os.path.dirname('ctr_optimised_result.pickle')) +\
                     '/data/ctr_optimised_result.pickle'

ctr_optimised_result = {}

try:
    with open(CTR_PATH, 'rb') as f:
        unpickler = pickle.Unpickler(f)
        ctr_optimised_result = unpickler.load()
    
except Data_Load_Exception:
    print('Please optimise and save the CTR data first!')

# save xrr optimised data path

XRR_PATH = os.path.abspath(os.path.dirname('xrr_optimised_result.pickle')) +\
                     '/data/xrr_optimised_result.pickle'
                     
####################### FUNCTIONS ###########################

# elements = [A,B]
# result density - g/cm3
def density(elements,lattice):
    
#elements = ['Sr','Ti']
#lattice = [3.905,3.905,3.905,np.pi/2,np.pi/2,np.pi/2]
    
    """theroy density of ABO3 pervoskite oxide"""
    
    try:
        
        A_mass = ATOM_TABLE.at[elements[0],'mass']
        B_mass = ATOM_TABLE.at[elements[1],'mass']
        O_mass = ATOM_TABLE.at['O','mass']
        
        a = lattice[0]
        b = lattice[1]
        c = lattice[2]
        
        alpha = lattice[3]*np.pi/180
        beta  = lattice[4]*np.pi/180
        gamma = lattice[5]*np.pi/180
        
    except Data_Format_Exception:
        print('Parameters error!')
        
    lattice_volume = a*b*c*np.sqrt(1 + 2*np.cos(alpha)*np.cos(beta)*np.cos(gamma)-\
                                   np.cos(alpha)**2 - \
                                   np.cos(beta)**2 - \
                                   np.cos(gamma)**2)
    A_unitcell = A_mass
    B_unitcell = B_mass
    O_unitcell = 3*O_mass
    
    na = NA/1e23
    
    density_A = A_unitcell/(lattice_volume*na*0.1)
    density_B = B_unitcell/(lattice_volume*na*0.1)
    density_O = O_unitcell/(lattice_volume*na*0.1)
    
    density_pervoskite = density_A + density_B + density_O
        
    return density_pervoskite, density_A, density_B, density_O
    
# wave length should be Angs
# energy is the x-ray energy kev
# elements = [A,B]
def imag_f(elements, energy):
    
    """imag part of x-ray scattering factor for a certin element"""
    
    A_energys = ASF_TABLE[elements[0]]['E(kev)']
    B_energys = ASF_TABLE[elements[1]]['E(kev)']
    O_energys = ASF_TABLE['O']['E(kev)']
    
    A_imagf = ASF_TABLE[elements[0]]['imag_f']
    B_imagf = ASF_TABLE[elements[1]]['imag_f']
    O_imagf = ASF_TABLE['O']['imag_f']
    
    def f2(energy, energys, imag_f):
        
        atom_f2 = np.interp(energy, energys, imag_f)
        
        return atom_f2
    
    A_f2 = f2(energy, A_energys, A_imagf)
    B_f2 = f2(energy, B_energys, B_imagf)
    O_f2 = f2(energy, O_energys, O_imagf)
    
    return A_f2, B_f2, O_f2

# energy is x-ray energy kev
# elements = [A,B]
# atttenuation coefficiment unit is Angs
def attenuation_coefficient(elements, densities, f2, energy):
    
    wave_length = 12.38/energy # unit Angs
    na = NA/1e23
    
    A_ac = (densities[1]*na/ATOM_TABLE.at[elements[0],'mass'])*2*R0*wave_length*f2[0]
    B_ac = (densities[2]*na/ATOM_TABLE.at[elements[1],'mass'])*2*R0*wave_length*f2[1]
    O_ac = (densities[3]*na/ATOM_TABLE.at['O','mass'])*2*R0*wave_length*f2[2]
    
    ac = A_ac + B_ac + O_ac
    
    return ac

######################## CLASSS #############################
        
class XRR(initialization_parameters, initialization_rhkl):
    
    def __init__(self, parameters, experiment_data, energy):
        
        initialization_parameters.__init__(self, parameters)
        initialization_parameters._var_list(self)
        initialization_parameters._var_table(self)
        initialization_rhkl.__init__(self, experiment_data)
            
        self.energy = energy  # unit - kev
        self.wave_length = 12.38/energy # unit-Angs
        self.coefficient = {}
        
        self.iq,self.i = self._read_rhkl(self)
        self.lattice_c = self.var_list['substrate']['lattice'].at['constant','c']
        self.q = self.iq*2*np.pi/self.lattice_c
        
        for key in self.var_list:
            
            element = [self.var_list[key]['ions'].columns[3], self.var_list[key]['ions'].columns[0]]
            lattice = self.var_list[key]['lattice'].as_matrix().tolist()[0]
            
            densities = density(element, lattice)
            f2 = imag_f(element, energy)
            ac = attenuation_coefficient(element, densities, f2, energy)
            
            self.coefficient[key] = [element,lattice,densities,ac]
            
        # stack list
        
        self.scattering_factor = [] # density*R0 + 1j*attenuation_coefficient
        self.d_space = [] # stack thickness
        self.roughness = [] # roughness at each layer
        
        for layeri in range(len(self.var_table['posz_list'])):
            
            self.d_space.append(self.var_table['posz_table'][0,0]*self.lattice_c*0.98)
            self.roughness.append(0)
            
            slab = self.var_table['slab_list'][layeri]
            dens = self.coefficient[slab][2][0]
            ac = self.coefficient[slab][3]
            
            self.scattering_factor.append(dens*R0+1j*ac)
            
        self.scattering_factor.append(1e-21) # the scattering factor of vacuum
        self.roughness.append(0)
        self.roughness[0] = 0
        self.thickness = np.sum(self.d_space)
        
        # rt - ratio
        self.rt = 1
        
    # re initilization the factors-d_space, scattering_factor, roughness, thichness
    def re(self):
        
        # stack list
        
        self.scattering_factor = [] # density*R0 + 1j*attenuation_coefficient
        self.d_space = [] # stack thickness
        self.roughness = [] # roughness at each layer
        
        for layeri in range(len(self.var_table['posz_list'])):
            
            self.d_space.append(self.var_table['posz_table'][0,0]*self.lattice_c*0.98)
            self.roughness.append(0)
            
            slab = self.var_table['slab_list'][layeri]
            dens = self.coefficient[slab][2][0]
            ac = self.coefficient[slab][3]
            
            self.scattering_factor.append(dens*R0+1j*ac)
            
        self.scattering_factor.append(1e-21) # the scattering factor of vacuum
        self.roughness.append(0)
        self.roughness[0] = 0
        self.thickness = np.sum(self.d_space)

    def disp(self):
        
        # list all the self parameters
        
        print("--------------------------------------------")
        print("densities:\n")
        for key in self.coefficient:
            print("    " + key + ": %s\n" %self.coefficient[key][2][0])
            
        print("scattering factor: %s - %s\n" % (self.scattering_factor[ 0], 
                                                self.scattering_factor[-1]))
        print("roughness: %s - %s\n" % (self.roughness[ 0],
                                        self.roughness[-1]))
        print("d_space: %s - %s Angs\n" % (self.d_space[ 0],
                                           self.d_space[-1]))
        print("thichness: %s Angs\n" % np.sum(self.d_space))
        print('--------------------------------------------')
        
    # homogeneous slab reflection
    def homo_slab(self):
        
        slabs = list(self.var_list.keys())
        slabs.remove('substrate')
        
        # check if the slab is homogeneous
        if len(slabs) != 1:
            raise Data_Format_Exception('Not homogeneous slab!')
        
        dens = self.coefficient[slabs[0]][2][0]
        thickness = np.sum(self.d_space)
            
        reflect_inten = -1j*(4*np.pi*dens*R0*thickness/self.q)*\
                            (np.sin(self.q*thickness/2)/(self.q*thickness/2))*\
                            np.exp(1j*self.q*thickness/2)
                                
        return self.iq, reflect_inten
    
    # Parratt reflectivities 
    # Maybe a little difficult while optimizing parameter, parratt model is accurate
    # Recomed method
    def parratt(self):
        
        """"Elements of Modern X-ray Physics" by Jens Als-Nielsen and Des McMorrow,
        Calculates: Parratt reflectivity of a multilayer"""
        
        k = 2*np.pi/self.wave_length # diffraction vector#
        layer_num = len(self.scattering_factor) # layer number, vacuum is added
        
        #----- Calculate refractive index n of each layer
        delta = self.wave_length**2*np.real(self.scattering_factor)/(2*np.pi)
        beta  = self.wave_length*np.imag(self.scattering_factor)/(4*np.pi)
        # relfractive index
#        nu = 1 - delta + 1j*beta
        
        #----- Wavevector transfer in each layer
        trans_vector = np.zeros([layer_num+1, len(self.q)], dtype = np.complex)
        trans_vector[0,:] = self.q
        
        for i in range(layer_num):
            trans_vector[i+1,:] = np.sqrt(self.q**2 - 8*k**2*delta[i] + 1j*8*k**2*beta[i])
            
        #----- Reflection coefficients (no multiple scattering)
        reflect_coe = np.zeros([layer_num, len(self.q)], dtype = np.complex)
        
        for i in range(layer_num):
            reflect_coe[i,:] = ((trans_vector[i,:] - trans_vector[i+1,:])/\
                                (trans_vector[i,:] + trans_vector[i+1,:]))*\
                                np.exp(-0.5*trans_vector[i,:]*trans_vector[i+1,:]*self.roughness[i])
                                
        #----- Reflectivity from first layer
        reflectivity = np.zeros([layer_num-1, len(self.q)], dtype = np.complex)
        
        phase1 = np.exp(1j*trans_vector[layer_num-1]*self.d_space[layer_num-2])
        
        if layer_num > 1:
            reflectivity[0,:] = (reflect_coe[layer_num-2,:] + \
                                 reflect_coe[layer_num-1,:]*phase1)/\
                                (1 + reflect_coe[layer_num-2,:]*\
                                 reflect_coe[layer_num-1,:]*phase1)
        if layer_num > 2:
            for i in range(1,layer_num-1):
                
                phasei = np.exp(1j*trans_vector[layer_num-i-1,:]*self.d_space[layer_num-i-2])
                
                reflectivity[i,:] = (reflect_coe[layer_num-i-2,:] +\
                                     reflectivity[i-1,:]*phasei)/\
                                    (1 + reflect_coe[layer_num-i-2,:]*\
                                     reflectivity[i-1,:]*phasei)
        
        #------ Intensity reflectivity
        
        # should be reminded here! The data is not squared! To keep uniform with ctr fitting
        
        if layer_num == 1:
            reflect_inten = reflect_coe[0,:]
        else:
            reflect_inten = reflectivity[-1,:]
            
        return self.iq, reflect_inten
    
#    def p_xrr(q, inten):
#        
#        plt.plot(q, np.log(inten))

class xr(XRR):
    
    # the relax of surface, secface  and interface
    # parratt method only
    
    def xr_relax(self, interface = 0.9, secface = 1, surface = 1.03, rt = 1):
        
        XRR.re(self)
        
        self.rt = rt
        self.d_space[ 0] = interface
        self.d_space[-2] = secface
        self.d_space[-1] = surface
        
        q, inten = XRR.parratt(self)
        
        plt.plot(q, np.log(abs(self.rt*inten + ctr_optimised_result['substrate_ctr'])))
        plt.plot(q, np.log(abs(ctr_optimised_result['shkl'])))
        
        return q, inten
    
    # modulate the roughness at interface and surface
    # parratt method only
    def xr_roughness(self, interface = 0.1, surface = 0.1, rt = 1):
        
        XRR.re(self)
        
        self.rt = rt
        self.roughness[ 0] = interface
        self.roughness[-1] = surface
        
        q, inten = XRR.parratt(self)
        
        plt.plot(q, np.log(abs(rt*inten + ctr_optimised_result['substrate_ctr'])))
        plt.plot(q, np.log(abs(ctr_optimised_result['shkl'])))
        
        return q, inten
    
    # thickness modulation
    # homo slab method or parratt method
    def xr_thickness(self, miu = 0, mode = 'parratt', rt = 1):
        
        XRR.re(self)
        
        self.rt = rt
        self.d_space = (np.array(self.d_space) - miu).tolist()
        
        if mode == 'homo_slab':
            q, inten = XRR.homo_slab(self)
        elif mode == 'parratt':
            q, inten = XRR.parratt(self)
            
        plt.plot(q, np.log(abs(rt*inten + ctr_optimised_result['substrate_ctr'])))
        plt.plot(q, np.log(abs(ctr_optimised_result['shkl'])))
            
        return q, inten
    
    # export optimised xrr data to /data
    def save(self, mode = 'parratt'):
        
        xrr_optimised_result = {}
        
        if mode == 'homo_slab':
            q, inten = XRR.homo_slab(self)
        elif mode == 'parratt':
            q, inten = XRR.parratt(self)
            
        xrr_optimised_result['q'] = q
        xrr_optimised_result['inten'] = inten
        xrr_optimised_result['rt'] = self.rt
        
        f = open(XRR_PATH,'wb')
        pickle.dump(xrr_optimised_result, f)
        f.close()
        
        