import os, os.path
import re
import abc
import logging
import yaml

log = logging.getLogger(__name__)

class Item(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractproperty
    def name(self):
        pass

    def isuptodate(self, oldmeta, catalog):
        return False

    def render(self, catalog):
        """Render the item, return re-generated metadata."""
        pass


class Text(Item):
    def __init__(self, source, dest, filters, default_meta={}):
        self.source = source
        self.dest = dest
        self.filters = filters
        self.default_meta = default_meta

    def __repr__(self):
        return '<{0}({1}, {2}, ...)>'.format(
            self.__class__.__name__, self.source, self.dest)

    @property
    def name(self):
        return self.dest

    def render(self, catalog):
        meta = self.default_meta.copy()

        # Initialize data and metadata from source file.
        with open(self.source) as f:
            data = f.read().decode('utf-8')
        if data.startswith('---\n'):
            meta_yaml, data = data.split('\n---\n', 1)
            meta.update(yaml.safe_load(meta_yaml[4:]))

        # Appply filter.
        try:
            filter_name = meta['__filter__']
        except KeyError:
            log.error('{0}: No __filter__ defined.'.format(self.source))
        else:
            try:
                filter = self.filters[filter_name]
            except KeyError:
                log.error('{0}: No filter {0}.'.format(filter_name))
            else:
                data, meta = filter(data, meta, self.dest, catalog)

        # Write filtered data to destination file.
        with open(self.dest, 'w') as f:
            f.write(data.encode('utf-8'))

        return meta


def build(items, old_catalog={}):
    catalog = {}

    for item in items:
        name = item.name
        meta = old_catalog.get(name)
        if meta is None or not item.isuptodate(meta, catalog):
            meta = item.render(catalog)
        if name in catalog:
            log.warning('{0} already present.'.format(name))
        catalog[name] = meta

    return catalog


def chain_filters(*filters):
    def chained(data, meta, name, catalog):
        for filter in filters:
            data, meta = filter(data, meta, name, catalog)
        return data, meta

    return chained


def expand_rules(rules):
    rules = [(rexp if hasattr(rexp, 'match') else re.compile(rexp),
             repl, priority, func)
             for rexp, repl, priority, func in rules]

    priority_item_seq = []
    for root, dirs, files in os.walk('.'):
        for file in files:
            # For security, do not follow symlinks.
            source = os.path.join(root, file)
            if os.path.islink(os.path.join(root, file)):
                log.warn('Ignoring symlink: {0}'.format(source))
                continue
            for rexp, repl, priority, func in rules:
                if not rexp.match(source):
                    continue
                dest = rexp.sub(repl, source)
                priority_item_seq.append((priority, func(source, dest)))

    priority_item_seq.sort(key=lambda p_i: p_i[0])
    return [i for p, i in priority_item_seq]