from .electronic_property_calculators import (
    ElectronicPropertyCalculator
)


class MOPACElectronicProperties(ElectronicPropertyCalculator):
    def __init__(self, mopac_path):
        self.mopac_path = mopac_path

    def dipole_moment(self, mol, conformer=-1):
        """
        Calculates the dipole moment using MOPAC.

        Note that this requires MOPAC to be installed and have a
        valid license.

        Parameters
        ----------
        settings : :class:`dict`, optional
            A dictionary which maps the names of the optimization
            parameters to their values. Valid values are:

                'hamiltonian' : :class:`str` (default = ``'PM7'``
                    A series of different methods can be selected:
                    PM7, PM6, AM1, CIS (CISD, CISDT), MNDO, RM1, etc..

                    PM7 is the latest version of the reparametrization
                    of NDDO theory, where all the atomic and diatomic
                    parameters were re-optimized / updated from PM6
                    [#]_.

                'eps' : :class:`float` (default = ``80.1``)
                    Sets the dielectric constant for the solvent.
                    Presence of this keyword will cause the COSMO
                    (Conductor-like Screening Model) method to be used
                    to approximate the effect of a solvent model
                    surrounding the molecule. Solvents with a low
                    dielectric constant are not likely to work well
                    with this model. ``0`` means that the dielectric
                    constant is not included in the calculation.
                    ``80.1`` can be used to model a water environment
                    at room temperature.

                'charge' : :class:`float` (default = ``0``)
                    The charge of the system.

                'timeout' : :class:`float` (default = ``172800``)
                    The amount in seconds the calculation is allowed to
                    run before being terminated. The default value is
                    ``2`` days or ``172,800`` seconds.

        mopac_path : :class:`str`
            The full path to the MOPAC installation.

        Returns
        -------
        :class:`float`
            The calculated dipole.

        References
        ----------
        .. [#] http://openmopac.net/PM7_accuracy/PM7_accuracy.html

        """

        if settings is None:
            settings = {}

        # Define default vals for the MOPAC input
        vals = {
                'hamiltonian': 'PM7',
                'method': 'NOOPT',
                'eps': 80.1,
                'charge': 0,
                'timeout': 172800,
                }

        vals.update(settings)

        # To prevent conflicts when running this function in parallel,
        # a temporary copy of the molecular structure file is made and
        # used for mopac calculations.

        # Unique file name is generated by inserting a random int into
        # the file path.
        tmp_file = "{}.mol".format(uuid4().int)
        self.molecule.write(tmp_file)

        file_root, ext = os.path.splitext(tmp_file)

        # Generate the input file
        _create_mop(file_root, self.molecule, vals)
        # Run MOPAC
        _run_mopac(file_root, mopac_path)
        return _extract_MOPAC_dipole(file_root)

    def electron_affinity(self, mol, conformer=-1):
        """
        Calculates the electron affinity using MOPAC.

        Note that this requires MOPAC to be installed and have a
        valid license.

        Parameters
        ----------
        settings : :class:`dict`, optional
            A dictionary which maps the names of the optimization
            parameters to their values. Valid values are:

                'hamiltonian' : :class:`str` (default = ``'PM7'``
                    A series of different methods can be selected:
                    PM7, PM6, AM1, CIS (CISD, CISDT), MNDO, RM1, etc..

                    PM7 is the latest version of the reparametrization
                    of NDDO theory, where all the atomic and diatomic
                    parameters were re-optimized / updated from PM6
                    [#]_.

                'eps' : :class:`float` (default = ``80.1``)
                    Sets the dielectric constant for the solvent.
                    Presence of this keyword will cause the COSMO
                    (Conductor-like Screening Model) method to be used
                    to approximate the effect of a solvent model
                    surrounding the molecule. Solvents with a low
                    dielectric constant are not likely to work well
                    with this model. ``0`` means that the dielectric
                    constant is not included in the calculation.
                    ``80.1`` can be used to model a water environment
                    at room temperature.

                'charge' : :class:`float` (default = ``0``)
                    The charge of the system.

                'timeout' : :class:`float` (default = ``172800``)
                    The amount in seconds the calculation is allowed to
                    run before being terminated. The default value is
                    ``2`` days or ``172,800`` seconds.

        mopac_path : :class:`str`
            The full path to the MOPAC installation.

        Returns
        -------
        :class:`float`
            The calculated energy.

        References
        ----------
        .. [#] http://openmopac.net/PM7_accuracy/PM7_accuracy.html

        """

        if settings is None:
            settings = {}

        # Define default vals for the MOPAC input
        vals = {
                'hamiltonian': 'PM7',
                'method': 'NOOPT',
                'eps': 80.1,
                'charge': 0,
                'timeout': 172800,
                }
        vals.update(settings)

        # First check the energy of the neutral system
        # To prevent conflicts when running this function in parallel,
        # a temporary copy of the molecular structure file is made and
        # used for mopac calculations.

        # Unique file name is generated by inserting a random int into
        # the file path.
        tmp_file = "{}.mol".format(uuid4().int)
        self.molecule.write(tmp_file)

        file_root, ext = os.path.splitext(tmp_file)

        # Generate the input file
        _create_mop(file_root, self.molecule, vals)
        # Run MOPAC
        _run_mopac(file_root, mopac_path)

        # Extract the neutral energy
        en1 = _extract_MOPAC_en(file_root)

        # Update the settings for the anion optimization
        settings2 = {
                    'method': 'OPT',
                    'gradient': 0.01,
                    'charge': -1,
                    'fileout': 'PDBOUT'
                    }

        vals.update(settings2)

        # Now generate a new molecule
        mol2 = copy.deepcopy(self.molecule)
        # Run the mopac optimization
        mopac_opt(mol2, mopac_path, vals)
        # Extract the energy by using the self.mopac method
        vals['method'] = 'NOOPT'
        del vals['gradient']
        del vals['fileout']
        en2 = mol2.energy.mopac(mopac_path, vals)
        # Calculate the EA (eV)
        return en2 - en1

    def ionization_potential(self, mol, conformer=-1):
        """
        Calculates the ionization potential using MOPAC.

        Note that this requires MOPAC to be installed and have a
        valid license.

        Parameters
        ----------
        settings : :class:`dict`, optional
            A dictionary which maps the names of the optimization
            parameters to their values. Valid values are:

                'hamiltonian' : :class:`str` (default = ``'PM7'``
                    A series of different methods can be selected:
                    PM7, PM6, AM1, CIS (CISD, CISDT), MNDO, RM1, etc..

                    PM7 is the latest version of the reparametrization
                    of NDDO theory, where all the atomic and diatomic
                    parameters were re-optimized / updated from PM6
                    [#]_.

                'eps' : :class:`float` (default = ``80.1``)
                    Sets the dielectric constant for the solvent.
                    Presence of this keyword will cause the COSMO
                    (Conductor-like Screening Model) method to be used
                    to approximate the effect of a solvent model
                    surrounding the molecule. Solvents with a low
                    dielectric constant are not likely to work well
                    with this model. ``0`` means that the dielectric
                    constant is not included in the calculation.
                    ``80.1`` can be used to model a water environment
                    at room temperature.

                'charge' : :class:`float` (default = ``0``)
                    The charge of the system.

                'timeout' : :class:`float` (default = ``172800``)
                    The amount in seconds the calculation is allowed to
                    run before being terminated. The default value is
                    ``2`` days or ``172,800`` seconds.

        mopac_path : :class:`str`
            The full path to the MOPAC installation.

        Returns
        -------
        :class:`float`
            The calculated energy.

        References
        ----------
        .. [#] http://openmopac.net/PM7_accuracy/PM7_accuracy.html

        """

        if settings is None:
            settings = {}

        # Define default vals for the MOPAC input
        vals = {
                'hamiltonian': 'PM7',
                'method': 'NOOPT',
                'eps': 80.1,
                'charge': 0,
                'timeout': 172800,
                }
        vals.update(settings)

        # First check the energy of the neutral system
        # To prevent conflicts when running this function in parallel,
        # a temporary copy of the molecular structure file is made and
        # used for mopac calculations.

        # Unique file name is generated by inserting a random int into
        # the file path.
        tmp_file = "{}.mol".format(uuid4().int)
        self.molecule.write(tmp_file)

        file_root, ext = os.path.splitext(tmp_file)

        # Generate the input file
        _create_mop(file_root, self.molecule, vals)
        # Run MOPAC
        _run_mopac(file_root, mopac_path)

        # Extract the neutral energy
        en1 = _extract_MOPAC_en(file_root)

        # Update the settings for the cation optimization
        settings2 = {
                    'method': 'OPT',
                    'gradient': 0.01,
                    'charge': 1,
                    'fileout': 'PDBOUT'
                    }

        vals.update(settings2)

        # Now generate a new molecule
        mol2 = copy.deepcopy(self.molecule)
        # Run the mopac optimisation
        mopac_opt(mol2, mopac_path, vals)
        # Extract the energy by using the self.mopac method
        vals['method'] = 'NOOPT'
        del vals['gradient']
        del vals['fileout']
        en2 = mol2.energy.mopac(mopac_path, vals)
        # Calculate the IP (eV)
        return en2 - en1


def _run_mopac(file_root, mopac_path, timeout=3600):

    mop_file = file_root + '.mop'

    logger.info(f'Running MOPAC - {file_root}.')

    # To run MOPAC a command is issued to the console via
    # ``subprocess.Popen``. The command is the full path of the
    # ``mopac`` program.
    file_root, ext = os.path.splitext(mop_file)
    opt_cmd = [mopac_path, file_root]
    opt_proc = psutil.Popen(opt_cmd, stdout=sp.PIPE,
                            stderr=sp.STDOUT,
                            universal_newlines=True)

    try:
        if timeout:
            proc_out, _ = opt_proc.communicate(timeout=timeout)
        else:
            proc_out, _ = opt_proc.communicate()
    except sp.TimeoutExpired:
        logger.info(('Minimization took too long and was terminated '
                     'by force - {}').format(file_root))
        _kill_mopac(file_root)


def _kill_mopac(file_root):
    """
    Stops an in-progress MOPAC run.

    To kill a MOPAC run, a file with the molecule's name and a ``.end``
    extension is written.

    Parameters
    ----------
    file_root : :class:`str`
        The molecule's name.

    Returns
    -------
    None : :class:`NoneType`

    """
    end_file = file_root + '.end'

    with open(end_file, 'w') as end:
        end.write('SHUT')


def _mop_line(settings):
    """
    Formats `settings` into a MOPAC input string.

    Parameters
    ----------
    settings : :class:`dict`
        Dictionary defined in :func:`mopac_opt`, where all the run
        details are defined.

    Returns
    -------
    :class:`str`
        String containing all the MOPAC keywords correctly formatted
        for the input file.

    """

    # Generate an empty string
    mopac_run_str = ""

    # Add Hamiltonian info
    mopac_run_str = mopac_run_str + settings['hamiltonian']
    # Forcing a single point calculation
    mopac_run_str = mopac_run_str + ' NOOPT '
    # Add EPS info
    eps_info = ' EPS={} '.format(settings['eps'])
    mopac_run_str = mopac_run_str + eps_info
    # Add Charge info
    charge_info = ' CHARGE={} '.format(settings['charge'])
    mopac_run_str = mopac_run_str + charge_info

    return mopac_run_str


def _create_mop(file_root, molecule, settings):
    """
    Creates the ``.mop`` file holding the molecule to be optimized.

    Parameters
    ----------
    mol : :class:`.Molecule`
        The molecule which is to be optimized. Its molecular
        structure file is converted to a ``.mop`` file. The original
        file is also kept.

    settings : :class:`dict`
        Dictionary defined by the MOPAC methods, where all the run
        details are defined.

    Returns
    -------
    :class:`str`
        The full path of the newly created ``.mop`` file.

    """

    mop_file = file_root + '.mop'
    mol = molecule.mol

    logger.info('Creating .mop file - {}.'.format(file_root))

    # Generate the mop file containing the MOPAC run info.
    with open(mop_file, 'w') as mop:
        # Line for the run info.
        mop.write(_mop_line(settings) + "\n")
        # Line with the name of the molecule.
        mop.write(file_root + "\n\n")

        # Write the structural info.
        for atom in mol.GetAtoms():
            atom_id = atom.GetIdx()
            symbol = atom.GetSymbol()
            x, y, z = mol.GetConformer().GetAtomPosition(atom_id)
            atom_info = f'{symbol}   {x}   +1  {y}   +1  {z}   +1 \n'
            mop.write(atom_info)

    return mop_file


def _extract_MOPAC_en(file_root):
    mopac_out = file_root + '.arc'

    with open(mopac_out) as outfile:
        target = "TOTAL ENERGY"
        energy_str = str([x for x in outfile.readlines() if target in x][0])
        energy_val = float(energy_str.split()[3])

    return energy_val


def _extract_MOPAC_dipole(file_root):
    mopac_out = file_root + '.arc'

    with open(mopac_out) as outfile:
        target = "DIPOLE"
        energy_str = str([x for x in outfile.readlines() if target in x][0])
        energy_val = float(energy_str.split()[2])

    return energy_val
