#!/usr/bin/env python
#
# file: core.py
# author: Cyrus Harrison <cyrush@llnl.gov>
#
#

import os
import subprocess
import datetime
import json
import errno
import webbrowser

from os.path import join as pjoin

__all__ = ["Context",
           "shell",
           "svn",
           "cmake",
           "make",
           "inorder"]
# ----------------------------------------------------------------------------
#  Method: mkdir_p
#
#  Programmer: Cyrus Harrison
#  Date:       Mon Sept 30 2013
#
#  Helper that does recursive folder creation.
#
# Recipe from:
#  http://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python
# ----------------------------------------------------------------------------
def mkdir_p(path):
    try:
        os.makedirs(path)
    except OSError as exc: # Python >2.5
        if exc.errno == errno.EEXIST and os.path.isdir(path):
            pass
        else: 
            raise

# ----------------------------------------------------------------------------
#  Method: _decode_list
#
#  Programmer: Cyrus Harrison
#  Date:       Fri Jan 11 2013
#
#  Helper which decodes json unicode values (in lists) to standard strings.
#
# Recipe from:
#  http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python
# ----------------------------------------------------------------------------
def _decode_list(data):
    rv = []
    for item in data:
        if isinstance(item, unicode):
            item = item.encode('utf-8')
        elif isinstance(item, list):
            item = _decode_list(item)
        elif isinstance(item, dict):
            item = _decode_dict(item)
        rv.append(item)
    return rv

# ----------------------------------------------------------------------------
#  Method: _decode_dict
#
#  Programmer: Cyrus Harrison
#  Date:       Fri Jan 11 2013
#
#  Helper which decodes json unicode values (in dictonaries) to standard strings.
#
# Recipe from:
#  http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-unicode-ones-from-json-in-python
# ----------------------------------------------------------------------------
def _decode_dict(data):
    rv = {}
    for key, value in data.iteritems():
        if isinstance(key, unicode):
           key = key.encode('utf-8')
        if isinstance(value, unicode):
           value = value.encode('utf-8')
        elif isinstance(value, list):
           value = _decode_list(value)
        elif isinstance(value, dict):
           value = _decode_dict(value)
        rv[key] = value
    return rv

def json_loads(jsons):
    if os.path.isfile(jsons):
        jsons=open(jsons).load()
    return json.loads(jsons,object_hook=_decode_dict)

def json_dumps(v):
    return json.dumps(v,indent=2)

def timenow():
    return datetime.datetime.now()
    
def timestamp(t=None,sep="_"):
    """ Creates a timestamp that can easily be included in a filename. """
    if t is None:
        t = timenow()    
    sargs = (t.year,t.month,t.day,t.hour,t.minute,t.second)
    sbase = "".join(["%04d",sep,"%02d",sep,"%02d",sep,"%02d",sep,"%02d",sep,"%02d"])
    tstamp = sbase % sargs
    return      {"key":    tstamp,
                 "year":   t.year,
                 "month":  t.month,
                 "day":    t.day,
                 "hour":   t.hour,
                 "minute": t.minute,
                 "second": t.second}

def timedelta(t_start,t_end):
    t_delta = t_end - t_start
    days, seconds = t_delta.days, t_delta.seconds
    hours = days * 24 + seconds // 3600
    minutes = (seconds % 3600) // 60
    seconds = seconds % 60
    return {"key": "%02d_%02d_%02d" % (hours, minutes, seconds),
            "hours":hours,
            "minutes":minutes,
            "seconds": seconds}

def sexe(cmd,ret_output=False,echo = False,env=None):
        """ Helper for executing shell commands. """
        kwargs = {"shell":True}
        if not env is None:
            kwargs["env"] = env
        if echo:
            print "[exe: %s]" % cmd
        if ret_output:
            kwargs["stdout"] = subprocess.PIPE
            kwargs["stderr"] = subprocess.STDOUT
            p = subprocess.Popen(cmd,**kwargs)
            res =p.communicate()[0]
            return p.returncode,res
        else:
            return subprocess.call(cmd,**kwargs),""

class Context(object):
    def __init__(self,enable_logging=True,log_dir=None):
        self.enable_logging = enable_logging
        if enable_logging and log_dir is None:
                log_dir = pjoin(os.getcwd(),"_logs")
        self.log_dir    = log_dir
        if not self.log_dir is None and not os.path.isdir(log_dir):
            mkdir_p(log_dir)
        self.actions  = {}
        self.triggers = {}
    def to_dict(self):
        r = {"context":{"actions":{},
                     "triggers":{},
                     "log_dir":self.log_dir,
                     "enable_logging":self.enable_logging}}
        for k,v in self.actions.items():
            r["context"]["actions"][k] = v.params
        for k,v in self.triggers.items():
            r["context"]["triggers"][k] = v.params
        return r
    def to_json(self):
        return json_dumps(self.to_dict())
    @classmethod 
    def load_dict(cls,params):
        if params.has_key("context"):
            res = Context(enable_logging = params["context"]["enable_logging"],
                          log_dir        = params["context"]["log_dir"])
            for k,v in params["context"]["actions"].items():
                res.actions[k]  = Action.load_dict(v)
            for k,v in params["context"]["triggers"].items():
                res.triggers[k] = Action.load_dict(v)
        return res
    @classmethod 
    def load_json(cls,jsons):
        if os.path.isfile(jsons):
            jsons = open(jsons).read()
        params = json_loads(jsons)
        return cls.load_dict(params)
    def fire(self,trigger_name,key=None):
        t   = self.triggers[trigger_name]
        if key is None:
            key = self.unique_key()
        key += "_" + trigger_name
        res = t.fire(self,trigger_name,key)
        self.log(key,res)
        return res
    def log(self,key,result,tag=None):
        if self.log_dir is None:
            return
        if not tag is None:
            ofname = pjoin(self.log_dir,"log_" + key + "_" + tag +".json")
        else:
            ofname = pjoin(self.log_dir,"log_" + key + ".json")
        try:
            ofile = open(ofname,"w")
            ofile.write(json.dumps(result,indent=2))
            ofile.close()
            # create link
            lastlink = pjoin(self.log_dir,"last.json")
            if os.path.islink(lastlink):
                os.unlink(lastlink)
            os.symlink(ofname,lastlink)
        except Exception as e:
            print "<logging error> failed to write results to %s" % ofname
            raise e
    def unique_key(self):
        return timestamp()["key"]

class Action(object):
    def __init__(self):
        self.params = {}
    def to_dict(self):
        return dict(self.params)
    def to_json(self):
        return json_dumps(self.to_dict())
    @classmethod
    def load_dict(cls,params):
        if params.has_key("type"):
            atype = params["type"]
            aparams = dict(params)
            del aparams["type"]
            return globals()[atype](**aparams)
    @classmethod
    def load_json(cls,jsons):
        if os.path.isfile(jsons):
            jsons = open(jsons).read()
        params = json_loads(jsons)
        return cls.load_dict(params)

class ShellAction(Action):
    def __init__(self,
                 cmd,
                 type="shell",
                 working_dir=None,
                 description=None,
                 halt_on_error=True,
                 env=None):
        super(ShellAction,self).__init__()
        self.params["cmd"]  = cmd
        self.params["type"] = type
        self.params["halt_on_error"] = halt_on_error
        if working_dir is None:
            working_dir  = os.getcwd()
        self.params["working_dir"] = working_dir
        if description is None:
            description = ""
        self.params["description"] = description
        self.params["env"] = env
    def execute(self,base,key,tag,parent_res):
        t_start = timenow();
        res = {"action":
               {"key": key,
                "type":self.params["type"],
                "name":tag,
                "cmd": self.params["cmd"],
                "description": self.params["description"],
                "working_dir": self.params["working_dir"],
                "env": self.params["env"],
                "start_time":  timestamp(t_start),
                "halt_on_error": self.params["halt_on_error"],
                "finish_time":  None,
                "elapsed_time": None,
                "output": None}
                }
        parent_res["trigger"]["active_actions"] = [res]
        base.log(key=key,result=parent_res)
        cwd = os.path.abspath(os.getcwd())
        env = os.environ.copy()
        if not self.params["env"] is None:
            env.update(self.params["env"])
        try:
            if not os.path.isdir(self.params["working_dir"]):
                mkdir_p(self.params["working_dir"])

            print "[chdir to: %s]" % self.params["working_dir"]
            os.chdir(self.params["working_dir"])
            rcode, rout = sexe(self.params["cmd"],
                               ret_output=True,
                               echo=True,
                               env=env)
            res["action"]["output"] = rout
            res["action"]["return_code"]  = rcode
        except KeyboardInterrupt as e:
            res["action"]["error"] = "shell command interrupted by user (ctrl-c)"
        except Exception as e:
            print e
            res["action"]["error"] = str(e)
        t_end = timenow()
        res["action"]["finish_time"]  = timestamp(t_end)
        res["action"]["elapsed_time"] = timedelta(t_start,t_end) 
        os.chdir(cwd)
        parent_res["trigger"]["active_actions"] = []
        base.log(key=key,result=parent_res)
        return res


class SVNAction(ShellAction):
    def __init__(self,
                 svn_url,
                 svn_cmd,
                 svn_bin="svn",
                 working_dir=None,
                 description=None,
                 halt_on_error=True,
                 env=None):
        cmd = " ".join([svn_bin,svn_cmd,svn_url])
        super(SVNAction,self).__init__(cmd=cmd,
                                       type="svn",
                                       working_dir=working_dir,
                                       description=description,
                                       halt_on_error=halt_on_error,
                                       env=env)
        self.params["svn_url"] = svn_url
        self.params["svn_cmd"] = svn_cmd
        self.params["svn_bin"] = svn_bin


class CMakeAction(ShellAction):
    def __init__(self,
                 src_dir,
                 cmake_opts="",
                 cmake_bin="cmake",
                 working_dir=None,
                 description=None,
                 halt_on_error=True,
                 env=None):
        cmd = " ".join([cmake_bin,cmake_opts,src_dir])
        super(CMakeAction,self).__init__(cmd=cmd,
                                         type="cmake",
                                         working_dir=working_dir,
                                         description=description,
                                         halt_on_error=halt_on_error,
                                         env=env)
        self.params["src_dir"]    = src_dir
        self.params["cmake_opts"] = cmake_opts
        self.params["cmake_bin"]  = cmake_bin

class MakeAction(ShellAction):
    def __init__(self,
                 target="",
                 nthreads=1,
                 make_bin="make",
                 working_dir=None,
                 description=None,
                 halt_on_error=True,
                 env=None):
        cmd = " ".join([make_bin,
                        "-j%d" % nthreads,
                        target])
        super(MakeAction,self).__init__(cmd=cmd,
                                        type="make",
                                        working_dir=working_dir,
                                        description=description,
                                        halt_on_error=halt_on_error,
                                        env=env)
        self.params["target"]    = target
        self.params["nthreads"]  = nthreads
        self.params["make_bin"]   =make_bin

class InorderTrigger(Action):
    def __init__(self,actions=None):
        super(InorderTrigger,self).__init__()
        self.params["actions"] = []
        if not actions is None:
            self.params["actions"] = actions
        self.params["type"]    = "inorder" 
    def append(self,action_name):
        self.params["actions"].append(action_name)
    def extend(self,action_list):
        self.params["actions"].extend(action_list)
    def fire(self,base,trigger_name,key):
        t_start = timenow();
        res = {"trigger":{"name":trigger_name,
                          "key": key,
                          "start_time":  timestamp(t_start),
                          "end_time": None,
                          "elapsed_time": None,
                          "actions": self.params["actions"],
                          "active_actions":[],
                          "type":"inorder",
                          "results" :[]}}
        base.log(key,res)
        try:
            for  action in self.params["actions"]:
                print "[fire: %s]" % action
                a = base.actions[action]
                r = a.execute(base,key,action,res)
                res["trigger"]["results"].append(r)
                base.log(key,res)
                if r["action"].has_key("return_code"):
                    print "[rcode: %d]" % r["action"]["return_code"]
                if "error" in r["action"].keys() or \
                    (r["action"].has_key("return_code") and r["action"]["return_code"] != 0) :
                    emsg = "[action failed: %s]" % json.dumps(r,indent=2)
                    print emsg
                    if r["action"].has_key("halt_on_error") and r["action"]["halt_on_error"]:
                        raise Exception(emsg)
        except Exception as emsg:
            res["trigger"]["error"] = str(emsg)
        t_end = timenow()
        res["end_time"] = timestamp(t_end)
        res["elapsed_time"] = timedelta(t_start,t_end)
        base.log(key,res)
        return res


shell   = ShellAction
svn     = SVNAction
cmake   = CMakeAction
make    = MakeAction

inorder = InorderTrigger



def view_log(fname):
    port = 8000
    html_src = pjoin(os.path.split(os.path.abspath(__file__)[0],"html"))
    log_dir  = os.path.split(os.path.abspath(fname))[0]
    subprocess("cp -f %s/* %s" % html_src,log_dir)
    os.chdir(log_dir)
    subprocess.Popen([sys.executable, '-m', 'SimpleHTTPServer', str(port)])
    webbrowser.open_new_tab('localhost:8000/log_view?log=%s' % fname)

