#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
  Supernum is a pragmatic static site generator using jinja2 templates,
  reStructeredText or Markdown.

  Usage:
    supernum [-fh] [<workdir>] [-c <context>] [-t <templates>] [-i <index>] [-r <root>] [-b <build>] [-S] [-P <port>]

  Options:
    <workdir>       Directory where the source files can be found. Defaults to the
                    current working directory.
    -c <context>    Filename of the main context file. Defaults to context.yaml.
    -t <templates>  Filename of the directory containing the template files.
                    Defaults to templates.
    -i <index>      Filename of the directory index file. Defaults to
                    index.html.
    -I <ignore>     File containing filename patterns to ignore. Defaults to
                    .ignore.
    -r <root>       Name of the folder containing the document root. Defaults to
                    root.
    -b <build>      Name of the ouput folder. Defaults to build.
    -f              Delete all contents of the build folder if exists without
                    asking.
    -S              Run HTTP server.
    -P <port>       HTTP port number. Default: 5000.
    -h              Print this text.

"""

import os
import sys
import re
from docopt import docopt
import yaml
import shutil
import markdown
from docutils.core import publish_parts
from jinja2 import Environment, PackageLoader, FileSystemLoader
from copy import deepcopy
import datetime
import SimpleHTTPServer
import SocketServer
import socket
import gzip
import json
import pprint

def regex_replace(s, find, replace):
    return re.sub(find, replace, s)

def pretty_print(x):
    return json.dumps(x, indent=1)


def regex_replace(s, find, replace):
    return re.sub(find, replace, s)

class Supernum(object):
    feed_len = 10
    default_template = 'page.html'
    separator = '@@'
    ignore_patterns = []
    urls = []
    blog_posts = None

    decoders = {
        'rst': lambda x: publish_parts(x, writer_name='html')['html_body'],
        'md': lambda x: markdown.markdown(x.decode('utf-8')),
        'html': lambda x: x.decode('utf-8'),
    }

    def __init__(self, context='context.yaml', templates='templates',
        index='index.html', root='root', build='build', ignore='.ignore',
        force=False, port=5000):

        self.index = index
        self.root_dir = root
        self.build_dir = build
        self.force = force
        self.templates = templates
        self.ignore = ignore
        self.port = port

        # Create Jinja2 environment
        self.env = Environment(loader=FileSystemLoader(templates))
        self.env.filters['regex_replace'] = regex_replace
        self.env.filters['pprint'] = pretty_print

        # Load base context
        fd = open(context, 'rb')
        self.context = yaml.load(fd.read())
        fd.close()

        if self.context is None:
            self.context = {}

        self.load_ignore_patterns()
        self.load_globals()

    def load_globals(self):
        self.context.update({
            'now': datetime.datetime.now(),
            'today': datetime.date.today().strftime("%Y-%m-%d"),
        })

    def load_ignore_patterns(self):
        if not os.path.isfile(self.ignore):
            return

        fp = open(self.ignore, 'rb')
        for line in fp.readlines():
            self.ignore_patterns.append(re.compile(line.strip(), re.IGNORECASE))
        fp.close();

    def ignore_path(self, path):
        for r in self.ignore_patterns:
            if r.search(os.path.basename(path)):
                return True
        return False

    def read(self, path):
        fd = open(path, 'rb')
        content = fd.read()
        fd.close()

        return content

    def write(self, path, content):
        fd = open(path, 'wb')
        fd.write(content)
        fd.close()

    def makedir(self, path):
        if os.path.exists(path):
            if not self.force:
                self.force = raw_input(path + ' exists. Contents will be deleted. Continue [yes/no]? ') == 'yes'
            if not self.force:
                sys.exit(0)

            shutil.rmtree(path)

        os.makedirs(path)

    def create_context(self, text):
        context = deepcopy(self.context)
        context.update(yaml.load(text))
        return context

    def parse_file(self, text, decoder):
        parts = text.split(self.separator)
        meta = parts.pop(0)
        blocks = []

        for part in parts:
            name = part[:part.index('\n')].strip()
            block = decoder(part[part.index('\n'):].strip())
            blocks.append((name, block))

        context = self.create_context(meta)

        return (context, blocks)

    def load_template(self, context):
        filename = context.get('template', self.default_template)

        path = os.path.join(self.templates, filename)

        fd = file(path, 'rb')
        template = fd.read().decode('utf-8')
        fd.close()

        return template
        

    def create_template(self, extends, context, blocks):
        template  = "{%% extends '%s' %%}\n" % extends

        for block, content in blocks:
            template += '\n'
            template += '{%% block %s %%}\n' % block
            template += content
            template += '\n{% endblock %}'

        return template

    def render(self, content, decoder, ref):
        context, blocks = self.parse_file(content, decoder)
        context.update(ref)

        extends = context.get('template', self.default_template)
        template = self.create_template(extends, context, blocks)

        #pprint.pprint(context)

        return self.env.from_string(template).render(context).encode('utf-8')

    def render_yaml(self, content, ref):
        context = self.create_context(content)
        context.update(ref)

        filename = context.get('template', self.default_template)
        template = self.env.get_template(filename)

        return template.render(context).encode('utf-8')

    def colored(self, msg, color):
        colors = {
            'red': '\033[91m',
            'green': '\033[92m',
            'yellow': '\033[93m',
            'cyan': '\033[94m',
        }
        print(colors[color] + '>> ' + '\033[0m' + msg)

    def create_ref(self, base, dst):
        filename = os.path.basename(dst)
        path = os.path.join(base, filename)
        slug = '-'.join(filename.split('.')[:-1]).lower()

        return {
            'filename': filename,
            'path': path,
            'slug': slug,
        }

    def fabricate(self, src, dst, path):
        filetype = src.split('.')[-1]
        modify = filetype in self.decoders.keys() + ['yaml',]
        ref = None

        if self.ignore_path(src):
            self.colored(src, 'red')
            return

        if modify:
            self.colored(src, 'green')
        else:
            self.colored(src, 'cyan')

        fd = file(src, 'rb')
        content = fd.read()
        fd.close()

        if filetype == 'yaml':
            dst = dst[:-len(filetype)] + 'html'
            ref = self.create_ref(path, dst)
            content = self.render_yaml(content, ref)
            modified = True

        elif filetype in self.decoders.keys():
            dst = dst[:-len(filetype)] + 'html'
            ref = self.create_ref(path, dst)
            decoder = self.decoders[filetype]
            content = self.render(content, decoder, ref)
            modified = True

        # self.urls are used to create sitemap.xml
        if ref:
            if ref['path'][0] == '/':
	        url = ref['path']
            else:
	        url = '/' + ref['path']

            if url.split('/')[-1] == 'index.html':
                url = '/'.join(url.split('/')[:-1])

            if url not in self.context.get('sitemap_hidden', []):
               self.urls.append(self.context['hostname'] + url)

        self.write(dst, content)

    def walk(self):
        for base, dirs, files in os.walk(self.root_dir):
            path = base[len(self.root_dir):]
            output = self.build_dir + path

            for d in dirs:
                self.makedir(os.path.join(output, d))

            for f in files:
                src = os.path.join(base, f)
                dst = os.path.join(output, f)
                
                self.fabricate(src, dst, path)

    def blog_index(self):
        blog_dir = os.path.join(self.root_dir, 'blog')
        posts = []

        if not os.path.isdir(blog_dir):
            return

        for base, dirs, files in os.walk(blog_dir):
            for f in files:
                if self.ignore_path(f):
                    continue
                path = os.path.join(base, f)
                fp = open(path, 'rb')
                content = fp.read().decode('utf-8')
                fp.close()

                href = base[len(self.root_dir):] + '/' + '.'.join(f.split('.')[:-1]) + '.html'
                meta = yaml.load(content.split(self.separator)[0])

                published = meta.get('published')
                if published:
                    meta['published'] = datetime.datetime.strptime(published, "%d.%m.%Y")
                    meta['href'] = href
                    posts.append(meta)

        if len(posts):
             posts = sorted(posts, key=lambda x: x['published'], reverse=True)

        self.blog_posts = posts
        self.context['blog_posts'] = posts

    def sitemap(self):
        path = os.path.join(self.build_dir, 'sitemap.xml.gz')
        self.colored(path, 'yellow')

        lines = [
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
        ] + ['  <url><loc>' + x + '</loc></url>' for x in self.urls] + [
            '</urlset>',
        ]
        content = '\n'.join(lines)

        with gzip.open(path, 'wb') as f:
            f.write(content)

    def feed(self):
        rss = self.context.get('rss')
        path = os.path.join(self.build_dir, 'feed.xml')

        if not rss:
            self.colored(path, 'red')
            return

        self.colored(path, 'yellow')

        lines = [
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
            '  <channel>',
            '    <atom:link href="' + self.context['hostname'] + '/feed.xml" rel="self" type="application/rss+xml" />',
            '    <title>' + rss['title'] + '</title>',
            '    <link>' + rss['link'] + '</link>',
            '    <description>' + rss['description'] + '</description>',
        ]

        for post in self.blog_posts[:self.feed_len]:
            if not post.get('published'):
                continue

            lines += [
                '    <item>',
                '      <title>' + post['title'] + '</title>',
                '      <link>' + self.context['hostname'] + post['href'] + '</link>',
                '      <guid>' + self.context['hostname'] + post['href'] + '</guid>',
                '      <description>' + post['description'] + '</description>',
                '    </item>',
            ]

        lines += [
            '  </channel>',
            '</rss>',
        ]

        content = '\n'.join(lines).encode('utf-8')

        f = open(path, 'w')
        f.write(content)
        f.close()

    def build(self):
        self.makedir(self.build_dir)
        self.blog_index()
        self.walk()
        self.sitemap()
        self.feed()

    def server(self):
        class TCPServer(SocketServer.TCPServer):
            def server_bind(self):
                self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                self.socket.bind(self.server_address)

        os.chdir(self.build_dir)
        Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
        httpd = TCPServer(("", self.port), Handler)

        print "running on http://127.0.0.1:%s" % self.port
        try:
           httpd.serve_forever()
        except KeyboardInterrupt:
           pass


if __name__ == '__main__':
    args = docopt(__doc__)

    if args['<workdir>']:
        os.chdir(args['<workdir>'])

    kwargs = {}
    for key in ['context', 'templates', 'root', 'build', 'ignore', 'port']:
        try:
            v = args['<' + key + '>']
        except KeyError:
            pass
        else:
            kwargs[key] = v

    if args['-f']:
        kwargs['force'] = True

    snum = Supernum(**kwargs)

    snum.build()

    if args['-S']:
        snum.server()
