"""
Module to implement validators
"""
import os.path
import numpy as np

class ValidatorError(Exception):
    pass

class Validator():
    def __init__(self):
        """Base class for all validator"""
        pass

    def validate(self):
        raise NotImplementedError

class ValidatorReferenceFile(Validator):
    def __init__(self, ref, actual, rtol=1e-05, atol=1e-08):
        """Validator to compare data against a reference output file

        Reference and actual values are loaded into numpy arrays and the function
        np.allclose is used to determine whether the values are close enough

        Parameters:
        ref :  string
               path to the reference data
        actual : string
                 name of the actual output file to compare data against
                 the file will be search for in the directory where the program was executed
        atol : float
               absolute tolerance parameters
        rtol : float
               relative tolerance parameters
        """
        self.ref = ref
        self.actual = actual
        self.rtol = rtol
        self.atol = atol

    def validate(self, cwd=None):
        """Compare output data against reference values

        Parameters
        ----------
        cwd : string
            path to current working directory

        Exceptions
        ----------
        raise ValidatorError if the data values are not equal
        """
        if cwd is not None:
            ref = os.path.join(cwd, self.ref)
            actual = os.path.join(cwd, self.actual)
        else:
            ref = self.ref
            actual = self.actual

        ref_data = np.loadtxt(ref)
        actual_data = np.loadtxt(actual)

        if not np.allclose(actual_data, ref_data, rtol=self.rtol, atol=self.atol):
            error_abs = np.abs(ref_data - actual_data)
            max_abs = error_abs.max()
            error_rel = error_abs / np.abs(ref_data)
            max_rel = error_rel.max()

            error_msg = """{ref:} and {actual:} are different.
Maximum absolute error: {max_abs:12.5e} (atol = {atol:12.5e})
Maximum relative error: {max_rel:12.5e} (rtol = {rtol:12.5e})
""".format(ref=self.ref, actual=self.actual, max_abs=max_abs, max_rel=max_rel, atol=self.atol, rtol=self.rtol)
            raise ValidatorError(error_msg)

class ValidatorNISTEnergy(Validator):
    def __init__(self, E_disp, E_LRC, E_real, E_fourier, E_self, E_intra, E_total, rtol=1e-05, atol=0.0):
        """Validator to compare data against reference NIST energy computation

        Reference and actual values are loaded into numpy arrays and the function
        np.allclose is used to determine whether the values are close enough

        Parameters:
        E_disp :  float
            Dispersion energy in K
        E_LRC :  float
            Dispersion long range energy correction in K
        E_real :  float
            Real space coulomb energy in K
        E_fourier : float
            Reciprocal space coulomb energy in K
        E_self : float
            Ewald self interaction energy correction in K
        E_intra : float
            Ewald reciprocal space intramolecular energy correction in K
        E_total : float
            Potential energy in K
        atol : float
               absolute tolerance parameters
        rtol : float
               relative tolerance parameters
        """
        boltzmann = 1.3806488e-23
        hartree2J = 4.35974417e-18
        nist2mw = boltzmann /hartree2J

        # Convert energy into mw units (Hartree)
        self.E_disp = E_disp * nist2mw
        self.E_LRC = E_LRC * nist2mw
        self.E_real = E_real * nist2mw
        self.E_fourier = E_fourier * nist2mw
        self.E_self = E_self * nist2mw
        self.E_intra = E_intra * nist2mw
        self.E_total = E_total * nist2mw

        self.atol = atol
        self.rtol = rtol

    def validate(self, cwd=None):
        """Compare output data against reference values

        Parameters
        ----------
        cwd : string
            path to current working directory

        Exceptions
        ----------
        raise ValidatorError if the data values are not equal
        """
        if cwd is not None:
            actual = os.path.join(cwd, "energies_breakdown.out")

        E_pot, E_lr, E_sr, E_self, E_intra, E_disp, E_lrc,  = np.loadtxt(actual, usecols=(3,8,10,11,12,17,18), unpack=True)

        values = {"fourier": (self.E_fourier, E_lr,),
                  "real": (self.E_real+self.E_intra, E_sr+E_intra,),
                  "self": (self.E_self, E_self),
                  "coulomb": (self.E_fourier+self.E_real+self.E_intra+self.E_self, E_lr+E_sr+E_intra+E_self),
                  # "disp":(self.E_disp, E_disp,),
                  # "total": (self.E_total, E_pot),
        }

        failure = False
        error_msg = []
        for key, val in values.items():
            ref_data = val[0]
            actual_data = val[1]
            if not np.allclose(actual_data, ref_data, rtol=self.rtol, atol=self.atol):
                error_abs = np.abs(ref_data - actual_data)
                max_abs = error_abs.max()
                error_rel = error_abs / np.abs(ref_data)
                max_rel = error_rel.max()

                error_msg.append("""{ref:} and {actual:} are different for {Etype:s} energy
absolute error: {max_abs:12.5e} (atol = {atol:12.5e})
relative error: {max_rel:12.5e} (rtol = {rtol:12.5e})
""".format(Etype=key, ref=ref_data, actual=actual_data, max_abs=max_abs, max_rel=max_rel, atol=self.atol, rtol=self.rtol))

                failure = True

        if failure:
            raise ValidatorError('\n'.join(error_msg))
