"""
@package pmsco.elements.bindingenergy
Electron binding energies of the elements

Extends the element table of the `periodictable` package
(https://periodictable.readthedocs.io/en/latest/index.html)
by the electron binding energies.

The binding energies are compiled from Gwyn Williams' web page
(https://userweb.jlab.org/~gwyn/ebindene.html).
Please refer to the original web page or the x-ray data booklet
for original sources, definitions and remarks.
Binding energies of gases are replaced by respective values of a common compound
from the 'handbook of x-ray photoelectron spectroscopy' (Physical Electronics, Inc., 1995).

Usage
-----

This module requires the periodictable package (https://pypi.python.org/pypi/periodictable).

~~~~~~{.py}
import periodictable as pt
import pmsco.elements.bindingenergy

# read any periodictable's element interfaces, e.g.
print(pt.gold.binding_energy['4f7/2'])
print(pt.elements.symbol('Au').binding_energy['4f7/2'])
print(pt.elements.name('gold').binding_energy['4f7/2'])
print(pt.elements[79].binding_energy['4f7/2'])
~~~~~~

The database is loaded from the accompanying bindingenergy.json file on first demand.
Attributes are writable, you may update the values in your run-time instance of the database.

Normally, the user will not need to call any functions in this module directly.
The query_binding_energy() function queries all terms with a particular binding energy.


@author Matthias Muntwiler

@copyright (c) 2020-23 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
"""

import json
import numpy as np
from pathlib import Path
import periodictable as pt
import periodictable.core

index_energy = np.zeros(0)
index_number = np.zeros(0)
index_term = []

default_data_path = Path(Path(__file__).parent, "bindingenergy.json")


def load_data(data_path=None):
    """
    load binding energy data from json file

    the data file must be in the same format as generated by save_data.

    @param data_path file path of the data file. default: "bindingenergy.json" next to this module file

    @return dictionary
    """
    if data_path is None:
        data_path = default_data_path
    with open(data_path, "rt", encoding="utf8") as fp:
        data = json.load(fp)
    return data


def save_data(data_path=None):
    """
    save binding energy data to json file

    @param data_path file path of the data file. default: "bindingenergy.json" next to this module file

    @return None
    """
    if data_path is None:
        data_path = default_data_path
    data = {}
    for element in pt.elements:
        element_data = {}
        for term, energy in element.binding_energy.items():
            element_data[term] = energy
        if element_data:
            data[element.number] = element_data
    with open(data_path, "w", encoding="utf8") as fp:
        json.dump(data, fp, sort_keys=True, indent='\t')


def init(table, reload=False):
    if 'binding_energy' in table.properties and not reload:
        return
    table.properties.append('binding_energy')

    pt.core.Element.binding_energy = {}
    pt.core.Element.binding_energy_units = "eV"

    data = load_data()
    for el_key, el_data in data.items():
        try:
            el = table[int(el_key)]
        except ValueError:
            el = table.symbol(el_key)
        el.binding_energy = el_data


def build_index():
    """
    build an index for query_binding_energy().

    the index is kept in global variables of the module.

    @return None
    """

    global index_energy
    global index_number
    global index_term

    n = 0
    for element in pt.elements:
        n += len(element.binding_energy)

    index_energy = np.zeros(n)
    index_number = np.zeros(n)
    index_term = []

    for element in pt.elements:
        for term, energy in element.binding_energy.items():
            index_term.append(term)
            i = len(index_term) - 1
            index_energy[i] = energy
            index_number[i] = element.number


def query_binding_energy(energy, tol=1.0):
    """
    search the periodic table for a specific binding energy and return all matching terms.

    @param energy: binding energy in eV.

    @param tol: tolerance in eV.

    @return: list of dictionaries containing element and term specification.
             the list is ordered arbitrarily.
             each dictionary contains the following keys:
             @arg 'number': element number
             @arg 'symbol': element symbol
             @arg 'term': spectroscopic term
             @arg 'energy': actual binding energy
    """
    if len(index_energy) == 0:
        build_index()
    sel = np.abs(index_energy - energy) < tol
    idx = np.where(sel)
    result = []
    for i in idx[0]:
        el_num = int(index_number[i])
        d = {'number': el_num,
             'symbol': pt.elements[el_num].symbol,
             'term': index_term[i],
             'energy': index_energy[i]}
        result.append(d)

    return result


def export_flat_text(f):
    """
    export the binding energies to a flat general text file.

    the file has four space-separated columns `number`, `symbol`, `term`, `energy`.
    column names are included in the first row.

    @param f: file path or open file object
    @return: None
    """
    if hasattr(f, "write") and callable(f.write):
        f.write("number symbol term energy\n")
        for element in pt.elements:
            for term, energy in element.binding_energy.items():
                f.write(f"{element.number} {element.symbol} {term} {energy}\n")
    else:
        with open(f, "w") as fi:
            export_flat_text(fi)


def import_flat_text(f):
    """
    import binding energies from a flat general text file.

    data is in space-separated columns.
    the first row contains column names.
    at least the columns `number`, `term`, `energy` must be present.

    the function updates existing entries and appends entries of non-existing terms.
    existing terms that are not listed in the file remain unchanged.

    @param f: file path or open file object

    @return: None
    """
    data = np.atleast_1d(np.genfromtxt(f, names=True, dtype=None, encoding="utf8"))
    for d in data:
        pt.elements[d['number']].binding_energy[d['term']] = d['energy']


def _load_binding_energy():
    """
    delayed loading of the binding energy table.
    """

    init(periodictable.core.default_table())


periodictable.core.delayed_load(['binding_energy'], _load_binding_energy)
