"""Common functions and classes
"""
from sqlalchemy.engine import Engine
from sqlalchemy import event
from datetime import date
# Used to inspect objects
import inspect
import sys
import os


DMD_EPOCH_DATE = date(2000, 1, 1)
MSG_PREFIX = '[info]'
"""This is prefixed onto verbose massages (you do not have to use though).
(`str`)
"""

MSG_OUT = sys.stderr
"""The location of verbose messages
"""

try:
    _DEFAULT_CONFIG = os.path.join(os.environ['HOME'], '.db_conn.cnf')
    """The default fallback path for the config file, root of the home
    directory (`str`)
    """
except KeyError:
    # HOME environment variable does not exist for some reason
    _DEFAULT_CONFIG = None


_DEFAULT_SECTION = 'dmd'
"""The default section name in an uni config file that contains SQLAlchemy
connection parameters (`str`)
"""

_DEFAULT_PREFIX = 'db.'
"""The default prefix name in an ini config section each SQLAlchemy connection
parameter should be prefixed with this (`str`)
"""


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    """Enable foreign key enforcement for SQLite connections.

    Intended for use as a SQLAlchemy ``connect`` event listener.
    Silently ignored for non-SQLite backends.

    Parameters
    ----------
    dbapi_connection : DBAPI connection
        The raw DBAPI connection object.
    connection_record : `sqlalchemy.pool._ConnectionRecord`
        The connection record (unused).
    """
    cursor = dbapi_connection.cursor()
    try:
        cursor.execute("PRAGMA foreign_keys=ON")
    except Exception:
        # Probably not SQLite but do not know how to tell!
        pass
    cursor.close()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def print_orm_obj(obj, exclude=[]):
    """Prints the contents of the SQL Alchemy ORM object.

    Parameters
    ----------
    obj : `sqlalchemy.ext.declarative.Base`
        The SQLAlchemy declarative base model object. This should have a
        __table__ attribute that contains the corresponding SQLAlchemy table
        object.
    exclude : `NoneType` or `list` or `str`, optional, default `NoneType`
        A list of attributes (these correspond to table columns) that you do
        not want to include in the string.

    Returns
    -------
    formatted_string : `str`
        A formatted string that can be printed. The string contains the
        attributes of the object and the data they contain.

    Notes
    -----
    This is designed to be called from an SQLAlchemy ORM model object and it
    in the __repr__ method. It simply frovides a formatted string that has the
    data in the object.
    """
    exclude = exclude or []

    attr_values = []
    for i in obj.__table__.columns:
        if i.name in exclude:
            continue
        val = getattr(obj, i.name)
        if val.__class__.__name__ != "method":
            attr_values.append("{0}={1}".format(i.name, val))

    # Turn outlist into a string and return
    return "<{0}({1})>".format(obj.__class__.__name__, ", ".join(attr_values))


# @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
class Msg(object):
    """
    Outputs messages based on verbosity.
    prefix ~ text put at the start of each message
    outfile ~ option to direct the output elsewhere i.e. sys.stderr
    """
    def __init__(self, verbose=True, prefix="", file=sys.stdout):
        """
        Initialise

        Parameters
        ----------

        verbose : :obj:`bool` (optional)
            The verbosity of the message, if False then any calls
            to msg are not displayed
        prefix : :obj:`str` (optional)
            A prefix to add to every message when msg is called. The
            default is no prefix
        file : :obj:`FileLikeoObject`
            Where the message should be directed. The default is STDOUT
            but can be changed to STDERR by passing sys.stderr. Also can
            be redirected to a file if needed
        """

        # Make sure verbose is boolean
        if not isinstance(verbose, bool):
            raise TypeError('verbose should be true or false')

        self.verbose = verbose
        self.prefix = prefix
        self.file = file

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def msg(self, m, verbose=None, prefix=None, file=None):
        """
        Print the message if verbosity is True

        Parameters
        ----------
        m : :obj:`str`
            The message to print
        prefix : :obj:`str`, optional
            An optional prefix to temporarily overide any global prefix
            set when the object was created. This saves adjusting the
            prefix for small adjustments
        outfile : :obj:`file`, optional
            An optional outfile to temporarily overide any global outfile
            set when the object was created, i.e. sys.stderr
        verbose : :obj:`Bool`, optional
            An optional verbosity to temporarily overide any global
            verbosity set when the object was created
        """

        # If the optional verbose argument has not been set then we set to the
        # verbosity of the object
        if verbose is None:
            verbose = self.verbose

        # If we are being verbose
        if verbose is True:
            # Use local or global prefix and/or outfile
            prefix = prefix or self.prefix
            file = file or self.file

            # If prefix is defined add a single space
            if prefix:
                prefix = prefix + " "

            # Output the message
            print("%s%s" % (prefix, m), file=file)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def msg_atts(self, obj, **kwargs):
        """
        Print the attributes of an object message if verbosity is True

        Parameters
        ----------
        atts : :obj:`str`
            An object who's attributes can be obtained with getattr
        prefix : :obj:`str`, optional
            An optional prefix to temporarily overide any global prefix
            set when the object was created. This saves adjusting the
            prefix for small adjustments
        outfile : :obj:`file`, optional
            An optional outfile to temporarily overide any global outfile
            set when the object was created, i.e. sys.stderr
        verbose : :obj:`Bool`, optional
            An optional verbosity to temporarily overide any global
            verbosity set when the object was created
        prog_name : :obj:`str`, optional
            A program name argument, if passed then a mini program name banner
            will be displayed before outputting the obj attributes
        versiob : :obj:`float`, optional
            The version number for prog_name, only supply if prog_name is given
        """

        try:
            # See if we have the prog_name and version arguments. If so then
            # msg them out and also remove from kwargs so they do not cause an
            # error when passed to msg
            prog_name = kwargs.pop('prog_name')
            version = ''
            if 'version' in kwargs:
                version = " v%s" % str(kwargs.pop('version'))
            self.msg("= %s%s =" % (prog_name, version), **kwargs)
        except KeyError:
            pass

        # Loop through the objects attributes
        for i in inspect.getmembers(obj):
            # Ignores anything starting with underscore
            # (that is, private and protected attributes)
            if not i[0].startswith('_'):
                # Ignores methods
                if not inspect.ismethod(i[1]):
                    attribute = str(i[1])
                    modifier = "value"
                    # For lists, dicts and tuples we report the length
                    if isinstance(i[1], list) is True or\
                       isinstance(i[1], dict) is True or\
                       isinstance(i[1], tuple) is True:
                        attribute = str(len(i[1]))
                        modifier = "length"
                    # Output the message, here we use clean password of lazy
                    # SQLAlchemy to make sure any passwords are stripped
                    # from connection URLS
                    self.msg("%s %s: %s" % (i[0],
                                            modifier,
                                            attribute),
                             **kwargs)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def splice_spaces(splice_str, start, end):
    """Replace the text between ``start``, ``end`` with whitespace.

    Parameters
    ----------
    splice_str : `str`
        The string to splice spaces into.
    start : `int`
        The start position to splice from.
    end : `int`
        The end position to splice to.
    """
    return (
        splice_str[:start] +
        " " * (end - start) +
        splice_str[end:]
    )
