import sys
import os
from AVCommon.logger import logging
import ast
import re
import abc
from types import ModuleType
from AVCommon import config
import commands
import inspect
import glob
import time
import pickle
import importlib

import base64

inspect_getfile = inspect.getfile(inspect.currentframe())
cmd_folder = os.path.split(os.path.realpath(os.path.abspath(inspect_getfile)))[0]
if cmd_folder not in sys.path:
    sys.path.insert(0, cmd_folder)
parent = os.path.split(cmd_folder)[0]
if parent not in sys.path:
    sys.path.insert(0, parent)

known_commands = {}
context = {}

import exceptions
class WEFake:
    def __init__(self, *args):
        self.args = list(args)
    def __str__(self):
        return "%s %s" % ("WindowsError", self.args)

if "WindowsError" not in dir (exceptions):
    exceptions.WindowsError = WEFake

def init():
    global command_names
    global known_commands
    #logging.debug("initCommands")

    commands = []
    for d in ["AVAgent", "AVCommon", "AVMaster"]:
        for side in ["server", "client", "meta"]:
            search = os.path.join(parent, d, "commands", side, "*.py")
            dcommands = glob.glob(search)
            for dc in dcommands:
                name_file = os.path.split(dc)[1]
                name = os.path.splitext(name_file)[0]
                if name.startswith("__init__"):
                    continue

                path = "%s.%s.%s.%s" % (d, "commands", side, name)
                commands.append((name, side, path))
                #logging.debug("%s" % (name))

    for name, side, path in commands:
        m = importlib.import_module(path)
        m.side = side
        known_commands[name] = m

    #logging.info("Commands: %s" % known_commands.keys())


def normalize(data):
    """ a command cane be unserialized in many ways:
    - command instance
    - dict: { command: payload }
    - tuple/array: (cmd, success, payload) or (cmd, payload)
    - str: "(cmd, success, payload)"

    payload is evaluated via ast, so that it can contain a type like tuple, array, number, dict and so on
    if payload begins with a "|", it's considered a plain string and it's not evaluated
    """
    #ident, command, answer = serialized.split(',', 2)
    #assert(ident == "CMD")
    cmd = data
    success = None
    args = None
    result = None
    vm = None

    assert data, "cannot normalize a null argument"

    identified = "instance"
    if isinstance(data, Command):
        logging.warn("normalizing a command")
        return data.name, data.success, data.args, data.result, data.vm
    elif isinstance(data, dict):
        identified = "dict"
        assert len(data) == 1
        cmd = data.keys()[0]
        args = data[cmd]
    elif not isinstance(data, str) and len(data) == 3:
        identified = "len 3"
        cmd, success, payload = data
        if success == None:
            args = payload
        else:
            result = payload

    elif isinstance(data, str):
        identified = "str"
        m = re.compile("\('(\w+)\', (\w+), (.+)\)").match(data)
        if m:
            identified = "reg"
            groups = m.groups()
            assert len(groups) == 3
            cmd = groups[0]
            success = ast.literal_eval(groups[1])
            try:
                payload = ast.literal_eval(groups[2])
            except SyntaxError:
                payload = groups[2]
            except ValueError:
                payload = groups[2]
            if success == None:
                args = payload
            else:
                result = payload

    #logging.debug(1)Command.knownCommands

    if config.verbose:
        logging.debug("identified: %s" % identified)
    assert isinstance(success, bool) or success is None, "success: %s" % success
    assert isinstance(cmd, str), "not a string: %s" % cmd

    return (cmd, success, args, result, vm)


def factory(data):
    if not known_commands:
        init()

    name, success, args, result, vm = normalize(data)

    return _factory(name, success, args, result, vm)


def eval_safe(attr,  value):

    if isinstance(value, str) and attr.startswith("|"):
        a = value[1:]
    else:
        try:
            a = ast.literal_eval(value)
        except:
            a = value

    attr = a


def _factory(name, success, args, result,  vm, timestamp = None):
    assert name in known_commands.keys(), "Unknown command: %s" % name

    m = known_commands[name]
    if not timestamp:
        #logging.debug("new timestamp required")
        timestamp=time.time()
    c = Command(name, success, args, result, vm, m.side, timestamp)

    c.execute = m.execute
    if c.side == "client":
        c.on_answer = m.on_answer
        c.on_init = m.on_init
    else:
        c.on_answer = lambda x, y, z: None
        c.on_init = lambda x, y, z: None

    # payload eval in safe way
    eval_safe(c.args, args)
    eval_safe(c.result, result)

    return c


def unserialize(message):
    data = base64.b64decode(message)

    name, success, args, result, vm, side, timestamp = pickle.loads(data)
    if config.verbose:
        logging.debug("unserialized: (%s,%s,%s,%s,%s,%s)" % (name, success, args, str(result)[:50], vm, timestamp))
    return _factory(name, success, args, result, vm, timestamp)


class Command(object):
    """ A Command is a base class for any instruction to give on a channel.
    A command is defined by a name and an implementation class. Each class can be Server, Client or Meta.

    """
    success = None

    side = None
    vm = None

    def __init__(self, name, success=None, args="", result=None, vm=None, side=None,  timestamp=None):
        """ A command is constructed with a name, that identifies the derived class """
        self.name = name
        self.success = success
        self.args = args
        if  isinstance(result, Exception):
            self.result = str(result)
        else:
            self.result = result
        if not timestamp:
            self.timestamp = time.time()
        else:
            self.timestamp = timestamp
        self.vm = vm
        self.side = side

    def reset(self, vm):
        self.vm = vm
        self.timestamp = time.time()

    def serialize(self):
        logging.debug("serialize result: %s" % self.result)

        serialized = pickle.dumps(( self.name, self.success, self.args, self.result, self.vm, self.side, self.timestamp ),
                                  pickle.HIGHEST_PROTOCOL)
        #logging.debug("pickle.dumps(%s)" % serialized)
        return base64.b64encode(serialized)

    def basic_string(self):
        ts = time.strftime("%y%m%d-%H%M%S", time.localtime(self.timestamp))
        return "%s, %s, %s, %s" % (self.name, self.success, ts, self.args)

    def __str__(self):
        ts = time.strftime("%y%m%d-%H%M%S", time.localtime(self.timestamp))
        return "%s, %s, %s, %s, %s" % (self.name, self.success, ts, self.args, self.result)