"""
prop_args2.py
Set, read, and write program-wide properties in one location. Includes logging.
"""
import logging
import sys
import platform
import networkx as nx
import json
import os
from django.core.exceptions import ObjectDoesNotExist

from IndrasNet.models import ABMModel

SWITCH = '-'
PERIODS = 'periods'
BASE_DIR = 'base_dir'
DATAFILE = 'datafile'

OS = "OS"

UTYPE = "user_type"
# user types (DUMMY type is only for automated test)
TERMINAL = "terminal"
IPYTHON = "iPython"
IPYTHON_NB = "iPython Notebook"
WEB = "Web browser"
DUMMY = "dummy"

VALUE = "val"
QUESTION = "question"
DEFAULT_VAL = "default_val"
ATYPE = "atype"
HIVAL = "hival"
LOWVAL = "lowval"

global user_type
user_type = TERMINAL

INT = 'INT'
FLT = 'DBL'
BOOL = 'BOOL'
STR = 'STR'
type_dict = {INT: int, FLT: float, BOOL: bool, STR: str}


def get_prop_from_env():
    global user_type
    try:
        user_type = os.environ[UTYPE]
    except KeyError:
# this can't be done before logging is set up!
#        logging.info("Environment variable user type not found")
        user_type = TERMINAL
    return user_type


def read_props(model_nm, file_nm):
    """
    Create a new PropArgs object from a json file
    """
    props = json.load(open(file_nm))
    return PropArgs.create_props(model_nm, props)


class Prop():
    """
    Container for prop attributes.

    Attributes include:
        val - the value to be used in the model run
        question - a question prompt for the user's input for the prop value
        atype - the user's answer type (INT, DBL, BOOL, STR, etc.)
        default_val - the property's default value
        lowval - the lowest value val can take on
        hival - the highest value val can take on
    """

    def __init__(self, val=None, question=None, atype=None, default_val=None,
                        lowval=None, hival=None):
        self.val = val
        self.question = question
        self.atype = atype
        self.default_val = default_val
        self.lowval = lowval
        self.hival = hival

    def to_json(self):
        return {"val": self.val,
                "question": self.question,
                "atype": self.atype,
                "default_val": self.default_val,
                "lowval": self.lowval,
                "hival": self.hival}

    def __str__(self):
        return str(self.val)


class PropArgs():
    """
    This class holds named properties for program-wide values.
    It enables getting properties from a file, a database,
    or from the user, either via the command line or a prompt.
    """

    @staticmethod
    def create_props(model_nm, prop_dict=None):
        """
        Create a property object with values in 'props'.
        """
        if prop_dict is None:
            prop_dict = {}
        return PropArgs(model_nm, prop_dict=prop_dict)


    def __init__(self, model_nm, logfile=None, prop_dict=None,
                 loglevel=logging.INFO):
        """
        Loads and sets properties in the following order:
        1. The Database
        2. The User's Environment (operating system, dev/prod settings, etc.)
        3. Property File
        4. Command Line
        5. Questions Prompts During Run-Time
        """
        self.logfile = logfile
        self.model_nm = model_nm
        self.graph = nx.Graph()
        self.props = {}

        # 1. The Database
        self.set_props_from_db()

        # 2. The Environment
        self.overwrite_props_from_env()

        # 3. Property File
        self.overwrite_props_from_dict(prop_dict)

        if self.props[UTYPE].val in (TERMINAL, IPYTHON, IPYTHON_NB):
            # 4. process command line args and set them as properties:
            self.overwrite_props_from_cl()

            # 5. Ask the user questions.
            self.overwrite_props_from_user()

        elif self.props[UTYPE].val == WEB:
            self.props[PERIODS] = Prop(val=1)
            self.props[BASE_DIR] = Prop(val=os.environ[BASE_DIR])

        self.logger = Logger(self, model_name=model_nm, logfile=logfile)
        self.graph.add_edge(self, self.logger)

    def set_props_from_db(self):
        try:
            params = ABMModel.objects.get(name=self.model_nm).params.all()
            for param in params:
                atype = param.atype
                typed_default_val = self._type_val_if_possible(param.default_val,
                                                               param.atype)
                self.props[param.prop_name] = Prop(val=typed_default_val,
                                                   question=param.question,
                                                   atype=atype,
                                                   default_val=typed_default_val,
                                                   lowval=param.lowval,
                                                   hival=param.hival)
        except ObjectDoesNotExist:
            print("ABMModel not found in db: " + self.model_nm)

    def overwrite_props_from_env(self):
        global user_type
        user_type = get_prop_from_env()
        self.props[UTYPE] = Prop(val=user_type)
        self.props[OS] = Prop(val=platform.system())

    def overwrite_props_from_dict(self, prop_dict):
        """
        General Dict:

            {
                prop_name:
                    {
                        val: <something>,
                        question: <something>,
                        atype: <something>,
                    }
                prop_name:
                    {
                        val: <something>,
                        hival: <something>,
                        lowval: <something>,
                    }
            }

        Simple Dict:

            {
                prop_name: val,
                prop_name: val
            }

        """
        for prop_nm in prop_dict:
            if type(prop_dict[prop_nm]) is dict:
                atype = prop_dict[prop_nm].get(ATYPE, None)
                val = self._type_val_if_possible(prop_dict[prop_nm].get(VALUE,
                                                                        None),
                                                 atype)
                question = prop_dict[prop_nm].get(QUESTION, None)
                default_val = prop_dict[prop_nm].get(DEFAULT_VAL, None)
                hival = prop_dict[prop_nm].get(HIVAL, None)
                lowval = prop_dict[prop_nm].get(LOWVAL, None)
                self.props[prop_nm] = Prop(val=val, question=question, atype=atype, default_val=default_val,
                                           hival=hival, lowval=lowval)
            else:
                self[prop_nm] = prop_dict[prop_nm]

    def overwrite_props_from_cl(self):
        prop_nm = None
        for arg in sys.argv:
            # the first arg (-prop) names the property
            if arg.startswith(SWITCH):
                prop_nm = arg.lstrip(SWITCH)
            # the second arg is the property value
            if prop_nm is not None:
                self.props[prop_nm].val = arg
                prop_nm = None

    def overwrite_props_from_user(self):
        for prop_nm in self:
            if (hasattr(self.props[prop_nm], QUESTION)
                and self.props[prop_nm].question):
                self.props[prop_nm].val = self._keep_asking_until_correct(prop_nm)
    
    @staticmethod
    def _type_val_if_possible(val, atype):
        if atype in type_dict:
            type_cast = type_dict[atype]
            return type_cast(val)
        else:
            return val

    def _keep_asking_until_correct(self, prop_nm):
        atype = None
        if hasattr(self.props[prop_nm], ATYPE):
            atype = self.props[prop_nm].atype

        while True:
            answer = input(self.get_question(prop_nm))
            if not answer:
                return self.props[prop_nm].val

            try:
                typed_answer = self._type_val_if_possible(answer, atype)
            except ValueError:
                print("Input of invalid type. Should be {atype}"
                      .format(atype=atype))
                continue

            if not self._answer_within_bounds(prop_nm, typed_answer):
                print("Input must be between {lowval} and {hival} inclusive."
                      .format(lowval=self.props[prop_nm].lowval,
                              hival=self.props[prop_nm].hival))
                continue

            return typed_answer

    def _answer_within_bounds(self, prop_nm, typed_answer):
        if (self.props[prop_nm].atype is None 
            or self.props[prop_nm].atype in (STR, BOOL)):
            return True

        if (self.props[prop_nm].lowval is not None 
            and self.props[prop_nm].lowval > typed_answer):
            return False

        if (self.props[prop_nm].hival is not None 
            and self.props[prop_nm].hival < typed_answer):
            return False

        return True

    def display(self):
        """
        How to represent the properties on screen.
        """
        ret = "Properties for " + self.model_nm + "\n"
        for prop_nm in self:
            ret += "\t" + prop_nm + ": " + str(self.props[prop_nm].val) + "\n"

        return ret

    def __iter__(self):
        return iter(self.props)

    def __str__(self):
        return self.display()

    def __len__(self):
        return len(self.props)

    def __contains__(self, key):
        return key in self.props

    def __setitem__(self, key, v):
        """
        Set a property value.
        """
        if key in self:
            self.props[key].val = v
        else:
            self.props[key] = Prop(val=v)

    def __getitem__(self, key):
        return self.props[key].val

    def __delitem__(self, key):
        del self.props[key]

    def items(self):
        return self.props.items()

    def get_logfile(self):
        """
        Special get function for logfile name
        """
        return self.props["log_fname"].val

    def write(self, file_nm):
        """
        Write properties to json file.
        Useful for storing interesting parameter sets.
        """
        dict_for_json = {}
        for prop_name in self.props:
            dict_for_json[prop_name] = self.props[prop_name].to_json()
        f = open(file_nm, 'w')
        json.dump(dict_for_json, f, indent=4)
        f.close()

    def to_json(self):
        return { prop_nm: self.props[prop_nm].to_json() for prop_nm in self.props }

    def get(self, key, default=None):
        if key in self.props and self.props[key].val:
            return self.props[key].val
        return default

    def get_question(self, prop_nm):
            return "{question} [{lowval}-{hival}] ({default}) "\
                   .format(question=self.props[prop_nm].question, 
                           lowval=self.props[prop_nm].lowval,
                           hival=self.props[prop_nm].hival,
                           default=self.props[prop_nm].val)


class Logger():
    """
    A class to track how we are logging.
    """

    DEF_FORMAT = '%(asctime)s:%(levelname)s:%(message)s'
    DEF_LEVEL = logging.INFO
    DEF_FILEMODE = 'w'
    # DEF_FILENAME = 'log.txt'

    def __init__(self, props, model_name, logfile=None,
                 loglevel=logging.INFO):
        if logfile is None:
            logfile = model_name + ".log"
        fmt = props["log_format"] if "log_format" in props else Logger.DEF_FORMAT
        lvl = props["log_level"] if "log_level" in props else Logger.DEF_LEVEL
        fmd = props["log_fmode"] if "log_fmode" in props else Logger.DEF_FILEMODE
        props["log_fname"] = logfile
# we put the following back in once the model names are fixed
#  fnm = props.get("log_fname", logfile)
        logging.basicConfig(format=fmt,
                            level=lvl,
                            filemode=fmd,
                            filename=logfile)
        logging.info("Logging initialized.")