import logging
import pprint
import re
import time
from pathlib import Path
from string import Template

from i3configger import defaults

log = logging.getLogger(__name__)


class I3Configger:
    def __init__(self,
                 sourcePath=defaults.SOURCES_PATH,
                 targetPath=defaults.TARGET_PATH,
                 suffix=defaults.SOURCE_SUFFIX,
                 selectors=None, statusMarker=None):
        self.sourcePath = sourcePath
        self.mainTargetPath = targetPath
        self.suffix = suffix
        self.selectors = selectors or []
        self.statusMarker = statusMarker
        log.info("initialized %s", self)

    def __str__(self):
        return "%s(%s)" % (
            self.__class__.__name__, pprint.pformat(self.__dict__))

    def build(self):
        self.build_main_config()
        self.build_i3status()

    def build_main_config(self):
        """two pass rendering to figure out from content what"""
        paths = self.get_file_paths(
            self.sourcePath,
            selectors=self.selectors + [[self.statusMarker, None]])
        self.content = self.get_content(paths)
        self.vars = self.render_vars(self.content)
        config = self.render_config(self.content, self.vars)
        self.mainTargetPath.write_text(config)

    def build_i3status(self):
        settings = self.fetch_settings('i3status', self.vars)
        for _, target in settings.items():
            targetPath = Path(target).expanduser()
            srcPath = self.sourcePath / targetPath.name
            content = self.get_content(srcPath)
            status = self.render_config(content, self.vars)
            targetPath.write_text(status)

    @classmethod
    def render_config(cls, content, vars_):
        """Works out of the box, because $ is the standard substitution marker 
        for string.Template"""
        tpl = Template(content)
        renderedContent = tpl.safe_substitute(vars_)
        return renderedContent

    @classmethod
    def render_vars(cls, content):
        """resolve and remove $prefix for string substitution"""
        vars_ = cls.parse(content)
        resolvedVars = cls.resolve(vars_)
        return {key[1:]: value for key, value in resolvedVars.items()}

    @staticmethod
    def fetch_settings(marker, vars_):
        """Simple way of communicating settings to i3configger

        $i3configger_somecommonsetting_keyname value
        $i3configger_somecommonsetting_otherkeyname othervalue

        will be turned into ...
        
        dict(keyname=value, otherkeyname=othervalue)
        """
        marker = 'i3configger_' + marker + '_'
        markerLen = len(marker)
        return {k[markerLen:]: v for k, v in vars_.items()
                if k.startswith(marker)}

    def get_file_paths(self, sourcePath, selectors=None):
        """:returns: list of pathlib.Path"""
        filePaths = []
        for sp in [p for p in self.sourcePath.iterdir()]:
            if not sp.is_file():
                continue
            if sp.suffix != self.suffix:
                continue
            parts = sp.stem.split('.')
            if len(parts) > 1 and selectors:
                criterion, spec, *_ = parts
                for wanted, value in selectors:
                    if criterion == wanted:
                        if not value:
                            continue
                        if ((isinstance(spec, str) and spec == value)
                                or spec in value):
                            filePaths.append(sp)
            else:
                filePaths.append(sp)
        return sorted(filePaths)

    @staticmethod
    def resolve(vars_):
        resolvedVars = vars_.copy()
        """resolve values that are vars_ (e.g. set $var $ otherVar)"""
        for key, value in resolvedVars.items():
            if value.startswith('$'):
                try:
                    resolvedVars[key] = resolvedVars[value]
                except KeyError:
                    log.exception("[IGNORED] %s, %s", key, value)
        return resolvedVars

    @staticmethod
    def get_content(pathOrPaths):
        paths = [pathOrPaths] if isinstance(pathOrPaths, Path) else pathOrPaths
        msg = '# generated by i3configger (%s) #' % (time.asctime())
        sep = "#" * len(msg)
        out = ["%s\n%s\n%s\n" % (sep, msg, sep)]
        for filePath in paths:
            content = filePath.read_text()
            content = "### %s ###\n%s\n" % (filePath.name, content)
            out.append(content)
        return ''.join(out)

    @classmethod
    def parse(cls, content):
        """read all set commands"""
        vars_ = {}
        for line in [l.strip() for l in content.splitlines()]:
            if cls.looks_like_assignment(line):
                key, value = cls.get_assignment(line)
                vars_[key] = value
        return vars_

    @staticmethod
    def looks_like_assignment(line):
        return line.startswith('set ')

    ASSIGNMENT_RE = re.compile(r'(\$\S+)\s+(.*)')

    @classmethod
    def get_assignment(cls, line):
        sanitizedLine = (line.split('set')[1].split(' # ')[0]).strip()
        if len([c for c in sanitizedLine if c == '#']) > 1:
            raise MalformedAssignment("comments need space: '%s'", line)
        match = re.match(cls.ASSIGNMENT_RE, sanitizedLine)
        if not match or len(match.groups()) != 2:
            raise MalformedAssignment("can't match properly: '%s'", line)
        return match.group(1), match.group(2)


class MalformedAssignment(Exception):
    pass
