import numpy as np
import os
import re
import json
from .xrd import cal_UnitCellVolume, cal_dspacing
from .jcpds_dioptas import jcpds, jcpds_reflection
import datetime
from scipy import interpolate

# import numpy.ma as ma

try:
    import pymatgen.core as mg
    from pymatgen.analysis.diffraction.xrd import XRDCalculator
    from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
except ImportError:
    mg = None
    XRDCalculator = None
    SpacegroupAnalyzer = None


def bm3_p(volume, v0, k0, k0p):
    volume = np.asarray(volume, dtype=float)
    eta = np.power(v0 / volume, 1.0 / 3.0)
    pressure = 1.5 * k0 * (np.power(eta, 7.0) - np.power(eta, 5.0))
    pressure *= 1.0 + 0.75 * (k0p - 4.0) * (np.power(eta, 2.0) - 1.0)
    if pressure.ndim == 0:
        return float(pressure)
    return pressure


def bm3_v(pressure, v0, k0, k0p, min_strain=0.3):
    pressure = float(pressure)
    if pressure <= 0.0:
        return float(v0)

    min_volume = float(v0) * max(1.0e-6, (1.0 - float(min_strain)) ** 3)
    low = min_volume
    high = float(v0)

    p_low = float(bm3_p(low, v0, k0, k0p))
    while p_low < pressure and low > v0 * 1.0e-6:
        low *= 0.5
        p_low = float(bm3_p(low, v0, k0, k0p))

    for __ in range(80):
        mid = 0.5 * (low + high)
        p_mid = float(bm3_p(mid, v0, k0, k0p))
        if abs(p_mid - pressure) < 1.0e-8:
            return mid
        if p_mid > pressure:
            low = mid
        else:
            high = mid
    return 0.5 * (low + high)


def _require_pymatgen_for_cif():
    if mg is not None and XRDCalculator is not None and SpacegroupAnalyzer is not None:
        return
    raise ImportError(
        "CIF import requires the optional 'pymatgen' dependency.")


class DiffractionLine:
    """
    Class that defines a single diffraction line.
    Attributes:
      dsp0: d-spacing at reference conditions (P = 0 GPa and T = 300 K)
      dsp:  d-spacing at high PT
      intensity:  diffraction intensity read from the file
      h, k, l: Miller index
    """

    def __init__(self):
        self.dsp0 = 0.
        self.dsp = 0.
        self.intensity = 0.
        self.h = 0.
        self.k = 0.
        self.l = 0.
        self.label = ''
        self.pressure_coeffs = []
        self.temperature_coeffs = []


class UnitCell(object):
    """
    Class that defines a unit cell properties
    """

    def __init__(self):
        self.name = ''
        self.a = 0.
        self.b = 0.
        self.c = 0.
        self.alpha = 0.
        self.beta = 0.
        self.gamma = 0.
        self.v = 0.
        self.DiffLines = []
        self.symmetry = ''
        self.color = ''
        self.display = True

    def cal_dsp(self):
        """
        b_a and c_a are newly included for adjusting axial ratios.
        For cubic structure, these two inputs are ignored.
        For tetragonal and hexagonal, only c_a will be used.
        """
#            a = self.a; b = self.b; c = self.c;
#            alpha = self.alpha; beta = self.beta; gamma = self.gamma
        self.v = cal_UnitCellVolume(self.symmetry, self.a, self.b, self.c,
                                    self.alpha, self.beta, self.gamma)
        for dl in self.DiffLines:
            dsp = cal_dspacing(self.symmetry, dl.h, dl.k, dl.l,
                               self.a, self.b, self.c,
                               self.alpha, self.beta, self.gamma)
            dl.dsp = dsp

    def get_tthVSint(self, wavelength):
        """
        Returns twoth and intensity for bar plots in PyPeakPo
        Note that tth and intensity are numpy array not list
        If P, T, b_a, c_a have changed, run cal_dsp first for update
        """
#        self.cal_dsp(pressure, temperature, b_a, c_a)
        dsp = []
        intensity = []
        for line in self.DiffLines:
            d = line.dsp
            i = line.intensity
            dsp.append(d)
            intensity.append(i)
        tth = 2. * np.degrees(np.arcsin(wavelength / 2. / np.array(dsp)))
        return tth, np.array(intensity)

    def find_DiffLine(self, tth_c, wavelength):
        """
        Returns index of the cloest diffraction line, its difference
            in abs tth
        If P, T, b_a, c_a have changed, run cal_dsp first for update
        """
        tth, intensity = self.get_tthVSint(wavelength)

        idx = (np.abs(tth - tth_c)).argmin()

        return idx, abs(tth[idx] - tth_c), tth[idx]

    def make_TextOutput(self, pressure, temperature):
        if self.phase_kind == 'rapo':
            textout = self.name + '\n'
            textout += 'RAPO Raman mode list\n'
            textout += 'Below are the modes at {0: 6.2f} GPa, {1: 5.1f} K\n'.format(
                pressure, temperature)
            textout += 'mode, shift (cm-1), intensity\n'
            for dl in self.DiffLines:
                label = getattr(dl, 'label', 'mode')
                textout += '{0}, {1: .5f}, {2: .1f}\n'.format(
                    label, float(dl.dsp), float(dl.intensity))
            return textout
        textout = self.name + '\n'
        textout += self.symmetry + '\n'
        textout += '******************\n'
        textout += \
            'Values for Ucfit\n'
        textout += 'a = {0: .5f} A, b = {1: .5f} A, c = {2: .5f} A\n'.\
            format(float(self.a), float(self.b), float(self.c))
        textout += 'alpha = {0: .5f}, beta = {1: .5f}, gamma = {2: .5f}\n'.\
            format(self.alpha, self.beta, self.gamma)
        textout += 'b/a = {0: .5f}, c/a = {1: .5f}\n'.\
            format(self.b / self.a, self.c / self.a)
        textout += 'V = {0: .5f} A^3\n\n'.format(float(self.v))
        textout += 'Below are the peaks at {0: 6.1f} GPa, {1: 5.0f} K\n'.\
            format(pressure, temperature)
        textout += 'd-spacing (A), intensity (%), h, k, l\n'
        for dl in self.DiffLines:
            textout += \
                "{0: .5f}, {1: .1f}, {2: .0f}, {3: .0f}, {4: .0f}\n".\
                format(float(dl.dsp), dl.intensity, dl.h, dl.k, dl.l)
        return textout


class JCPDS(object):
    """
    Class that defines a single JCPDS card.
    see __init__ for the involved parameters
    """

    def __init__(self, filename=None):
        self.phase_kind = 'jcpds'
        self.reference_pressure = 0.0
        self.reference_temperature = 300.0
        if filename is None:
            self.file = ' '
            self.name = ' '
            self.version = 0
            self.comments = []
            self.symmetry = ''
            self.k0 = 0.
            self.k0p = 0.  # k0p at 298K
            self.thermal_expansion = 0.  # alphat at 298K
            self.a0 = 0.
            self.b0 = 0.
            self.c0 = 0.
            self.alpha0 = 0.
            self.beta0 = 0.
            self.gamma0 = 0.
            self.v0 = 0.
            self.DiffLines = []
        else:
            self.file = filename
            self.read_file(self.file)
        self.a = 0.
        self.b = 0.
        self.c = 0.
        self.alpha = 0.
        self.beta = 0.
        self.gamma = 0.
        self.v = 0.
        self._cache_cal_dsp_key = None
        self._cache_tth = {}
        self._cache_hkl_key = None
        self._cache_hkl_text = None

    def _invalidate_plot_cache(self):
        self._cache_cal_dsp_key = None
        self._cache_tth = {}
        self._cache_hkl_key = None
        self._cache_hkl_text = None

    def read_file(self, file):
        """
        read a jcpds file
        """
        self.file = file
        self._invalidate_plot_cache()
        self.phase_kind = 'jcpds'
        # Construct base name = file without path and without extension
        name = os.path.splitext(os.path.basename(self.file))[0]
        self.name = name
        self.comments = []
        self.DiffLines = []
        if os.path.splitext(str(file))[1].lower() == '.rapo':
            self._read_rapo(file)
            return
        # Prefer PeakPo-style parser first; if it fails, fallback to Dioptas.
        try:
            self._read_peakpo_style(file)
        except Exception:
            self._read_dioptas_style(file)

        self._cal_v0()
        self.v = self.v0
        self.a = self.a0
        self.b = self.b0
        self.c = self.c0
        self.alpha = self.alpha0
        self.beta = self.beta0
        self.gamma = self.gamma0

    def _read_rapo(self, file):
        with open(file, 'r', encoding='utf-8') as f:
            payload = json.load(f)
        self.phase_kind = 'rapo'
        self.file = file
        self.name = str(payload.get('name') or os.path.splitext(os.path.basename(file))[0])
        self.version = int(payload.get('version', 1))
        self.comments = str(payload.get('comments', ''))
        self.symmetry = 'nosymmetry'
        self.k0 = 0.0
        self.k0p = 0.0
        self.thermal_expansion = 0.0
        self.a0 = 1.0
        self.b0 = 1.0
        self.c0 = 1.0
        self.alpha0 = 90.0
        self.beta0 = 90.0
        self.gamma0 = 90.0
        self.v0 = 1.0
        self.reference_pressure = float(payload.get('reference_pressure', 0.0))
        self.reference_temperature = float(payload.get('reference_temperature', 300.0))
        self.DiffLines = []
        for idx, mode in enumerate(payload.get('modes', [])):
            line = DiffractionLine()
            line.label = str(mode.get('name', f'mode_{idx + 1}'))
            line.dsp0 = float(mode.get('shift0', mode.get('shift', 0.0)))
            line.dsp = line.dsp0
            line.intensity = float(mode.get('intensity', 100.0))
            line.h = idx + 1
            line.pressure_coeffs = [
                float(val) for val in mode.get('pressure_coeffs', mode.get('pressure', []))
            ]
            line.temperature_coeffs = [
                float(val) for val in mode.get('temperature_coeffs', mode.get('temperature', []))
            ]
            self.DiffLines.append(line)
        self.v = self.v0
        self.a = self.a0
        self.b = self.b0
        self.c = self.c0
        self.alpha = self.alpha0
        self.beta = self.beta0
        self.gamma = self.gamma0

    def _make_cal_dsp_key(self, pressure, temperature, b_a, c_a, use_table_for_0GPa):
        return (
            float(pressure),
            float(temperature),
            None if b_a is None else float(b_a),
            None if c_a is None else float(c_a),
            bool(use_table_for_0GPa),
            str(self.symmetry),
            float(self.k0),
            float(self.k0p),
            float(self.v0),
            float(self.thermal_expansion),
            float(self.a0),
            float(self.b0),
            float(self.c0),
            float(self.alpha0),
            float(self.beta0),
            float(self.gamma0),
            len(self.DiffLines),
        )

    def _read_peakpo_style(self, file):
        inp = open(file, 'r').readlines()
        self.version = int(inp[0])  # JCPDS version number
        header = inp[1]  # header
        self.comments = header

        item = str.split(inp[2])
        crystal_system = int(item[0])
        if crystal_system == 1:
            self.symmetry = 'cubic'
        elif crystal_system == 2:
            self.symmetry = 'hexagonal'
        elif crystal_system == 3:
            self.symmetry = 'tetragonal'
        elif crystal_system == 4:
            self.symmetry = 'orthorhombic'
        elif crystal_system == 5:
            self.symmetry = 'monoclinic'
        elif crystal_system == 6:
            self.symmetry = 'triclinic'
        elif crystal_system == 7:
            self.symmetry = 'nosymmetry'
        # 1 cubic, 2 hexagonal, 3 tetragonal, 4 orthorhombic
        # 5 monoclinic, 6 triclinic, 7 no nosymmetry P, d-sp input

        self.k0 = float(item[1])
        self.k0p = float(item[2])

        item = str.split(inp[3])  # line for unit-cell parameters

        if crystal_system == 1:  # cubic
            a = float(item[0])
            b = a
            c = a
            alpha = 90.
            beta = 90.
            gamma = 90.
        elif crystal_system == 7:  # P, d-sp input
            a = float(item[0])
            b = a
            c = a
            alpha = 90.
            beta = 90.
            gamma = 90.
        elif crystal_system == 2:  # hexagonal
            a = float(item[0])
            c = float(item[1])
            b = a
            alpha = 90.
            beta = 90.
            gamma = 120.
        elif crystal_system == 3:  # tetragonal
            a = float(item[0])
            c = float(item[1])
            b = a
            alpha = 90.
            beta = 90.
            gamma = 90.
        elif crystal_system == 4:  # orthorhombic
            a = float(item[0])
            b = float(item[1])
            c = float(item[2])
            alpha = 90.
            beta = 90.
            gamma = 90.
        elif crystal_system == 5:  # monoclinic
            a = float(item[0])
            b = float(item[1])
            c = float(item[2])
            beta = float(item[3])
            alpha = 90.
            gamma = 90.
        elif crystal_system == 6:  # triclinic
            a = float(item[0])
            b = float(item[1])
            c = float(item[2])
            alpha = float(item[3])
            beta = float(item[4])
            gamma = float(item[5])

        self.a0 = a
        self.b0 = b
        self.c0 = c
        self.alpha0 = alpha
        self.beta0 = beta
        self.gamma0 = gamma

        item = str.split(inp[4])
        if self.version == 3:
            self.thermal_expansion = 0.
        else:
            self.thermal_expansion = float(item[0])

        for line in inp[6:]:
            item = str.split(line)
            if len(item) != 5:
                break
            diff_line = DiffractionLine()
            diff_line.dsp0 = float(item[0])
            diff_line.intensity = float(item[1])
            diff_line.h = float(item[2])
            diff_line.k = float(item[3])
            diff_line.l = float(item[4])
            self.DiffLines.append(diff_line)

    def _read_dioptas_style(self, file):
        # Parse Dioptas/JCPDSTools keyword format robustly:
        # "KEY: value" with flexible spaces/tabs.
        comments = []
        symmetry_upper = ''

        with open(file, 'r') as fp:
            for raw_line in fp:
                line = raw_line.strip()
                if (line == '') or line.startswith('#') or (':' not in line):
                    continue
                tag, value = line.split(':', 1)
                tag = tag.strip().upper()
                value = value.strip()

                if tag == 'VERSION':
                    try:
                        self.version = int(float(value))
                    except Exception:
                        self.version = 4
                elif tag == 'COMMENT':
                    comments.append(value)
                elif tag == 'K0':
                    self.k0 = float(value)
                elif tag == 'K0P':
                    self.k0p = float(value)
                elif tag == 'SYMMETRY':
                    symmetry_upper = value.upper()
                elif tag == 'A':
                    self.a0 = float(value)
                elif tag == 'B':
                    self.b0 = float(value)
                elif tag == 'C':
                    self.c0 = float(value)
                elif tag == 'ALPHA':
                    self.alpha0 = float(value)
                elif tag == 'BETA':
                    self.beta0 = float(value)
                elif tag == 'GAMMA':
                    self.gamma0 = float(value)
                elif tag == 'VOLUME':
                    self.v0 = float(value)
                elif tag == 'ALPHAT':
                    self.thermal_expansion = float(value)
                elif tag == 'DIHKL':
                    nums = re.findall(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?', value)
                    if len(nums) < 5:
                        continue
                    diff_line = DiffractionLine()
                    diff_line.dsp0 = float(nums[0])
                    diff_line.dsp = diff_line.dsp0
                    diff_line.intensity = float(nums[1])
                    diff_line.h = float(nums[2])
                    diff_line.k = float(nums[3])
                    diff_line.l = float(nums[4])
                    self.DiffLines.append(diff_line)

        if self.version == 0:
            raise ValueError("Not a Dioptas-style JCPDS file")

        self.comments = comments[0] if len(comments) > 0 else ''
        self.symmetry = symmetry_upper.lower()

        # Fill symmetry-dependent defaults for missing cell parameters.
        if self.symmetry == 'cubic':
            self.b0 = self.a0
            self.c0 = self.a0
            self.alpha0 = 90.
            self.beta0 = 90.
            self.gamma0 = 90.
        elif self.symmetry == 'tetragonal':
            self.b0 = self.a0
            self.alpha0 = 90.
            self.beta0 = 90.
            self.gamma0 = 90.
        elif self.symmetry == 'orthorhombic':
            self.alpha0 = 90.
            self.beta0 = 90.
            self.gamma0 = 90.
        elif self.symmetry in ('hexagonal', 'trigonal'):
            self.b0 = self.a0
            self.alpha0 = 90.
            self.beta0 = 90.
            self.gamma0 = 120.
            # PeakPo uses hexagonal equations for trigonal.
            self.symmetry = 'hexagonal'
        elif self.symmetry == 'monoclinic':
            self.alpha0 = 90.
            self.gamma0 = 90.

        if self.v0 == 0.:
            self.v0 = cal_UnitCellVolume(
                self.symmetry, self.a0, self.b0, self.c0,
                self.alpha0, self.beta0, self.gamma0
            )

    def _cal_v0(self):
        """
        Computes the unit cell volume of the material at zero pressure and
        temperature from the unit cell parameters.
        """
        self.v0 = cal_UnitCellVolume(self.symmetry,
                                     self.a0, self.b0, self.c0,
                                     self.alpha0, self.beta0, self.gamma0)

    def _cal_v(self, pressure=0., temperature=300.):
        """
        Calculate volume at high pressure
        """
        pressure_st = pressure - self.thermal_expansion * \
            self.k0 * (temperature - 300.)
        if pressure == 0.0:  # p = 0 GPa
            self.v = self.v0
        else:
            if self.symmetry == 'nosymmetry':
                self.v = self.v0
            else:
                # print(pressure_st, self.v0, self.k0, self.k0p)
                try:
                    v_temp = bm3_v(pressure_st, self.v0, self.k0, self.k0p,
                                min_strain=0.3)
                except:
                    n_pts = 110
                    v_min = self.v0*0.1
                    v_calib = np.linspace(self.v0, v_min, n_pts)
                    p_calib = bm3_p(v_calib, self.v0, self.k0, self.k0p)
                    bm3s_v = interpolate.InterpolatedUnivariateSpline(p_calib, v_calib)
                    v_temp = bm3s_v(pressure) 
                    del_p = pressure - bm3_p(v_temp, self.v0, self.k0, self.k0p)
                    print(str(datetime.datetime.now())[:-7], 
                        ': Spline used for JCPDS calculation, residue = {0:.3e}'.format(del_p))
                self.v = v_temp

    def cal_dsp(self, pressure=0., temperature=300.,
                b_a=None, c_a=None, use_table_for_0GPa=True):
        """
        b_a and c_a are newly included for adjusting axial ratios.
        For cubic structure, these two inputs are ignored.
        For tetragonal and hexagonal, only c_a will be used.

        recalculate_zero = False: use the table d-spacing value for 0 GPa
        """

        cal_key = self._make_cal_dsp_key(
            pressure, temperature, b_a, c_a, use_table_for_0GPa)
        if cal_key == self._cache_cal_dsp_key:
            return
        if self.phase_kind == 'rapo':
            dp = float(pressure) - float(self.reference_pressure)
            dt = float(temperature) - float(self.reference_temperature)
            for dl in self.DiffLines:
                shift = float(dl.dsp0)
                for order, coeff in enumerate(getattr(dl, 'pressure_coeffs', []), start=1):
                    shift += float(coeff) * (dp ** order)
                for order, coeff in enumerate(getattr(dl, 'temperature_coeffs', []), start=1):
                    shift += float(coeff) * (dt ** order)
                dl.dsp = shift
            self._cache_cal_dsp_key = cal_key
            self._cache_tth = {}
            return
        # angles are always set to original values, so no need to reset
        # self.alpha = self.alpha0;
        # self.beta = self.beta0; self.gamma = self.gamma0
        self._cal_v(pressure, temperature)
        if b_a is None:
            b_a = self.b0 / self.a0
        if c_a is None:
            c_a = self.c0 / self.a0

        if (((pressure == 0.0) and (temperature == 300.) and (use_table_for_0GPa))
                or (self.symmetry == 'nosymmetry')):
            # p = 0 GPa, resetting to uc0 is necessary
            self.v = self.v0
            self.a = self.a0
            self.b = self.b0
            self.c = self.c0
            for dl in self.DiffLines:
                dsp = dl.dsp0
                dl.dsp = dsp
#            DLines = self.get_DiffractionLines()
#            for dl in DLines:
#                dsp = dl.dsp0
#                dl.dsp = dsp
        else:
            self._cal_UCPatPT(b_a, c_a)
            DLines = self.get_DiffractionLines()
#            a = self.a; b = self.b; c = self.c;
#            alpha = self.alpha; beta = self.beta; gamma = self.gamma
            for dl in DLines:
                dsp = cal_dspacing(self.symmetry, dl.h, dl.k, dl.l,
                                   self.a, self.b, self.c,
                                   self.alpha, self.beta, self.gamma)
                dl.dsp = dsp
            # the code worked without the following line, perhaps
            # due to the referencing or copy issue in python list
            self.DiffLines = DLines[:]
        """
        elif ((pressure == 0.0) and (temperature == 300.)) and (not use_table_for_0GPa):
            self._cal_UCPatPT(b_a, c_a)
            DLines = self.get_DiffractionLines()
#            a = self.a; b = self.b; c = self.c;
#            alpha = self.alpha; beta = self.beta; gamma = self.gamma
            for dl in DLines:
                dsp = cal_dspacing(self.symmetry, dl.h, dl.k, dl.l,
                                   self.a0, self.b0, self.c0,
                                   self.alpha0, self.beta0, self.gamma0)
                dl.dsp = dsp
            # the code worked without the following line, perhaps
            # due to the referencing or copy issue in python list
            self.DiffLines = DLines[:]
        """
        self._cache_cal_dsp_key = cal_key
        self._cache_tth = {}

    def get_DiffractionLines(self):
        """
        Returns the information for each reflection for the material.
        This information is an array of elements of class jcpds_reflection
        """
        return self.DiffLines

    def get_tthVSint(self, wavelength):
        """
        Returns twoth and intensity for bar plots in PyPeakPo
        Note that tth and intensity are numpy array not list
        If P, T, b_a, c_a have changed, run cal_dsp first for update
        """
#        self.cal_dsp(pressure, temperature, b_a, c_a)
        cache_key = (float(wavelength), self._cache_cal_dsp_key, len(self.DiffLines))
        cached = self._cache_tth.get(cache_key)
        if cached is not None:
            return cached
        DLines = self.get_DiffractionLines()
        if self.phase_kind == 'rapo':
            shift = np.fromiter((float(line.dsp) for line in DLines), dtype=float, count=len(DLines))
            intensity = np.fromiter((float(line.intensity) for line in DLines), dtype=float, count=len(DLines))
            out = (shift, intensity)
            self._cache_tth[cache_key] = out
            return out
        dsp = np.fromiter((float(line.dsp) for line in DLines), dtype=float, count=len(DLines))
        intensity = np.fromiter((float(line.intensity) for line in DLines), dtype=float, count=len(DLines))
        reciprocal_d = wavelength / 2. / dsp
        with np.errstate(invalid="ignore"):
            tth = 2. * np.degrees(np.arcsin(reciprocal_d))
        if np.any(reciprocal_d > 1.0):
            print(str(datetime.datetime.now())[:-7], 
                ": Ignore arcsin warning. Some dsp in jcpds are too small.")
        out = (tth, intensity)
        self._cache_tth[cache_key] = out
        return out

    def get_hkl_in_text(self):
        hkl_key = tuple((line.h, line.k, line.l) for line in self.DiffLines)
        if hkl_key == self._cache_hkl_key and self._cache_hkl_text is not None:
            return self._cache_hkl_text
        hkl = []
        for line in self.DiffLines:
            hkl.append("{0:.0f} {1:.0f} {2:.0f}".format(line.h, line.k, line.l))
        self._cache_hkl_key = hkl_key
        self._cache_hkl_text = hkl
        return hkl

    def find_DiffLine(self, tth_c, wavelength):
        """
        Returns index of the cloest diffraction line, its difference
            in abs tth
        If P, T, b_a, c_a have changed, run cal_dsp first for update
        """
        tth, intensity = self.get_tthVSint(wavelength)

        idx = (np.abs(tth - tth_c)).argmin()

        return idx, abs(tth[idx] - tth_c), tth[idx]

    def _cal_UCPatPT(self, b_a, c_a):
        if self.symmetry == 'cubic':
            self.a = (self.v)**(1. / 3.)
            self.b = self.a
            self.c = self.a
        elif (self.symmetry == 'hexagonal') or (self.symmetry == 'trigonal'):
            # self.a = (2. * self.v / (np.sqrt(3.)*self.c0/self.a0) )**(1./3.)
            self.a = (2. * self.v / (np.sqrt(3.) * c_a))**(1. / 3.)
            self.b = self.a
            self.c = self.a * c_a
        elif self.symmetry == 'tetragonal':
            # self.a = (self.v/(self.c0/self.a0))**(1./3.) ; self.b = self.a
            self.a = (self.v / (c_a))**(1. / 3.)
            self.b = self.a
            self.c = c_a * self.a
        elif self.symmetry == 'orthorhombic':
            # self.a = (self.v/(self.b0/self.a0*self.c0/self.a0))**(1./3.)
            self.a = (self.v / (b_a * c_a))**(1. / 3.)
            self.c = c_a * self.a
            self.b = b_a * self.a
        elif self.symmetry == 'monoclinic':
            # self.a = ( self.v / (self.b0/self.a0*self.c0/self.a0*\
            #        np.sin(np.radians(self.beta0))))**(1./3.)
            self.a = (self.v / (b_a * c_a *
                                np.sin(np.radians(self.beta0))))**(1. / 3.)
            self.c = c_a * self.a
            self.b = b_a * self.a
        elif self.symmetry == 'triclinic':
            a_term = np.sqrt(1. - (np.cos(np.radians(self.alpha0)))**2. -
                             (np.cos(np.radians(self.beta0)))**2. -
                             (np.cos(np.radians(self.gamma0)))**2. +
                             2. * np.cos(np.radians(self.alpha0)) *
                             np.cos(np.radians(self.beta0)) *
                             np.cos(np.radians(self.gamma0)))
            # self.a = (self.v/(self.b0/self.a0*self.c0/self.a0*a_term))
            # **(1./3.)
            self.a = (self.v / (b_a * c_a * a_term))**(1. / 3.)
            self.c = c_a * self.a
            self.b = b_a * self.a
        else:
            print(str(datetime.datetime.now())[:-7], 
                ': No symmetry is given')

#    return {'ver': ver, 'header': header, 'crystal_system': crystal_system, \
#            'K0': K0, 'K0p': K0p, 'u_param':
#               np.asarray([a,b,c,alpha,beta,gamma]), \
#            'alpha': thermal_expansion, 'peaks':
#           np.asarray([dsp, intensity, h, k, l]).T, \
#            'b_a': b_a, 'c_a': c_a}
    def set_from_cif(self, fn_cif, k0, k0p, file=None, name='', int_min=0.1,
                     version=4, comments='', thermal_expansion=0.,
                     two_theta_range=(0., 30.)):
        """
        define a JCPDS from CIF file.  made for jupyter notebook

        Parameters
        ----------
        fn_cif = file name and path of cif
        k0 = bulk modulus
        k0p = pressure derivative of k0
        file = name of file, optional (default = None)
        name = name of JCPDS, optional (default = '')
        version = version for JCPDS format, optional (default = 4)
        comments = comments for JCPDS file, optional (default = '')
        thermal_expansion = thermal expansion, optional (default = 0.)

        Returns
        -------

        """
        _require_pymatgen_for_cif()

        mg_version = mg.__version__
        mg_version_split = mg_version.split('.')
        d_current_version = datetime.datetime(int(mg_version_split[0]),
                                              int(mg_version_split[1]),
                                              int(mg_version_split[2]))
        d_work = datetime.datetime(2019, 4, 11)

        if d_current_version < d_work:
            print(str(datetime.datetime.now())[:-7], 
                ': Update your pymatgen newer than 2019.4.11.')
            return False

        structure = mg.Structure.from_file(fn_cif)
        crystal_system = SpacegroupAnalyzer(structure).get_crystal_system()
        if crystal_system == 'triclinic':
            print(str(datetime.datetime.now())[:-7], 
                ': Your cif has triclinic symmetry.  PeakPo cannot currently handle this.')
            return False
        self.set_from_pymatgen(structure, k0, k0p, file=file,
                               name=name, version=version,
                               comments=comments, int_min=int_min,
                               thermal_expansion=thermal_expansion)
        return True

    def set_from_pymatgen(self, structure, k0, k0p, file=None,
                          name='', version=4, int_min=0.1,
                          comments='', thermal_expansion=0.,
                          two_theta_range=(0., 30.)):
        """
        set parameters from pymatgen outputs

        Parameters
        ----------
        structure = pymatgen structure object
        k0 = bulk modulus
        k0p = pressure derivative of bulk modulus
        file = file name (optional)
        name = name (optional)
        version = version (optional)
        comments = comments (optional)
        thermal_expansion = 0 (optional)

        Returns
        -------

        """
        lattice = structure.lattice
        self.k0 = k0
        self.k0p = k0p
        self.a0 = float(lattice.a)
        self.b0 = float(lattice.b)
        self.c0 = float(lattice.c)
        self.alpha0 = float(lattice.alpha)
        self.beta0 = float(lattice.beta)
        self.gamma0 = float(lattice.gamma)
        symm = str(SpacegroupAnalyzer(structure).get_crystal_system())
        if symm == 'trigonal':
            self.symmetry = 'hexagonal'
        else:
            self.symmetry = symm
        self.file = file
        self.name = name
        self.version = version
        self.comments = comments
        self.thermal_expansion = thermal_expansion
        self.v0 = cal_UnitCellVolume(self.symmetry,
                                     self.a0, self.b0, self.c0,
                                     self.alpha0, self.beta0,
                                     self.gamma0)
        c = XRDCalculator(wavelength=0.3344)
        pattern = c.get_pattern(structure, two_theta_range=two_theta_range)
        h = []
        k = []
        l = []
        for i in range(pattern.hkls.__len__()):
            h.append(pattern.hkls[i][0]['hkl'][0])
            k.append(pattern.hkls[i][0]['hkl'][1])
            l.append(pattern.hkls[i][0]['hkl'][-1])
        d_lines = np.transpose([pattern.x, pattern.d_hkls, pattern.y, h, k, l])
        DiffLines = []
        for line in d_lines:
            if float(line[2]) >= int_min:
                d_line = DiffractionLine()
                d_line.dsp0 = float(line[1])
                #d_line.dsp = float(line[1])
                d_line.intensity = float(line[2])
                d_line.h = float(line[3])
                d_line.k = float(line[4])
                d_line.l = float(line[5])
                DiffLines.append(d_line)
        self.DiffLines = DiffLines

    def write_to_string(self, comments=" ",
                      calculate_1bar_table = False,
                      int_min=0., dsp_min=0.):

        textoutput = "{:d}\n".format(self.version)
        textoutput += comments + "\n"

        # 1 cubic, 2 hexagonal, 3 tetragonal, 4 orthorhombic
        # 5 monoclinic, 6 triclinic, 7 nosymmetry P, d-sp input

        str_el = "{0:.2f}   {1:.2f}".format(self.k0, self.k0p)

        if self.symmetry == 'cubic':  # cubic
            crystal_system = '1   '
            str_uc = "{0:.5f}".format(self.a0)
        elif self.symmetry == 'nosymmetry':  # P, d-sp input
            crystal_system = '7   '
            str_uc = "{0:.5f}".format(self.a0)
        elif self.symmetry == 'hexagonal' or self.symmetry == 'trigonal':
            crystal_system = '2   '
            str_uc = "{0:.5f}   {1:.5f}".format(self.a0, self.c0)
        elif self.symmetry == 'tetragonal':  # tetragonal
            crystal_system = '3   '
            str_uc = "{0:.5f}   {1:.5f}".format(self.a0, self.c0)
        elif self.symmetry == 'orthorhombic':  # orthorhombic
            crystal_system = '4   '
            str_uc = "{0:.5f}   {1:.5f}   {2:.5f}".format(self.a0,
                                                      self.b0, self.c0)
        elif self.symmetry == 'monoclinic':  # monoclinic
            crystal_system = '5   '
            str_uc = "{0:.5f}   {1:.5f}   {2:.5f}   {3:.5f}".format(
                self.a0, self.b0, self.c0, self.beta0)
        elif self.symmetry == 'triclinic':  # triclinic
            crystal_system = '6   '
            str_uc = "{0:.5f}   {1:.5f}   {2:.5f}   {3:.5f}   {4:.5f} \
                {5:.5f}".format(
                self.a0, self.b0, self.c0,
                self.alpha0, self.beta0, self.gamma0)

        textoutput += (crystal_system + str_el + " \n")
        textoutput += str_uc + " \n"
        textoutput += "{:.4e} \n".format(self.thermal_expansion)
        textoutput += "d-spacing    I/I0     h   k   l \n"

        if calculate_1bar_table:
            self.cal_dsp(use_table_for_0GPa=False)
            for line in self.DiffLines:
                if (line.dsp > dsp_min) and (line.intensity > int_min):
                    textoutput += \
                        "{0:.6f}   {1:.2f}   {2:.1f}   {3:.1f}   {4:.1f} \n".format(
                        line.dsp, line.intensity, line.h, line.k, line.l)
        else:
            for line in self.DiffLines:
                if (line.dsp0 > dsp_min) and (line.intensity > int_min):
                    textoutput += \
                        "{0:.6f}   {1:.2f}   {2:.1f}   {3:.1f}   {4:.1f} \n".format(
                        line.dsp0, line.intensity, line.h, line.k, line.l)
        return textoutput


    def write_to_file(self, filename, comments=" ",
                      calculate_1bar_table = False,
                      int_min=0., dsp_min=0.):
        """
        write a JCPDS file

        Parameters
        ----------
        filename = path and name of file

        Returns
        -------

        """
        textoutput = self.write_to_string(comments=comments,
                          calculate_1bar_table = calculate_1bar_table,
                          int_min=int_min, dsp_min=dsp_min)
        f = open(filename, 'w')
        f.write(textoutput)
        f.close()

    def write_to_dioptas_jcpds(self, filename, int_min=0., dsp_min=0.):
        jcpds_dioptas = jcpds()
        jcpds_dioptas.params['comments'].append(self.comments)
        jcpds_dioptas.params['symmetry'] = self.symmetry
        jcpds_dioptas.params['k0'] = self.k0
        jcpds_dioptas.params['k0p0'] = self.k0p
        jcpds_dioptas.params['alpha_t0'] = self.thermal_expansion
        jcpds_dioptas.params['a0'] = self.a0
        jcpds_dioptas.params['b0'] = self.b0
        jcpds_dioptas.params['c0'] = self.c0
        jcpds_dioptas.params['alpha0'] = self.alpha0
        jcpds_dioptas.params['beta0'] = self.beta0
        jcpds_dioptas.params['gamma0'] = self.gamma0
        jcpds_dioptas.params['v0'] = self.v0

        reflections = []

        for line in self.DiffLines:
            if (line.dsp0 > dsp_min) and (line.intensity > int_min):
                reflection = jcpds_reflection()
                reflection.d0 = line.dsp0
                reflection.intensity = line.intensity
                reflection.h = line.h
                reflection.k = line.k
                reflection.l = line.l
                reflections.append(reflection)
        jcpds_dioptas.reflections = reflections

        jcpds_dioptas.save_file(filename)


class Session(object):
    '''
    Developed for PeakPo and shared with PeakFt
    From 2015/3/15, PeakPo save using this class
    '''

    def __init__(self):
        self.pattern = None  # linked spectrum/pattern object
        self.waterfallpatterns = []  # list of pattern object
        self.wavelength = 0.0  # normal value at GSECARS
        self.pressure = 0.0  # DONOT CHANGE, matches with Qt Designer file
        self.temperature = 300.  # DONOT CHANGE, matches with Qt Designer file
        self.jlist = []  # list for JCPDS
        self.bg_roi = [4., 16.]
        self.bg_params = [20, 10, 20]
        self.jcpds_path = ''  # redundant, but easier to code
        self.chi_path = ''  # reduendant, but easier to code


class JCPDSplt(JCPDS):
    """
    Define new JPCDS for plot.  Add two attributes, color and show switch
    Developed for supporting PeakPo but used and shared with PeakFt
    """

    def __init__(self):
        ''' _org parameter needed : self.k0, self.k0p,
        self.thermal_expansion, self.v0
        '''
        super(JCPDSplt, self).__init__()
        self.color = ''
        self.display = True
        self.maxint = 1.0
        self.twk_b_a = 1.0
        self.twk_c_a = 1.0
        self.twk_v0 = 1.0
        self.twk_k0 = 1.0
        self.twk_k0p = 1.0
        self.twk_thermal_expansion = 1.0
        self.twk_int = 1.0

    def _sync_org_from_current(self):
        """Cache untweaked baseline values after any load/set operation."""
        self.k0_org = self.k0
        self.k0p_org = self.k0p
        self.v0_org = self.v0
        self.thermal_expansion_org = self.thermal_expansion

    def read_file(self, file):
        '''
        *_twk are tweaked parameters, and twk_* are tweaking coefficients
        a0, b0, c0, alpha, beta, gamma, v0 should not be tweaked
        '''
        super(JCPDSplt, self).read_file(file)
        self._sync_org_from_current()

    def set_from_cif(self, fn_cif, k0, k0p, file=None, name='', version=4,
                     comments='', int_min=0.1, thermal_expansion=0.):
        """
        Ensure CIF-created cards also initialize *_org tweak baselines.
        """
        success = super(JCPDSplt, self).set_from_cif(
            fn_cif, k0, k0p, file=file, name=name, version=version,
            comments=comments, int_min=int_min,
            thermal_expansion=thermal_expansion
        )
        if success:
            self._sync_org_from_current()
        return success

    def cal_dsp(self, pressure, temperature, b_a=None, c_a=None,
                use_table_for_0GPa=True):
        '''DiffLines are tweaked one unlike other notations'''
        if self.phase_kind == 'rapo':
            super(JCPDSplt, self).cal_dsp(
                pressure, temperature, b_a, c_a,
                use_table_for_0GPa=use_table_for_0GPa)
            return
        # set tweaked parameters
        self.k0 = self.k0_org * self.twk_k0
        self.k0p = self.k0p_org * self.twk_k0p
        self.v0 = self.v0_org * self.twk_v0
        self.thermal_expansion = self.thermal_expansion_org * \
            self.twk_thermal_expansion
        self.b_a = (self.b0 / self.a0) * self.twk_b_a
        self.c_a = (self.c0 / self.a0) * self.twk_c_a
        # get DiffLines
        super(JCPDSplt, self).cal_dsp(pressure, temperature,
                                      self.b_a, self.c_a,
                                      use_table_for_0GPa=use_table_for_0GPa)

    def get_dsp(self):
        dsp = []
        for dl in self.DiffLines:
            d = float(dl.dsp)
            dsp.append(d)
        return dsp

    def make_TextOutput(self, pressure, temperature):
        if self.phase_kind == 'rapo':
            textout = 'File from: ' + self.file + '\n'
            textout += 'Format: RAPO\n'
            textout += 'Comment from original file: ' + self.comments + '\n\n'
            textout += 'Mode, shift (cm-1), intensity\n'
            for dl in self.DiffLines:
                label = getattr(dl, 'label', 'mode')
                textout += ' {0}, {1:10.5f}, {2:10.1f}\n'.format(
                    label, float(dl.dsp), float(dl.intensity))
            return textout
        textout = 'File from: ' + self.file + '\n'
        textout += 'Version: ' + str(self.version) + '\n'
        textout += 'Comment from original file: ' + self.comments + '\n'
        textout += '\n'
        textout += 'Name: ' + self.name +'\n'
        textout += 'Crystal system: ' + self.symmetry + '\n'
        textout += '\n'
        textout += 'Values at high P-T after tweak \n'
        textout += ' a = {0:.5f} A, b = {1:.5f} A, c = {2:.5f} A\n'.\
            format(float(self.a), float(self.b), float(self.c))
        textout += ' alpha = {0:.2f}, beta = {1:.2f}, gamma = {2:.2f}\n'.\
            format(self.alpha, self.beta, self.gamma)
        textout += ' V = {0:.5f} A^3\n'.format(float(self.v))
        textout += '\n'
        if self.symmetry != 'nosymmetry':
            textout += 'Values at 1 bar and 300 K after tweak \n'
            a0_twk, b0_twk, c0_twk = get_cell_prm_twk(
                self.symmetry, self.v0, self.a0, self.b0, self.c0,
                self.alpha0, self.beta0, self.gamma0, self.twk_b_a, self.twk_c_a)
            textout += ' a0 = {0:.5f} A, b0 = {1:.5f} A, c0 = {2:.5f} A\n'.\
                format(float(a0_twk), float(b0_twk), float(c0_twk))
            textout += ' V0 = {0:.5f} A^3\n'.format(self.v0)  # v0 is tweaked value
            textout += ' K0 = {0:.1f} GPa, K0p = {1:.2f}, alpha = {2:.2e}\n'.\
                format(self.k0, self.k0p, self.thermal_expansion)
            textout += ' b/a = {0:.5f}, c/a = {1:.5f}\n'.\
                format(self.b / self.a, self.c / self.a)
            textout += ' Tweak for b/a = {0:.5f}, Tweak for c/a = {1:.5f}\n'.\
                format(self.twk_b_a, self.twk_c_a)
            textout += '\n'
            textout += 'Values in original JCPDS file\n'
            textout += ' a0 = {0:.5f} A, b0 = {1:.5f} A, c0 = {2:.5f} A\n'.\
                format(self.a0, self.b0, self.c0)
            textout += ' alpha0 = {0:.2f}, beta0 = {1:.2f}, gamma0 = {2:.2f}\n'.\
                format(self.alpha0, self.beta0, self.gamma0)
            textout += ' b0/a0 = {0:.5f}, c0/a0 = {1:.5f}\n'.\
                format(self.b0 / self.a0, self.c0 / self.a0)
            textout += ' V0 = {0:.5f} A^3\n'.format(self.v0_org)
            textout += ' K0 = {0:.1f} GPa, K0p = {1:.2f}, alpha = {2:.2e}\n'.\
                format(self.k0_org, self.k0p_org, self.thermal_expansion_org)
        textout += '\nBelow are the peaks at {0:6.2f} GPa, {1:5.0f} K\n'.\
            format(pressure, temperature)
        textout += ' d-spacing (A), intensity (%), h, k, l\n'
        for dl in self.DiffLines:
            textout += \
                " {0:10.5f}, {1:10.1f}, {2:5.0f}, {3:5.0f}, {4:5.0f}\n".\
                format(float(dl.dsp), dl.intensity, dl.h, dl.k, dl.l)

        return textout

    def write_to_twk_jcpds(self, filename, comments=" "):
        """
        write a twk JCPDS file

        Parameters
        ----------
        filename = path and name of file

        Returns
        -------

        """
        if self.phase_kind == 'rapo':
            payload = {
                'name': self.name,
                'version': self.version,
                'comments': comments,
                'reference_pressure': self.reference_pressure,
                'reference_temperature': self.reference_temperature,
                'modes': [],
            }
            for dl in self.DiffLines:
                payload['modes'].append({
                    'name': getattr(dl, 'label', 'mode'),
                    'shift0': float(dl.dsp0),
                    'intensity': float(dl.intensity),
                    'pressure_coeffs': list(getattr(dl, 'pressure_coeffs', [])),
                    'temperature_coeffs': list(getattr(dl, 'temperature_coeffs', [])),
                })
            with open(filename, 'w', encoding='utf-8') as f:
                json.dump(payload, f, indent=2)
            return
        f = open(filename, 'w')
        f.write("{:d}\n".format(self.version))
        f.write(comments + "\n")

        # 1 cubic, 2 hexagonal, 3 tetragonal, 4 orthorhombic
        # 5 monoclinic, 6 triclinic, 7 nosymmetry P, d-sp input

        str_el = "{0:.2f} {1:.2f}".format(self.k0, self.k0p)

        a0_twk, b0_twk, c0_twk = get_cell_prm_twk(
            self.symmetry, self.v0, self.a0, self.b0, self.c0,
            self.alpha0, self.beta0, self.gamma0, self.twk_b_a, self.twk_c_a)

        if self.symmetry == 'cubic':  # cubic
            crystal_system = '1 '
            str_uc = "{0:.5f}".format(a0_twk)
        elif self.symmetry == 'nosymmetry':  # P, d-sp input
            crystal_system = '7 '
            str_uc = "{0:.5f}".format(a0_twk)
        elif self.symmetry == 'hexagonal' or self.symmetry == 'trigonal':
            crystal_system = '2 '
            str_uc = "{0:.5f} {1:.5f}".format(a0_twk, c0_twk)
        elif self.symmetry == 'tetragonal':  # tetragonal
            crystal_system = '3 '
            str_uc = "{0:.5f} {1:.5f}".format(a0_twk, c0_twk)
        elif self.symmetry == 'orthorhombic':  # orthorhombic
            crystal_system = '4 '
            str_uc = "{0:.5f} {1:.5f} {2:.5f}".format(a0_twk,
                                                      b0_twk, c0_twk)
        elif self.symmetry == 'monoclinic':  # monoclinic
            crystal_system = '5 '
            str_uc = "{0:.5f} {1:.5f} {2:.5f} {3:.5f}".format(
                a0_twk, b0_twk, c0_twk, self.beta0)
        elif self.symmetry == 'triclinic':  # triclinic
            crystal_system = '6 '
            str_uc = "{0:.5f} {1:.5f} {2:.5f} {3:.5f} {4:.5f} \
                {5:.5f}".format(
                a0_twk, b0_twk, c0_twk,
                self.alpha0, self.beta0, self.gamma0)

        f.write(crystal_system + str_el + " \n")
        f.write(str_uc + " \n")
        f.write("{:.4e} \n".format(self.thermal_expansion))
        f.write("d-spacing    I/I0     h   k   l \n")

        self.cal_dsp(0., 300., use_table_for_0GPa=False)
        for line in self.DiffLines:
            f.write("{0:.6f} {1:.2f} {2:.1f} {3:.1f} {4:.1f} \n".format(
                line.dsp0, line.intensity, line.h, line.k, line.l))
        f.close()


def get_cell_prm_twk(symmetry, v_twk, a0, b0, c0, alpha0, beta0, gamma0,
                     twk_b_a, twk_c_a):
    b_a_twk = b0 / a0 * twk_b_a
    c_a_twk = c0 / a0 * twk_c_a
    if symmetry == 'cubic':
        a_twk = (v_twk)**(1. / 3.)
        b_twk = a_twk
        c_twk = a_twk
    elif (symmetry == 'hexagonal') or (symmetry == 'trigonal'):
        a_twk = (2. * v_twk / (np.sqrt(3.) * c_a_twk))**(1. / 3.)
        b_twk = a_twk
        c_twk = a_twk * c_a_twk
    elif symmetry == 'tetragonal':
        a_twk = (v_twk / (c_a_twk))**(1. / 3.)
        b_twk = a_twk
        c_twk = c_a_twk * a_twk
    elif symmetry == 'orthorhombic':
        a_twk = (v_twk / (b_a_twk * c_a_twk))**(1. / 3.)
        c_twk = c_a_twk * a_twk
        b_twk = b_a_twk * a_twk
    elif symmetry == 'monoclinic':
        a_twk = (v_twk / (b_a_twk * c_a_twk *
                          np.sin(np.radians(beta0))))**(1. / 3.)
        c_twk = c_a_twk * a_twk
        b_twk = b_a_twk * a_twk
    elif symmetry == 'triclinic':
        a_term = np.sqrt(1. - (np.cos(np.radians(alpha0)))**2. -
                         (np.cos(np.radians(beta0)))**2. -
                         (np.cos(np.radians(gamma0)))**2. +
                         2. * np.cos(np.radians(alpha0)) *
                         np.cos(np.radians(beta0)) *
                         np.cos(np.radians(gamma0)))
        a_twk = (v_twk / (b_a_twk * c_a_twk * a_term))**(1. / 3.)
        c_twk = c_a_twk * a_twk
        b_twk = b_a_twk * a_twk
    else:
        #print('no symmetry is given')
        a_twk = a0
        b_twk = b0
        c_twk = c0

    return a_twk, b_twk, c_twk
