"""
@package pmsco.elements.spectrum
photoelectron spectrum simulator

this module calculates the basic structure of a photoelectron spectrum.
it calculates positions and approximate amplitude of elastic peaks
based on photon energy, binding energy, photoionization cross section, and stoichiometry.
escape depth, photon flux, analyser transmission are not accounted for.


usage
-----

this module requires python 3.6, numpy, matplotlib and
the periodictable package (https://pypi.python.org/pypi/periodictable).

~~~~~~{.py}
import numpy as np
import periodictable as pt
import pmsco.elements.spectrum as spec

# for working with the data
labels, energy, intensity = spec.build_spectrum(800., {"Ti": 1, "O": 2})

# for plotting
spec.plot_spectrum(800., {"Ti": 1, "O": 2})
~~~~~~



@author Matthias Muntwiler

@copyright (c) 2020 by Paul Scherrer Institut @n
Licensed under the Apache License, Version 2.0 (the "License"); @n
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
  http://www.apache.org/licenses/LICENSE-2.0
"""

from matplotlib import pyplot as plt
import numpy as np
import periodictable as pt
from . import bindingenergy
from . import photoionization


def get_element(number_or_symbol):
    """
    return the given Element object of the periodic table.

    @param number_or_symbol: atomic number (int) or chemical symbol (str).
    @return: Element object.
    """
    try:
        el = pt.elements[number_or_symbol]
    except KeyError:
        el = pt.elements.symbol(number_or_symbol)
    return el


def get_binding_energy(photon_energy, element, nlj):
    """
    look up the binding energy of a core level and check whether it is smaller than the photon energy.

    @param photon_energy: photon energy in eV.
    @param element: Element object of the periodic table.
    @param nlj: (str) spectroscopic term, e.g. '4f7/2'.
    @return: (float) binding energy or numpy.nan.
    """
    try:
        eb = element.binding_energy[nlj]
    except KeyError:
        return np.nan
    if eb < photon_energy:
        return eb
    else:
        return np.nan


def get_cross_section(photon_energy, element, nlj, gamma=None):
    """
    look up the photo-ionization cross-section.

    since the Yeh/Lindau tables do not resolve the spin-orbit splitting,
    this function applies the normal relative weights of a full sub-shell.

    the result is a linear interpolation between tabulated values.

    @param photon_energy: photon energy in eV.
    @param element: Element object of the periodic table.
    @param nlj: (str) spectroscopic term, e.g. '4f7/2'.
        the j-value can be left out, in which case the sum over all j-states is returned.
    @param gamma: (float) angle in degrees between linear polarization vector and photoelectron emission direction.
        By default (None), unpolarized light or magic angle (54.7 deg) geometry is assumed.
    @return: (float) total (gamma=None) or differential (gamma not None) cross section in Mb.
    """

    if not hasattr(element, "photoionization"):
        element = get_element(element)
    try:
        pi = element.photoionization[nlj]
    except KeyError:
        return np.nan

    if gamma is None:
        cs = pi.cross_section(photon_energy)
    else:
        cs = pi.diff_cross_section(photon_energy, gamma)

    return cs


def build_spectrum(photon_energy, elements, binding_energy=False, work_function=4.5, gamma=None):
    """
    calculate the positions and amplitudes of core-level photoemission lines.

    the function looks up the binding energies and cross sections of all photoemission lines in the energy range
    given by the photon energy and returns an array of expected spectral lines.

    @param photon_energy: (numeric) photon energy in eV.
    @param elements: list or dictionary of elements.
        elements are identified by their atomic number (int) or chemical symbol (str).
        if a dictionary is given, the (float) values are stoichiometric weights of the elements.
    @param binding_energy: (bool) return binding energies (True) rather than kinetic energies (False, default).
    @param work_function: (float) work function of the instrument in eV.
    @param gamma: (float) angle in degrees between linear polarization vector and photoelectron emission direction.
        By default (None), unpolarized light or magic angle (54.7 deg) geometry is assumed.
    @return: tuple (labels, positions, intensities) of 1-dimensional numpy arrays representing the spectrum.
        labels are in the format {Symbol}{n}{l}{j}.
    """
    ekin = []
    ebind = []
    intens = []
    labels = []

    for element in elements:
        el = get_element(element)
        for n in range(1, 8):
            for l in "spdf":
                for j in ['', '1/2', '3/2', '5/2', '7/2']:
                    nlj = f"{n}{l}{j}"
                    eb = get_binding_energy(photon_energy, el, nlj)
                    cs = get_cross_section(photon_energy, el, nlj, gamma=gamma)
                    try:
                        cs = cs * elements[element]
                    except (KeyError, TypeError):
                        pass
                    if not np.isnan(eb) and not np.isnan(cs):
                        ekin.append(photon_energy - eb - work_function)
                        ebind.append(eb)
                        intens.append(cs)
                        labels.append(f"{el.symbol}{nlj}")

    ebind = np.array(ebind)
    ekin = np.array(ekin)
    intens = np.array(intens)
    labels = np.array(labels)

    if binding_energy:
        return labels, ebind, intens
    else:
        return labels, ekin, intens


def plot_spectrum(photon_energy, elements, binding_energy=False, work_function=4.5, gamma=None, show_labels=True):
    """
    plot a simple spectrum representation of a material.

    the function looks up the binding energies and cross sections of all photoemission lines in the energy range
    given by the photon energy and returns an array of expected spectral lines.

    the spectrum is plotted using matplotlib.pyplot.stem.

    @param photon_energy: (numeric) photon energy in eV.
    @param elements: list or dictionary of elements.
        elements are identified by their atomic number (int) or chemical symbol (str).
        if a dictionary is given, the (float) values are stoichiometric weights of the elements.
    @param binding_energy: (bool) return binding energies (True) rather than kinetic energies (False, default).
    @param work_function: (float) work function of the instrument in eV.
    @param gamma: (float) angle in degrees between linear polarization vector and photoelectron emission direction.
        By default (None), unpolarized light or magic angle (54.7 deg) geometry is assumed.
    @param show_labels: (bool) show peak labels (True, default) or not (False).
    @return: (figure, axes)
    """
    labels, energy, intensity = build_spectrum(photon_energy, elements, binding_energy=binding_energy,
                                               work_function=work_function, gamma=gamma)

    fig, ax = plt.subplots()
    ax.stem(energy, intensity, basefmt=' ', use_line_collection=True)
    if show_labels:
        for sxy in zip(labels, energy, intensity):
            ax.annotate(sxy[0], xy=(sxy[1], sxy[2]), textcoords='data')

    ax.grid()
    if binding_energy:
        ax.set_xlabel('binding energy')
    else:
        ax.set_xlabel('kinetic energy')
    ax.set_ylabel('intensity')
    ax.set_title(elements)
    return fig, ax


def plot_cross_section(el, nlj):
    energy = np.arange(100, 1500, 140)
    cs = get_cross_section(energy, el, nlj)
    fig, ax = plt.subplots()
    ax.set_yscale("log")
    ax.plot(energy, cs)
