#! /usr/bin/env python

import sys
import os
import argparse
import glob
import logging
import tarfile
import shutil
import tempfile
import pickle
import csv

try:
    OPENEYE_MOD_LOADED = False
    from openeye import oechem
    try:
        if oechem.OEChemIsLicensed():
            OPENEYE_MOD_LOADED = True
        else:
            sys.stderr.write('WARNING: No valid license found for openeye ' +
                             'Please verify OE_LICENSE environment variable'
                             'is set to valid license file\n')
    except AttributeError as ae:
        sys.stderr.write('WARNING Unable to check if openey is licensed' +
                         str(ae))
except ImportError as e:
    sys.stderr.write('WARNING: Unable to import oechem: ' +
                     str(e) + ' validation will NOT work\n')


import d3r
from d3r.celpp import util
from d3r.celpp.task import D3RParameters


# create logger
logger = logging.getLogger('d3r.molfilevalidator')
DEFAULT_LOG_LEVEL = 'ERROR'
p = D3RParameters()
p.loglevel = DEFAULT_LOG_LEVEL
util.setup_logging(p)


# delimiter used to extract ligand name from molecule file
LIG_NAME_DELIM = '-'

COMMA = ','

# suffix for .mol aka molecule files
MOL_SUFFIX = '.mol'

# supported modes which are set by user
# via first argument to script
VALIDATE_MODE = 'validate'
GENMOLECULEDB_MODE = 'genmoleculedb'

# optional command line parameters
USER_SUBMISSION = 'usersubmission'
OUTFILE = 'outputfile'
MOLDB = 'moleculedb'


def _parse_arguments(desc, args):
    """Parses command line arguments using argparse.
    """
    parsed_arguments = D3RParameters()

    help_formatter = argparse.RawDescriptionHelpFormatter
    parser = argparse.ArgumentParser(description=desc,
                                     formatter_class=help_formatter)
    parser.add_argument("mode", choices=[VALIDATE_MODE, GENMOLECULEDB_MODE],
                        help='Sets what mode script will run in. ' +
                             VALIDATE_MODE + ' mode checks a usersubmission ' +
                             'set by --' + USER_SUBMISSION + ' flag and ' +
                        GENMOLECULEDB_MODE + ' mode generates the molecule ' +
                        ' database writing it to --' + OUTFILE)
    parser.add_argument('--moldir', help='Directory containing mol files '
                                         'used to generate database from')
    parser.add_argument('--molcsv', help='CSV file sent to participants '
                                         'containing ligand id and Smile '
                                         'string for molecules. Used to '
                                         'generate molecule database')
    parser.add_argument('--excludedir',
                        help='Comma delimited list of of directories to'
                             'not search through when examining user'
                             'submission tar file (default: SuppInfo)',
                        default='SuppInfo')
    parser.add_argument('--molcsvligandcol', default=0, type=int,
                        help='Column containing ligand id in csv file'
                        'set via --molcsv. 0 offset so 1st column'
                        'is 0 (default 0)')
    parser.add_argument('--molcsvsmilecol', default=1, type=int,
                        help='Column containing SMILE string in csv file'
                             'set via --molcsv. 0 offset so 1st column'
                             'is 0 (default 1)')
    parser.add_argument('--skipligand', help='comma delimited list of '
                                             'ligands to skip')
    parser.add_argument('--skipsmilecompare', action='store_true',
                        help='If set, skip comparison of Canonical SMILE '
                             'strings')
    parser.add_argument('--' + USER_SUBMISSION,
                        help='tar.gz file containing .mol files to validate')
    parser.add_argument('--' + OUTFILE,
                        help='Destination file to write molecule database')
    parser.add_argument('--' + MOLDB,
                        help='Molecule database generated by --' +
                             GENMOLECULEDB_MODE + ' mode')
    parser.add_argument("--log", dest="loglevel",
                        choices=['DEBUG', 'INFO', DEFAULT_LOG_LEVEL,
                                 'ERROR', 'CRITICAL'],
                        help="Set the logging level (default " +
                             DEFAULT_LOG_LEVEL + ")",
                        default=DEFAULT_LOG_LEVEL)
    parser.add_argument('--version', action='version',
                        version=('%(prog)s ' + d3r.__version__))
    return parser.parse_args(args, namespace=parsed_arguments)


class D3RAtom(object):
    """Represents an Atom"""
    def __init__(self):
        """Constructor"""
        self._ishydrogen = False
        self._atomicnumber = 0
        self._name = ''

    def is_hydrogen(self):
        """Gets boolean denoting if atom is a hydrogen
        :returns bool: True if its a hydrogen otherwise False
        """
        return self._ishydrogen

    def set_is_hydrogen(self, val):
        """Sets value for IsHydrogen
        """
        self._ishydrogen = val

    def get_atomic_number(self):
        """Gets atomic number"""
        return self._atomicnumber

    def set_atomic_number(self, val):
        """SEts atomic number"""
        self._atomicnumber = val

    def get_atomic_name(self):
        """Gets atomic name"""
        return self._name

    def set_atomic_name(self, val):
        """Sets atomic name"""
        self._name = val


class D3RMolecule(object):
    """Represents a Molecule"""
    def __init__(self):
        self._atoms = None
        self._smiles = None

    def set_atoms(self, atoms):
        """Sets list of atoms
        """
        self._atoms = atoms

    def get_atoms(self):
        """Gets list of atoms
        """
        return self._atoms

    def set_canonical_smiles_str(self, smiles_str):
        """Sets unique smiles string"""
        self._smiles = smiles_str

    def get_canonical_smiles_str(self):
        """Gets unique smiles string"""
        return self._smiles


class D3RMoleculeFromOpeneyeFactory(object):
    def __init__(self):
        """Constructor"""

    def get_d3rmolecule(self, source):
        """Gets D3RMolecule from `source`
        """
        openeye_mol = self._get_molecule_from_openeye(source)
        d3ratoms = []
        for atom in openeye_mol.GetAtoms():
            d3ratom = D3RAtom()
            d3ratom.set_atomic_name(atom.GetName())
            d3ratom.set_atomic_number(atom.GetAtomicNum())
            d3ratom.set_is_hydrogen(atom.IsHydrogen())
            d3ratoms.append(d3ratom)

        d3rmol = D3RMolecule()
        d3rmol.set_atoms(d3ratoms)
        smi_str = oechem.OECreateCanSmiString(openeye_mol)
        d3rmol.set_canonical_smiles_str(smi_str)
        return d3rmol

    def _get_molecule_from_openeye(self, source):
        """Uses openeye oechem to read in smile_str and
        create oechem.OEMol() object
        """
        istream = self._get_oechem_istream(source)
        openeye_mol = oechem.OEMol()
        try:
            val = oechem.OEReadMolecule(istream, openeye_mol)
            if val is False:
                raise ValueError('OEReadMolecule returned False '
                                 'when trying to read mol file')
        finally:
            if istream is not None:
                istream.close()
        return openeye_mol


class D3RMoleculeFromMolFileViaOpeneyeFactory(D3RMoleculeFromOpeneyeFactory):
    """Creates D3RMolecule objects using """
    def __init__(self):
        """Constructor"""
        super(D3RMoleculeFromOpeneyeFactory, self).__init__()

    def _get_oechem_istream(self, source):
        istream = oechem.oemolistream()
        istream.open(source)
        return istream


class D3RMoleculeFromSmileViaOpeneyeFactory(D3RMoleculeFromOpeneyeFactory):
    """Creates D3RMolecule objects using """
    def __init__(self):
        """Constructor"""
        super(D3RMoleculeFromOpeneyeFactory, self).__init__()

    def _get_oechem_istream(self, source):
        if source == '':
            logger.error('Source string is empty')
            return None
        istream = oechem.oemolistream()
        istream.SetFormat(oechem.OEFormat_USM)
        istream.openstring(source)
        return istream


def get_molecule_weight_and_summary(themolecule):
    """Parses Molecule object `rdmol` to generate
       summary information about the molecule
    :param themolecule: `oechm.OEMol()` already loaded
    :return: tuple (# of non hydrogen atoms, non-hydrogen atomic weight,
                    dictionary where key is atomic # and values are a count
                    of atoms of that type)
    """
    atom_dic = {}
    heavy_atom = 0
    molecular_weight = 0
    smi_str = ''
    if themolecule is None:
        logger.error('Molecule passed in is None')
        return -1, -1, {}, smi_str

    if themolecule.get_atoms() is None:
        logger.error('Molecule has None for atoms')
        return heavy_atom, molecular_weight, atom_dic, smi_str

    for atom in themolecule.get_atoms():
        if not atom.is_hydrogen():
            heavy_atom += 1
            atomical_number = atom.get_atomic_number()
            molecular_weight += atomical_number
            if atomical_number not in atom_dic:
                atom_dic[atomical_number] = 1
            else:
                atom_dic[atomical_number] += 1
        logger.debug('Atom (' + str(atom.get_atomic_name()) +
                     ') atomic weight (' +
                     str(molecular_weight) + ') atom dictionary ' +
                     str(atom_dic))

    smi_str = themolecule.get_canonical_smiles_str()
    return heavy_atom, molecular_weight, atom_dic, smi_str


class CompareMolecules(object):
    """Compares 2 D3RMolecule objects
    :param moleculedb: Dictionary of ligand id as keys and
                       values set to output of
                       get_molecule_weight_and_summary()
    :param skipsmilecompare: If True then skip comparison of canonical
                             SMILE strings
    """
    def __init__(self, moleculedb, skipsmilecompare=False):
        """Constructor"""
        self._moleculedb = moleculedb
        self._skip_smile_compare = skipsmilecompare

    def compare_molecules(self, molfile, vreport, ligand_name,
                          user_molecule):
        """Compares the `user_molecule` assumed to be right answer with
        data in the moleculedb set in Constructor.
        :returns: string with error or None upon success
        """
        if ligand_name not in self._moleculedb:
            vreport.add_ligand_error(molfile, ligand_name,
                                     'ligand not in molecule database')
            return False

        (h_atom, m_weight,
         atom_dic, smi_str) = get_molecule_weight_and_summary(user_molecule)

        if h_atom == self._moleculedb[ligand_name][0]:
            if m_weight == self._moleculedb[ligand_name][1]:
                if self._skip_smile_compare is True:
                    return True

                if smi_str == self._moleculedb[ligand_name][3]:
                    return True
                vreport.add_molecule_error(molfile, ligand_name,
                                           (h_atom, m_weight, atom_dic,
                                            smi_str),
                                           self._moleculedb[ligand_name],
                                           'Canonical SMILE strings do NOT '
                                           'match')
                return False

        vreport.add_molecule_error(molfile, ligand_name,
                                   (h_atom, m_weight, atom_dic, smi_str),
                                   self._moleculedb[ligand_name],
                                   'Number of heavy atoms and or molecular '
                                   'weight did not match ')
        return False


class ValidationReport(object):
    MOLFILE = 'molfile'
    LIGAND = 'ligand'
    MESSAGE = 'message'
    USERMOL = 'usermol'
    EXPECTEDMOL = 'expectedmol'

    def __init__(self):
        """Constructor"""
        self._ligand_errors = []
        self._mol_errors = []

    def get_ligand_errors(self):
        """Gets list of ligand errors in list of dictionaries with format:
          [{ValidationReport.MOLFILE: molfile,
           ValidationReport.LIGAND: ligand_name,
           ValidationReport.MESSAGE: message}]
        """
        return self._ligand_errors

    def get_molecule_errors(self):
        """Gets list of ligand errors in list of dictionaries with format:
          [{ValidationReport.MOLFILE: molfile,
           ValidationReport.LIGAND: ligand_name,
           ValidationReport.USERMOL: usermoltuple,
           ValidationReport.EXPECTEDMOL: dbmoltuple,
           ValidationReport.MESSAGE: message})]

           usermoltuple and dbmoltuple are from output from
           `get_molecule_weight_and_summary()`
        """
        return self._mol_errors

    def add_ligand_error(self, molfile, ligand_name, message):
        """Adds error message with ligand name"""
        self._ligand_errors.append({ValidationReport.MOLFILE: molfile,
                                    ValidationReport.LIGAND: ligand_name,
                                    ValidationReport.MESSAGE: message})

    def add_molecule_error(self, molfile, ligand_name, usermoltuple,
                           dbmoltuple, message):
        self._mol_errors.append({ValidationReport.MOLFILE: molfile,
                                 ValidationReport.LIGAND: ligand_name,
                                 ValidationReport.USERMOL: usermoltuple,
                                 ValidationReport.EXPECTEDMOL: dbmoltuple,
                                 ValidationReport.MESSAGE: message})

    def get_as_string(self):
        """Iterates across ligand and molecule errors
        outputting a human readable report of the problems
        """
        res = ''
        if len(self._ligand_errors) > 0:
            res += '\nLigand Errors\n------------\n\n'

        for entry in self._ligand_errors:
            res += ('In file: ' +
                    os.path.basename(str(entry[ValidationReport.MOLFILE])))
            res += (' ligand: ' + str(entry[ValidationReport.LIGAND]) +
                    '\n\t' + str(entry[ValidationReport.MESSAGE]) + '\n\n')

        if len(self._mol_errors) > 0:
            res += '\nMolecule Errors\n------------\n\n'

        for entry in self._mol_errors:
            logger.debug('For: ' + str(entry[ValidationReport.MOLFILE]))
            res += ('In file: ' +
                    os.path.basename(str(entry[ValidationReport.MOLFILE])))

            if entry[ValidationReport.EXPECTEDMOL] is None or \
               entry[ValidationReport.USERMOL] is None:
                res += (' ligand: ' + str(entry[ValidationReport.LIGAND]) +
                        '\n\t' +
                        str(entry[ValidationReport.MESSAGE]) + '\n\n')
            else:

                exp_nonh_atoms = str(entry[ValidationReport.EXPECTEDMOL][0])
                usr_nonh_atoms = str(entry[ValidationReport.USERMOL][0])
                exp_m_weight = str(entry[ValidationReport.EXPECTEDMOL][1])
                usr_m_weight = str(entry[ValidationReport.USERMOL][1])

                exp_atom_map = str(entry[ValidationReport.EXPECTEDMOL][2])
                usr_atom_map = str(entry[ValidationReport.USERMOL][2])

                exp_smi = str(entry[ValidationReport.EXPECTEDMOL][3])
                usr_smi = str(entry[ValidationReport.USERMOL][3])

                res += (' ligand: ' + str(entry[ValidationReport.LIGAND]) +
                        ' ' + str(entry[ValidationReport.MESSAGE]) + '\n')

                if exp_nonh_atoms != usr_nonh_atoms:
                    res += ('\tExpected ' + exp_nonh_atoms +
                            ' non hydrogen atoms, but got ' +
                            usr_nonh_atoms + '\n')
                if exp_m_weight != usr_m_weight:
                    res += ('\tExpected ' + exp_m_weight +
                            ' for non hydrogen atomic weight, but got ' +
                            usr_m_weight + '\n')

                if exp_smi != usr_smi:
                    res += ('\tExpected SMILE: ' + exp_smi + '\n' +
                            '\tReceived SMILE: ' + usr_smi + '\n')

                res += ('\tExpected atom map { atomic #: # atoms,...} ' +
                        exp_atom_map + ', but got ' + usr_atom_map + '\n\n')

        return res


def _get_ligand_name_from_file_name(file_name):
    """Extracts ligand name from file name. It is assume
    the file name has format of XXXX-<LIGAND NAME>-X.mol
    """
    if file_name is None:
        raise ValueError('file_name cannot be None')

    base_fname = os.path.basename(file_name)
    hyphen_split = base_fname.split(LIG_NAME_DELIM)
    if len(hyphen_split) < 2:
        raise ValueError('Error parsing ligand name from file name: ' +
                         base_fname)
    return hyphen_split[1]


def _write_molecule_database(theargs, ligand_dictionary):
    """Writes ligand_dictionary to pickle file specified by
    theargs.outputfile
    """
    logger.debug('Writing output to: ' + theargs.outputfile)
    p_f = open(theargs.outputfile, "w")
    pickle.dump(ligand_dictionary, p_f)
    p_f.close()


def _generate_molecule_database_fromcsv(theargs, molfactory):
    """Creates molecule database which is a pickle file
       containing a dictionary where keyword is LIGAND ID and
       value is a tuple `from get_molecule_weight_and_summary()`
       This molecules are obtained by parsing csv file set via
       `theargs.molcsv` with ligand column set to `theargs.molcsvligandcol`
       and the smiles column set to `theargs.molcsvsmilecol`
       The molecule database is written to a file specified by
       `theargs.outputfile`
    :returns: 0 upon success otherwise failure
    """
    if theargs.molcsv is None:
        logger.error('--molcsv must be set to csv file to generate database')
        return 1

    ligand_col = theargs.molcsvligandcol
    smile_col = theargs.molcsvsmilecol
    counter = -1
    ligand_dic = {}
    with open(theargs.molcsv, 'r') as cf:
        reader = csv.reader(cf, quotechar='"')
        for row in reader:
            counter += 1
            if counter is 0:
                logger.info('Skipping header row: ' + str(row))
                continue
            numcols = len(row)
            if ligand_col >= numcols:
                logger.error('In row ' + str(counter) +
                             ' ligand column index ' + str(ligand_col) +
                             ' is >= number of columns: ' + str(numcols))
                continue
            if smile_col >= numcols:
                logger.error('In row ' + str(counter) +
                             ' smile column index ' + str(ligand_col) +
                             ' is >= number of columns: ' + str(numcols))
                continue

            ligand_name = row[ligand_col]
            if row[smile_col] == '':
                logger.error('In row ' + str(counter) +
                             ' smile string is empty: ' + str(row))
                continue

            themol = molfactory.get_d3rmolecule(row[smile_col])
            if ligand_name not in ligand_dic:
                ligand_dic[ligand_name] = \
                    get_molecule_weight_and_summary(themol)

    _write_molecule_database(theargs, ligand_dic)
    return 0


def _generate_molecule_database_frommolfiles(theargs, molfactory):
    """Creates molecule database which is a pickle file
       containing a dictionary where keyword is LIGAND ID and
       value is a tuple `from get_molecule_weight_and_summary()`
       This molecules are obtained by looking for all files ending
       with MOL_SUFFIX in `theargs.moldir` directory.
       The molecule database is written to a file specified by
       `theargs.outputfile`
    :returns: 0 upon success otherwise failure
    """
    if theargs.moldir is None:
        logger.error('--moldir must be set to generate the molecule database')
        return 1

    search_path = os.path.join(theargs.moldir, '*' + MOL_SUFFIX)
    all_mol_files = glob.glob(search_path)
    if len(all_mol_files) == 0:
        logger.error('No files with suffix ' + MOL_SUFFIX + ' found in ' +
                     str(theargs.moldir))
        return 4
    ligand_dic = {}
    for mol_file in all_mol_files:
        logger.info('Reading file: ' + mol_file)
        ligand_name = _get_ligand_name_from_file_name(mol_file)
        logger.info('Ligand: ' + ligand_name)
        themol = molfactory.get_d3rmolecule(mol_file)
        if ligand_name not in ligand_dic:
            ligand_dic[ligand_name] = get_molecule_weight_and_summary(themol)

    _write_molecule_database(theargs, ligand_dic)
    return 0


def _molfile_from_tarfile_generator(thefile, tmpdir=None,
                                    direxclude=None):
    """Generator method that takes a tarfile and
       returns molfiles written to a temporary directory
    """
    temp_dir = tempfile.mkdtemp(dir=tmpdir)
    tf = None

    if direxclude is not None and direxclude is not '':
        direxcludelist = direxclude.split(COMMA)
        logger.debug('Directory exclude list set to: ' + str(direxcludelist))
    else:
        direxcludelist = None

    skip_entry = False
    try:
        tf = tarfile.open(thefile, 'r:gz')
        for entry in tf.getmembers():
            if entry.name.endswith(MOL_SUFFIX) and entry.isfile():
                fname = os.path.basename(entry.name)
                if direxcludelist is not None:
                    split_dir = os.path.dirname(entry.name).split(os.sep)
                    for d in direxcludelist:
                        if d in split_dir:
                            skip_entry = True
                            break
                if skip_entry is True:
                    logger.info('Skipping ' + str(entry.name) +
                                ' since directory path in exclude list')
                    skip_entry = False
                    continue
                dest_molfile = os.path.join(temp_dir, fname)
                with open(dest_molfile, 'w') as f:
                    shutil.copyfileobj(tf.extractfile(entry), f)
                yield dest_molfile
    finally:
        shutil.rmtree(temp_dir)
        if tf is not None:
            tf.close()


def _get_molecule_database(theargs):
    mol_db_file = open(theargs.moleculedb, 'r')
    mole_db = pickle.load(mol_db_file)
    mol_db_file.close()
    return mole_db


def _validate_molfiles_in_tarball(theargs, molfactory, moleculedb):
    """Generates molecule validation report
    """
    if theargs.usersubmission is None:
        logger.error('--usersubmission must be set with path to tarball '
                     'to validate')
        return 1

    ligand_skip_list = []

    try:
        if theargs.skipligand is not None:
            ligand_skip_list = theargs.skipligand.split(',')
            logger.debug('Skipping ligands: ' + str(ligand_skip_list))
    except AttributeError as e:
        logger.debug('Got attribute error ' + str(e))

    skipsmilecompare = False
    try:
        if theargs.skipsmilecompare is True:
            logger.info('--skipsmilecompare flag set. Skipping SMILE '
                        'string comparison')
            skipsmilecompare = True
    except AttributeError:
        pass

    comparemols = CompareMolecules(moleculedb,
                                   skipsmilecompare=skipsmilecompare)
    report = ValidationReport()
    for molfile in _molfile_from_tarfile_generator(theargs.usersubmission,
                                                   direxclude=theargs.excludedir):
        try:
            ligand_name = _get_ligand_name_from_file_name(molfile)
        except ValueError as e:
            report.add_ligand_error(molfile, None, str(e))
            continue
        if ligand_name in ligand_skip_list:
            logger.debug(ligand_name + ' in skip list. Skipping...')
            continue
        if ligand_name not in moleculedb:
            report.add_ligand_error(molfile, ligand_name,
                                    'ligand not in molecule database')
            continue
        try:
            d3rmol = molfactory.get_d3rmolecule(molfile)
        except Exception as e:
            report.add_molecule_error(molfile, ligand_name, None, None,
                                      'Unable to parse file: ' + str(e))
            continue
        comparemols.compare_molecules(molfile, report, ligand_name, d3rmol)
    return report


def _run_validation(theargs, molfactory):
    moleculedb = _get_molecule_database(theargs)
    report = _validate_molfiles_in_tarball(theargs, molfactory, moleculedb)

    report_str = report.get_as_string()
    if report_str == '':
        return 0
    sys.stdout.write(report_str)
    return 1


def main(args):
    """Main entry into genmoleculedb
    :param args: should be set to sys.argv which is a list of arguments
                 starting with script name as the first argument
    """
    desc = """
              Version {version}

              Performs mol file validation on files with {mol_suffix}
              extension in gzipped tar files.

              This script runs in two modes: {genmol_mode} & {validate_mode}

              These modes are set via the first argument passed into this
              script.

              In general '{genmol_mode}' mode is run first and
              '{validate_mode}' mode is run multiple times to perform the
              validation.

              '{genmol_mode}' mode takes a directory of {mol_suffix} files or a
                             CSV file with SMILES strings and generates a
                            molecule database. This database is a pickle file
                            and is used to validate the mol files. The output
                            database is specified by the --{output} flag.
                            This database basically is a dictionary of
                            Ligand names as parsed from the mol file
                            name XXX-####-XXX.mol where the ligand name is
                            expected to be the value between first and
                             second - character.

                            Any problems found are output to standard out/err
                             and a non 0 exit code is returned.

              '{validate_mode}' mode takes the molecule database from
                               {genmol_mode} (which is passed in via
                                --{moldb} flag) and
                                validates all mol files found in the  tarfile
                                specified by --{usersubmission} flag. It is
                                assumed all mol files have a file name format
                                like this: XXX-####-XXX.mol where #### between
                                1st and second - is considered to be the
                                Ligand ID.

                              Validation is done by comparing number and
                                atomic weight of non hydrogen atoms against the
                                database.

                              Any problems found are output to standard
                               out/err and a non 0 exit code is returned.

              For more information visit: http://www.drugdesigndata.org

              """.format(version=d3r.__version__,
                         mol_suffix=MOL_SUFFIX,
                         output=OUTFILE,
                         genmol_mode=GENMOLECULEDB_MODE,
                         validate_mode=VALIDATE_MODE,
                         moldb=MOLDB,
                         usersubmission=USER_SUBMISSION)

    theargs = _parse_arguments(desc, args[1:])
    theargs.program = args[0]
    theargs.version = d3r.__version__

    util.setup_logging(theargs)

    try:
        molfactory = D3RMoleculeFromMolFileViaOpeneyeFactory()

        if theargs.mode == GENMOLECULEDB_MODE:
            logger.info('Running in ' + GENMOLECULEDB_MODE +
                        ' database generation mode')

            if theargs.outputfile is None:
                logger.error('--outputfile must be set to generate the '
                             'molecule database')
                return 3
            if theargs.moldir is not None:
                return _generate_molecule_database_frommolfiles(theargs,
                                                                molfactory)
            if theargs.molcsv is not None:
                smilefactory = D3RMoleculeFromSmileViaOpeneyeFactory()
                return _generate_molecule_database_fromcsv(theargs,
                                                           smilefactory)
            raise ValueError('Either --moldir or --molcsv must be '
                             'set to generate molecule database')
        if theargs.mode == VALIDATE_MODE:
            logger.info('Running in ' + VALIDATE_MODE + ' validation mode')
            return _run_validation(theargs, molfactory)
        raise ValueError('Unsupported mode: ' + str(theargs.mode))
    except Exception:
        logger.exception("Error caught exception")
        return 2


if __name__ == '__main__':  # pragma: no cover
    sys.exit(main(sys.argv))
