
from collections import Sequence
import os

import six
from six.moves.urllib import parse
import threading

from .assets import AssetManager
from .exceptions import TemplateNotFound, InvalidTag

try:
    from lxml import etree as ET
except ImportError:
    try:
        from xml.etree import cElementTree as ET
    except ImportError:
        from xml.etree import ElementTree as ET

from werkzeug.utils import find_modules, import_string


class Config(object):

    def __init__(self, kwargs):
        self.data = {
            "base_dir": os.getcwd(),
            "static_url": "static/",
            "static_dirs": [os.path.join(os.path.dirname(__file__), "static")],
            "port": 8000,
            "host": "127.0.0.1"
        }
        values = kwargs
        for key in values:
            method = "clean_%s" % key
            try:
                func = getattr(self, method)
            except AttributeError:
                if key in self.data:
                    self.data[key] = values[key]
                else:
                    raise KeyError("%r is not a valid configuration key" % key)
            else:
                self.data[key] = func(values[key])

    def abspath(self, *args):
        return os.path.join(self.base_dir, *args)

    def clean_static(self, value):
        dirs = []
        if not isinstance(value, (tuple, list)):
            value = [value]
        for val in value:
            path = self.abspath(val)
            if os.path.isdir(path):
                dirs.append(path)
        return dirs

    def clean_static_url(self, value):
        value = value.strip("/") + "/"
        return value

    def clean_port(self, value):
        return int(value)

    def clean_host(self, value):
        return value

    def dict(self):
        return self.data.copy()


class Environment(object):

    def __init__(self, **kwargs):
        self.local = threading.local()
        conf = Config(kwargs).dict()
        self.base_dir = conf["base_dir"]
        self.static_url = conf["static_url"]
        self.static_dirs = conf["static_dirs"]
        self.port = conf["port"]
        self.host = conf["host"]
        self.elements = {}
        self.load_elements()

    def load_modules(self):
        for pkg in ("kaya.tags", ):
            mods = find_modules(pkg)
            for m in mods:
                import_string(m, True)

    def load_elements(self):
        from kaya.tags.base import Element
        self.elements.clear()
        self.load_modules()
        store = Element.__subclasses__()
        while store:
            cls = store.pop(0)
            store.extend(cls.__subclasses__())
            name = cls.__name__.lower()
            if not name.startswith("_") and not cls.abstract:
                self.elements[name] = cls

    def abspath(self, path_name):
        return os.path.join(self.base_dir, path_name)

    def get_template_path(self, name):
        path = self.abspath(name)
        return path

    def is_tag(self, node):
        return isinstance(node.tag, six.string_types)

    def get_xml_root(self, path_name):
        path = self.get_template_path(path_name)
        return ET.parse(path).getroot()

    def get_html(self, path_name, pretty_print=False):
        try:
            tree = self.get_xml_root(path_name)
        except (OSError, IOError):
            raise TemplateNotFound(path_name)
        ctx = Context(self, tree, path_name)
        content = ctx.render()
        if pretty_print:
            try:
                return ET.tostring(ET.fromstring(content), pretty_print=True, method="html", encoding="unicode")
            except TypeError:
                raise TypeError("pretty_print option is supported only when lxml is installed")
        return content

    @staticmethod
    def make_node(tag, attrib):
        return ET.Element(tag, attrib)

    def make_static_url(self, path):
        return parse.urljoin(self.static_url, path)

    def find_static(self, name):
        name = name.strip("/")
        for dir_ in self.static_dirs:
            filename = os.path.join(dir_, name)
            if os.path.isfile(filename):
                return filename


class Context(object):

    def __init__(self, env, root, path_name):
        self.env = env
        self.root = root
        self.path_name = path_name
        self.deps = set()

    def get_asset_tags(self):
        mgr = AssetManager(self.deps, self)
        return mgr.get_tags()

    def find_node(self, id, node=None):
        for parent, child in self.iter_nodes(node):
            if id == child.get("id"):
                return child

    def iter_nodes(self, node=None):
        if node is None:
            node = self.root
        store = [(node, c) for c in node]
        while store:
            parent, child = store.pop(0)
            yield parent, child
            for c in child:
                store.append((child, c))

    @staticmethod
    def replace_node(parent, child, node):
        index = list(parent).index(child)
        parent.remove(child)
        parent.insert(index, node)

    def is_tag(self, node):
        return self.env.is_tag(node)

    def get_xml_root(self, path):
        return self.env.get_xml_root(path)

    def get_element_class(self, node):
        return self.env.elements[node.tag]

    def make_element(self, node):
        try:
            return self.get_element_class(node)(node, self)
        except KeyError:
            raise InvalidTag(node, "Invalid tag")

    def make_node(self, *args, **kwargs):
        return self.env.make_node(*args, **kwargs)

    def require(self, *packages):
        for package in packages:
            if not isinstance(package, Sequence):
                package = [package]
            for p in package:
                self.deps.add(p)

    def render(self):
        el = self.make_element(self.root)
        return el.render()

    def find_static(self, *args, **kwargs):
        return self.env.find_static(*args, **kwargs)

    def make_static_url(self, *args, **kwargs):
        return self.env.make_static_url(*args, **kwargs)