""" provides Object base class that can load/save plus utility functions.  """

__txt__ = "Timestamped based JSON objects."
__version__ = 124

import _thread
import datetime
import fcntl
import importlib
import json
import logging
import obj
import os
import os.path
import time
import sys
import threading
import traceback
import types

exceptions = []
homedir = os.path.expanduser("~")
starttime = time.time()
waiting = []

aliases = {
           "c": "show cmds",
           "cfg": "show cfg",
           "e": "ed",
           "f":"find",
           "h": "show help",
           "m": "show mods",
           "ps": "show threads",
           "s": "show",
           "t": "show types",
           "v": "show version"
}

args = {"log": ["txt",]}

classes = {
         "cfg": "obj.Cfg",
         "command": "obj.command.Command",
         "dict": "obj.Dict",
         "event": "obj.event.Event",
         "handler": "obj.handler.Handler",
         "log": "obj.Log",
         "obj": "obj.Object",
         "shell": "obj.shell.Shell",
}

names = {
         "ed": "obj.cmds",
         "find": "obj.cmds",
         "log": "obj.cmds",
         "rm": "obj.cmds",
         "show": "obj.cmds",
         "stop": "obj.cmds",
         "undel": "obj.cmds",
}

modnames = {
         "Cfg": "obj",
         "Command": "obj.command",
         "Dict": "obj",
         "Event": "obj.event",
         "Handler": "obj.handler",
         "Log": "obj",
         "Object": "obj",
         "Shell": "obj.shell",
         "Store": "obj",
         "Task": "obj.tasks",
         "Token": "obj.command"
}

types = ["obj.Cfg", "obj.command.Command", "obj.Dict", "obj.event.Event", "obj.handler.Handler",
         "obj.Log", "obj.Object", "obj.shell.Shell", "obj.Store",
         "obj.tasks.Task", "obj.command.Token"]

def locked(func):

    """ basic locker decorator. """

    lock = _thread.allocate_lock()

    def lockedfunc(*args, **kwargs):
        """ locked function %s """ % get_name(func)
        lock.acquire()
        res = None
        try:
            res = func(*args, **kwargs)
        finally:
            try:
                lock.release()
            except Exception:
                pass
        return res

    return lockedfunc

class Object:

    """ basic object with load/save capabilities """

    def __eq__(self, o):
        return self.__dict__ == o.__dict__

    def __ne__(self, o):
        return self.__dict__ != o.__dict__

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

    def __str__(self):
        """ return json string. """
        return json.dumps(self, default=default, indent=4, sort_keys=True)

    def get(self, k, d=None):
        """ getattr on this object. """
        return getattr(self, k, d)

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

    def keys(self):
        return self.__dict__.keys()

    def load(self, path):
        """ load json file into this object. """
        assert cfg.storedir
        assert path
        logging.debug("load %s" % path)
        p = os.path.join(cfg.storedir, path)
        if not os.path.exists(p):
            cdir(p)
        with open(p) as f:
            d = json.load(f)
            self.update(d)
        return self

    def save(self, path="", stime=None, timed=False):
        """ save this object to disk. """
        assert cfg.storedir
        if not path or stime or timed:
            if not stime:
                stime = str(datetime.datetime.now()).replace(" ", os.sep)
            path = os.path.join(obj.get_type(self), stime)
        logging.info("save %s" % path)
        pp = os.path.join(cfg.storedir, path)
        cdir(pp)
        with open(pp, "w") as f:
            json.dump(self, f, default=default, indent=4, sort_keys=True)
        return path

    def set(self, k, v):
        """ setattr on this object. """
        return setattr(self, k, v)

    def update(self, o, skip=False):
        """ update this object. """
        if "items" not in dir(o):
            o = vars(o)
        for k, v in o.items():
            if skip and not v:
                continue
            self.set(k, v)
        return self

class Dict(Object, dict):

    """ mixed in with dict. """

    pass

class Cfg(Object):

    """ basic config object. """

    pass

class Log(Object):

    """ log some text. """

    pass

class Store(obj.Object):

    """ store objects on disk """

    def all(self, type, selector={}, index=None, delta=0, showdel=False):
        """ return all objects matching selector. """
        res = []
        nr = -1
        for fn in filenames(type, delta):
            nr += 1
            o = filehook(fn)
            if not showdel and ("_deleted" in dir(o) and o._deleted):
                continue
            if index is not None and nr != index:
                continue
            res.append((fn, o))
        return res

    def find(self, type, selector, index=None, delta=0, showdel=False):
        """ return specific objects matching selector. """
        nr = -1
        res = []
        for fn in filenames(type, delta):
            nr += 1
            o = filehook(fn)
            if not o:
                continue
            if not showdel and ("_deleted" in dir(o) and o._deleted):
                continue
            if search(o, selector):
                if index is not None and nr != index:
                    continue
                res.append((fn, o))
        return res

    def last(self, type, index=None, delta=0):
        """ return path, object of the last object of a type. """
        fns = filenames(type, delta)
        if fns:
            fn = fns[-1]
            return (fn, filehook(fn))
        return (None, None)

def cdir(path):
    """ create a directory. """
    if os.path.exists(path):
        return
    res = ""
    path = os.path.dirname(path)
    for p in path.split(os.sep):
        res += "%s%s" % (p, os.sep)
        padje = os.path.abspath(os.path.normpath(res))
        try:
            os.mkdir(padje)
        except (IsADirectoryError, NotADirectoryError, FileExistsError):
            pass
        except OSError as ex:
            logging.error(get_exception())
    return True

def cmd(txt):
    """ run a single command. """
    c = obj.event.Event()
    c.parse(txt)
    sh = obj.shell.Shell()
    sh.verbose = False
    e = sh.dispatch(c)
    e.wait()
    return e

def default(o):
    """ convert an object to it's proper string representation. """
    t = type(o)
    if isinstance(o, Object):
        return vars(o)
    if isinstance(o, dict):
        return o.items()
    if isinstance(o, list):
        return iter(o)
    if t in [str, True, False, int, float]:
        return o
    return repr(o)

def edit(o, setter):
    """ edit a object. """
    for key, value in setter.items():
        if "," in value:
            value = value.split(",")
        t = type(value)
        if value in ["True", "true"]:
            o.set(key, True)
        elif value in ["False", "false"]:
            o.set(key, False)
        elif t == list:
            o.set(key, value)
        elif t == str:
            o.set(key, value)
        else:
            o.set(key, value)
    return o

def filenames(name, delta=0):
    """ fetch all filenames in a named subdir. """ 
    if not name:
        return []
    p = os.path.join(obj.cfg.storedir, name) + os.sep
    res = []
    now = time.time()
    past = now + delta
    cdir(p)
    for rootdir, dirs, files in os.walk(p, topdown=True):
        if name and name not in rootdir:
            continue
        for fn in files:
            fnn = os.path.join(rootdir, fn).split(obj.cfg.workdir)[-1][1:]
            if delta:
                if fn_time(fnn) < past:
                    continue
            res.append(os.sep.join(fnn.split(os.sep)[1:]))
    return sorted(res, key=lambda x: fn_time(x))

def filehook(fn):
    """ load an object from disk and convert it to it's proper type. """
    t = get_fntype(fn)
    o = get_cls(t)()
    o.load(fn)
    return o    

def find(*args, **kwargs):
    """ find matching objects. """
    return [x[1] for x in store.find(*args, **kwargs)]

def fn_time(daystr):
    """ return timestamp derived from a string. """
    daystr = daystr.replace("_", ":")
    datestr = " ".join(daystr.split(os.sep)[-2:])
    try:
        datestr, rest = datestr.rsplit(".", 1)
    except ValueError:
        rest = ""
    try:
        t = time.mktime(time.strptime(datestr, "%Y-%m-%d %H:%M:%S"))
        if rest:
            t += float("." + rest)
    except ValueError:
        t = 0
    return t

def format(o, keys=None, full=False):
    """ format an object into a displayable string. """
    if keys is None:
        keys = vars(o).keys()
    res = []
    txt = ""
    for k in keys:
        v = getattr(o, k, None)
        if not v:
            continue
        v = str(v)
        if full:
            res.append("%s=%s " % (k, v))
        else:
            res.append(v)
    for v in res:
        txt += "%s " % v.strip()
    return txt.strip()

def get_cls(name):
    """ convert a object name to its type. """
    modname, clsname = name.rsplit(".", 1)
    mod = importlib.import_module(modname)
    return getattr(mod, clsname)

def get_exception(txt="", sep=""):
    """ print the raised exception in a oneliner. """
    exctype, excvalue, tb = sys.exc_info()
    trace = traceback.extract_tb(tb)
    result = ""
    for elem in trace:
        fname = elem[0]
        linenr = elem[1]
        func = elem[2]
        plugfile = fname[:-3].split(os.sep)
        mod = []
        for elememt in plugfile[::-1]:
            mod.append(elememt)
            if elememt == "obj":
                break
        ownname = '.'.join(mod[::-1])
        result += "%s:%s %s %s " % (ownname, linenr, func, sep)
    res = "%s%s: %s %s" % (result, exctype, excvalue, str(txt))
    del trace
    return res

def get_fntype(fn):
    """ retrieve type from filename. """
    return fn.split(os.sep)[0]

def get_name(o):
    """ determine name of a object. """
    nn = ""
    try:
        nn = o.__self__.__class__.__name__
    except AttributeError:
        nn = o.__class__.__name__
    n = ""    
    try:
        n = o.__name__
    except AttributeError:
        pass
    return "%s.%s" % (nn, n)

def get_type(o):
    """ determine type of a object (module/class name). """
    mod = ""
    try:
        mod = o.__self__.__class__.__module__
    except AttributeError:
        mod = o.__class__.__module__
    nn = ""
    try:
        nn = o.__self__.__class__.__name__
    except AttributeError:
        nn = o.__class__.__name__
    return "%s.%s" % (mod, nn)

def hook(t, d):
    """ convert a dict to a typed object. """
    o = get_cls(t)()
    o.update(d)
    return o    

def kill(thrname):
    """ stop a thread. """
    for task in threading.enumerate():
        if thrname not in str(task):
            continue
        if "cancel" in dir(task):
            task.cancel()
        if "exit" in dir(task):
            task.exit()
        if "stop" in dir(task):
            task.stop()

def last(t):
    """ return last object of a type. """
    v =  store.last(t)
    if v:
        return v[-1]

def launch(func, *args, **kwargs):
    """ launch a task. """
    t = obj.tasks.Task()
    t.start()
    t.put(func, args, kwargs)
    return t

def ld(*args):
    """ create filename in the logdir. """
    return os.path.abspath(os.path.join(cfg.logdir, *args))

def load(self, path):
    """ load a object from disk and return it properly typed. """
    return Object().load(path)

def hd(*args):
    """ return a subdir in the homedir. """
    return os.path.abspath(os.path.join(homedir, *args))

def search(o, m: None):
    """ match given selector (dict with keys/values to match). """
    if not m:
        m = {}
    res = False
    for k, v in m.items():
        vv = o.get(k, None)
        if vv:
            if v == None:
                res = True
            elif v in str(vv):
                res = True
            else:
                res = False
                break
        else:
            res = False
            break
    return res

def sliced(o, keys=None):
    """return a sliced object. """
    oo = obj.Object()
    if not keys:
        keys = o.keys()
    for k in keys:
        try:
            oo[k] = o[k]
        except KeyError:
            pass
    return oo

def wd(*args):
    """ create pathname in the working directory. """
    workdir = os.path.abspath(cfg.workdir)
    return os.path.abspath(os.path.join(workdir, *args))

#:
cfg = Cfg()
cfg.args = []
cfg.history = False
cfg.name = "obj"
cfg.storedir = ""
cfg.verbose = False
cfg.workdir = ""
cfg.logdir = ""
cfg.version = __version__
store = Store()
    
import obj.command
import obj.clock
import obj.event
import obj.loader
import obj.handler
import obj.shell
import obj.tasks
