from __future__ import unicode_literals
from builtins import str
from builtins import range
from builtins import object
from xpath.exceptions import *
import xpath.exceptions
import xpath.expr
import xpath.parser
import xpath.yappsrt

__all__ = ['find', 'findnode', 'findvalue', 'XPathContext', 'XPath']
__all__.extend((x for x in dir(xpath.exceptions) if not x.startswith('_')))

def api(f):
    """Decorator for functions and methods that are part of the external
    module API and that can throw XPathError exceptions.

    The call stack for these exceptions can be very large, and not very
    interesting to the user.  This decorator rethrows XPathErrors to
    trim the stack.

    """
    def api_function(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except XPathError as e:
            raise e
    api_function.__name__ = f.__name__
    api_function.__doc__ = f.__doc__
    return api_function

class XPathContext(object):
    def __init__(self, document=None, **kwargs):
        self.default_namespace = None
        self.namespaces = {}
        self.variables = {}

        if document is not None:
            if document.nodeType != document.DOCUMENT_NODE:
                document = document.ownerDocument
            if document.documentElement is not None:
                attrs = document.documentElement.attributes
                for attr in (attrs.item(i) for i in range(attrs.length)):
                    if attr.name == 'xmlns':
                        self.default_namespace = attr.value
                    elif attr.name.startswith('xmlns:'):
                        self.namespaces[attr.name[6:]] = attr.value

        self.update(**kwargs)

    def clone(self):
        dup = XPathContext()
        dup.default_namespace = self.default_namespace
        dup.namespaces.update(self.namespaces)
        dup.variables.update(self.variables)
        return dup

    def update(self, default_namespace=None, namespaces=None,
                  variables=None, **kwargs):
        if default_namespace is not None:
            self.default_namespace = default_namespace
        if namespaces is not None:
            self.namespaces = namespaces
        if variables is not None:
            self.variables = variables
        self.variables.update(kwargs)

    @api
    def find(self, expr, node, **kwargs):
        return xpath.find(expr, node, context=self, **kwargs)

    @api
    def findnode(self, expr, node, **kwargs):
        return xpath.findnode(expr, node, context=self, **kwargs)

    @api
    def findvalue(self, expr, node, **kwargs):
        return xpath.findvalue(expr, node, context=self, **kwargs)

    @api
    def findvalues(self, expr, node, **kwargs):
        return xpath.findvalues(expr, node, context=self, **kwargs)

class XPath(object):
    _max_cache = 100
    _cache = {}

    def __init__(self, expr):
        """Init docs.
        """
        try:
            parser = xpath.parser.XPath(xpath.parser.XPathScanner(str(expr)))
            self.expr = parser.XPath()
        except xpath.yappsrt.SyntaxError as e:
            raise XPathParseError(str(expr), e.pos, e.msg)

    @classmethod
    def get(cls, s):
        if isinstance(s, cls):
            return s
        try:
            return cls._cache[s]
        except KeyError:
            if len(cls._cache) > cls._max_cache:
                cls._cache.clear()
            expr = cls(s)
            cls._cache[s] = expr
            return expr

    @api
    def find(self, node, context=None, **kwargs):
        if context is None:
            context = XPathContext(node, **kwargs)
        elif kwargs:
            context = context.clone()
            context.update(**kwargs)
        return self.expr.evaluate(node, 1, 1, context)

    @api
    def findnode(self, node, context=None, **kwargs):
        result = self.find(node, context, **kwargs)
        if not xpath.expr.nodesetp(result):
            raise XPathTypeError("expression is not a node-set")
        if len(result) == 0:
            return None
        return result[0]

    @api
    def findvalue(self, node, context=None, **kwargs):
        result = self.find(node, context, **kwargs)
        if xpath.expr.nodesetp(result):
            if len(result) == 0:
                return None
            result = xpath.expr.string(result)
        return result

    @api
    def findvalues(self, node, context=None, **kwargs):
        result = self.find(node, context, **kwargs)
        if not xpath.expr.nodesetp(result):
            raise XPathTypeError("expression is not a node-set")
        return [xpath.expr.string_value(x) for x in result]

    def __repr__(self):
        return '%s.%s(%s)' % (self.__class__.__module__,
                              self.__class__.__name__,
                              repr(str(self.expr)))

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

@api
def find(expr, node, **kwargs):
    return XPath.get(expr).find(node, **kwargs)

@api
def findnode(expr, node, **kwargs):
    return XPath.get(expr).findnode(node, **kwargs)

@api
def findvalue(expr, node, **kwargs):
    return XPath.get(expr).findvalue(node, **kwargs)

@api
def findvalues(expr, node, **kwargs):
    return XPath.get(expr).findvalues(node, **kwargs)
