
import os
import re
import hashlib
import base64
from string import Template

class AngleBracketTemplate(Template):
    """ Template for substituting tokens in angle brackets.

    Tokens may have mixed case letters and underscores.
    E.g. <foo>, <foo_bar> or <Scene>
    """
    delimiter = '<'
    pattern = r"""
    \<(?:
    (?P<escaped>\<)|
    (?P<named>  )\>|
    (?P<braced>[A-Za-z][A-Za-z_]+)\>|
    (?P<invalid>)
    )
    """


class Expander(object):
    """
    Class to expand angle bracket tokens and env vars.

    We handle tokens in strings, list values, and dict values (but not keys)
    """

    def __init__(self, safe=False, **context):
        self.context = context
        self._safe = safe

    def evaluate(self, target):
        """Evaluate target, whether its a value, list, or dict."""
        if type(target) == dict:
            result = {}
            for k in target:
                result[k] = self.evaluate_item(target[k])
            return result
        elif type(target) == list:
            return [self.evaluate_item(item) for item in target]
        return self.evaluate_item(target)

    def evaluate_item(self, item):
        """Evaluate an expression string

        Replace <token>s with values provided by the context dict
        """
        item = os.path.expandvars(item.strip())

        # run all expression resolvers
        context = self.context.copy()
        for resolver_class in ExpressionResolver.resolvers():
            resolver = resolver_class(item, context)
            item = resolver.template
            context = resolver.context
        # do the substitutions
        if self._safe:
            return AngleBracketTemplate(item).safe_substitute(context)
        try:
            return AngleBracketTemplate(item).substitute(context)
        except KeyError:
            raise KeyError("Invalid token. Valid tokens are: {}".format(
                self.context.keys()))
        except ValueError:
            raise ValueError("Invalid token. Valid tokens are: {}".format(
                self.context.keys()))

class ExpressionResolver(object):

    """
    Expression resolver base class.

    Templates may contain modifiers, such as for padding numbers or manipulating
    paths. Since the AngleBracketTemplate class (derived from Template) can't
    handle expressions, at least not in the customer friendly format we want, we
    have to resolve the expressions first, then modify the template and the
    context so they can be handed over to the AngleBracketTemplate to be
    resolved.

    Example: A template may contain "myfile.<pad framenumber 4>.exr" where
    context holds {"framenumber": 76}. Ultimately, we want to resolve to
    "myfile.0076.exr".

    To do this, we replace the expression token <pad framenumber 4> with a hash,
    like "<lJxKwmdZfi>" and we add that hash as a key in the context, and give
    it the resolved (modified) value of the variable: "0076".

    The modified context and template are accessed through public members of the
    same name.

    We handle as much as possible of the above in this base class so that
    derived classes may be minimal plugins and only concern themselves with
    specifying the expression format regex, and doing the actual modification.
    See: PadResolver and PosixResolver

.
    """
    # must be overridden
    PATTERN = None

    @staticmethod
    def get_alpha_hash(rhs):
        """A 10 char hash of an expression string"""

        return "".join([c for c in base64.b64encode(
            hashlib.md5( str.encode(rhs) ).digest()).decode('ascii') if c.isalpha()][:10])
 
    @classmethod
    def get_rx(cls):
        """Combine the regex from derived class with total template regex.

        The reason for this is so that we don't accidentally resolve patterns in
        escaped angle brackets. Admittedly, this is unlikely to ever happen.
        """
        rx = r"\<(?:(?P<escaped>\<)|(?P<named>)\>|(?P<braced>"
        rx += cls.PATTERN
        rx += r")\>|(?P<invalid>))"
        return re.compile(rx)

    @classmethod
    def resolvers(cls):
        """All the plugin classes"""
        return cls.__subclasses__()

    def __init__(self, template,  context):
        """Modify the template and context."""
        self.context = context.copy()
        self.template = template

        found = [x for x in self.get_rx().findall(template) if x[2]]
        # Will be something like [("","","pad frame 5",""),("","","pad endframe 4,"")]
        # Where each tuple captures an angle bracket token with an expression
        # that matches this plugin's regex.

        for group in found:
            expr = group[2]
            args = expr.split()
            # args[1] is the variable / i.e. the original context key
            if args[1] in self.context:
                key = self.get_alpha_hash(" ".join(args))

                # replace the expression in the template with the key (hash)
                self.template = self.template.replace(
                    "<{}>".format(expr), "<{}>".format(key))

                # add the new key to the context with the resolved value
                self.context[key] = self.resolve(*args[1:])

    # @abc.abstractmethod
    def resolve(self):
        return NotImplementedError


class PadResolver(ExpressionResolver):
    """
    Resolve padded number expressions
    """

    # A regex that describes the expression. For this padding function, we need
    # the word "pad", followed by the variable name, followed by a single digit.
    # e.g. pad framenumber 4
    PATTERN = r"pad\s+[A-Za-z][A-Za-z_]+\s+\d"

    def resolve(self, variable, *args):
        pad_value = int(args[0])
        var_value = int(self.context[variable])
        return "{{:0{}d}}".format(pad_value).format(var_value)

class PosixResolver(ExpressionResolver):
    """
    Resolve windows->posix path expressions
    """
    # A regex that describes the expression. For this path modifier, we need the word
    # "posix", followed by the variable name.
    # e.g. posix outpath
    PATTERN = r"posix\s+[A-Za-z][A-Za-z_]+"

    def resolve(self, variable):
        result = re.sub(r"^[a-zA-Z]:", "", self.context[variable] )
        return result.replace("\\", "/")


class UpperResolver(ExpressionResolver):
    """
    Resolve upper case expression
    """
    PATTERN = r"upper\s+[A-Za-z][A-Za-z_]+"

    def resolve(self, variable):
        return self.context[variable].upper()

class LowerResolver(ExpressionResolver):
    """
    Resolve lower case expression
    """
    PATTERN = r"lower\s+[A-Za-z][A-Za-z_]+"

    def resolve(self, variable):
        return self.context[variable].lower()


class BasenameResolver(ExpressionResolver):
    """
    Resolve basename expression.

    Can have optional x arg to denote strip_ext
    """
    PATTERN = r"basename\s+[A-Za-z][A-Za-z_]+"

    def resolve(self, variable, *args):
        return re.split("\\\\|/", self.context[variable])[-1]
        

class BasenamexResolver(ExpressionResolver):
    """
    Resolve basename expression.

    Can have optional x arg to denote strip_ext
    """
    PATTERN = r"basename_x\s+[A-Za-z][A-Za-z_]+"

    def resolve(self, variable, *args):
        base = re.split("\\\\|/", self.context[variable])[-1]
        return os.path.splitext(base)[0]

