#! /usr/bin/env python
# coding=utf-8

'''
Generate web site
'''

import sys
reload(sys)
sys.setdefaultencoding('UTF8')

import os
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
import subprocess
import re
import StringIO
import traceback
import shutil
import copy
import yaml
import entwinelib

RETAG = re.compile(r'({{.*?}})', re.DOTALL)
REINDENT = re.compile(r'^( *)')
DATEFMT = '%-d %b %Y'

HEAD0 = r'<?xml version="1.0" encoding="UTF-8"?>'
HEAD1 = r'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'

def applytemplate(globs, body):
    '''
    Apply Python block template
    '''

    matches = RETAG.split(body)
    for i, chunk in enumerate(matches):
        if chunk.startswith('{{'):
            firstline = True
            lines = []
            for line in chunk[2:-2].splitlines():
                if line:
                    if firstline:
                        indent = len(REINDENT.match(line).group(1))
                        firstline = False
                    lines.append(line[indent:])

            sys.stdout = StringIO.StringIO()
            try:
                exec('\n'.join(lines), globs) # pylint: disable=exec-used
            except Exception: # pylint: disable=broad-except
                print >> sys.stderr, \
                    '\n'.join(traceback.format_exc().splitlines()[3:])
            matches[i] = sys.stdout.getvalue()
            sys.stdout = sys.__stdout__ # pylint: disable=redefined-variable-type

    return ''.join(matches)

def sitemaps(config, outdir, inrootdir, dirpaths, dryrun):
    '''
    Write site maps
    '''

    olddirpath = os.getcwd()
    os.chdir(outdir)

    for filename in config['sitemaps']['files']:
        print "Writing %s/%s" % (os.getcwd(), filename)
        if not dryrun:
            with open(filename, 'w') as fhl:
                print >> fhl, HEAD0
                print >> fhl, HEAD1
                print >> fhl, r'  <url><loc>%s</loc></url>' % \
                    config['sitemaps']['files'][filename]
                for url in config['sitemaps']['urls']:
                    print >> fhl, r'  <url><loc>%s/%s</loc></url>' % \
                        (config['sitemaps']['files'][filename], url)
                    try:
                        dirpaths.remove(inrootdir + '/' + url)
                    except ValueError:
                        print "%s in sitemap doesn't exist" % url
                print >> fhl, r'</urlset>'

    os.chdir(olddirpath)

    # Print paths omitted from sitemap
    for dirpath in dirpaths:
        if dirpath[len(inrootdir):] and \
            dirpath[len(inrootdir) + 1:] not in config['sitemaps']['exclude']:
            print "%s is missing from sitemap" % dirpath[len(inrootdir):]

def styles(config, cfgdir, dirs, dryrun):
    '''
    Write style sheet
    '''

    if 'styles' in config:
        olddirpath = os.getcwd()
        os.chdir(dirs['out'])

        inpath = cfgdir + '/' + config['styles']['in']
        outpath = config['styles']['out']

        if not os.path.lexists(outpath) or \
            entwinelib.getmtime(inpath) > entwinelib.getmtime(outpath):
            print "Writing %s/%s" % (os.getcwd(), outpath)
            if not dryrun:
                subprocess.call(['sass', inpath, outpath])

        os.chdir(olddirpath)

def getlatest(path):
    '''
    Get modification time for last-updated file or directory
    '''

    upd = 0
    for dirpath, dirnames, filenames in os.walk(path):
        for filename in filenames + dirnames:
            mtime = entwinelib.getmtime(dirpath + '/' + filename)
            upd = mtime if mtime > upd else upd

    return upd

def markdown(cfgdir, # pylint: disable=too-many-locals, too-many-statements, too-many-branches, too-many-arguments
             globs, meta, tmplmtime, inrootdir, outrootdir, indir,
             infilename, outfilename,
             force, dryrun):
    '''
    Run Markdown (and Python beforehand)
    '''

    # Move to input directory
    olddirpath = os.getcwd()
    os.chdir(indir)
    dirpath = indir[len(inrootdir):]
    outdir = outrootdir + dirpath
    outfile = outdir + '/' + outfilename

    # Check last change in this subtree
    if not os.path.isfile(outfile) or force or \
        getlatest('.') > entwinelib.getmtime(outfile) or \
        tmplmtime > entwinelib.getmtime(outfile):

        print "Writing " + outfile

        # Load Markdown file
        filemeta, body = entwinelib.loadmd(infilename)

        # Override defaults in metadata
        if filemeta:
            for key in filemeta:
                if key == 'options':
                    for subkey in filemeta[key]:
                        if key in meta:
                            meta[key][subkey] = filemeta[key][subkey]
                        else:
                            meta[key] = filemeta[key]
                else:
                    meta[key] = filemeta[key]

        # Write more metadata
        if meta is None:
            meta = {}
        if 'path' not in meta:
            meta['path'] = dirpath

        # Build options
        opts = []
        if 'options' in meta:
            for opt in meta['options']:
                if isinstance(meta['options'][opt], bool):
                    if meta['options'][opt]:
                        opts.append('--' + opt)
                else:
                    opts.append('--%s=%s' % (opt, meta['options'][opt]))

        # Set more globals
        globs['outdir'] = outdir
        globs['meta'] = meta

        # Run inline Python
        body = applytemplate(globs, body)

        # Edit metadata for Pandoc's use of it in templates
        if 'date' in meta:
            try:
                meta['date'] = meta['date'].strftime(DATEFMT)
            except AttributeError:
                pass

        # Write Pandoc temporary template
        with open(cfgdir + '/template.html') as fhlr:
            with open(cfgdir + '/template.tmp.html', 'w') as fhlw:
                fhlw.write(applytemplate(globs, fhlr.read()))

        # Write Pandoc temporary input file because using stdin is unreliable
        with open(cfgdir + '/input.tmp.md', 'w') as fhl:
            fhl.write(yaml.dump(meta,
                                default_flow_style=False,
                                explicit_start=True,
                                explicit_end=True,
                                width=1024) + body)

        # Run Markdown
        proc = subprocess.Popen(['pandoc', '-thtml5',
                                 '--template',
                                 cfgdir + '/template.tmp.html'] + \
                                opts + [cfgdir + '/input.tmp.md'],
                                stdout=subprocess.PIPE)

        out, _ = proc.communicate()

        # Write HTML
        if not dryrun:
            with open(outfile, 'w') as fhl:
                fhl.write(out)

    # Step back
    os.chdir(olddirpath)

def abspath(path):
    '''
    Return absolute path from user-expanded path
    '''

    return os.path.abspath(os.path.expanduser(path))

def main(): # pylint: disable=too-many-locals
    '''
    Main loop
    '''

    # Parse arguments
    parser = ArgumentParser(description="generate web site",
                            formatter_class=ArgumentDefaultsHelpFormatter)
    parser.add_argument('--cfgdir', '-c', help="configuration directory",
                        default='~/.entwine', type=abspath)
    parser.add_argument('--indir', '-i', help="input page directory",
                        type=os.path.expanduser)
    parser.add_argument('--outdir', '-o', help="output page directory",
                        type=os.path.expanduser)
    parser.add_argument('--dryrun', '-n',
                        help="don't write anything except directories",
                        action='store_true')
    parser.add_argument('--force', '-f',
                        help="update all files even if there's no need to",
                        action='store_true')
    args = parser.parse_args()

    # Configuration
    with open(args.cfgdir + '/entwine.yaml') as fhl:
        config = yaml.load(fhl)
    dirs = config['directories']

    # Load globals
    globs = {
        '__params__': {},
        'os': os,
        'entwinelib': entwinelib,
    }
    if os.path.exists(args.cfgdir + '/modules'):
        sys.path.append(args.cfgdir + '/modules')
        for modulefile in os.listdir(args.cfgdir + '/modules'):
            if modulefile.endswith('.py'):
                module = __import__(modulefile[:-3])
                globs[modulefile[:-3]] = module

    # Get template modification time
    tmplmtime = entwinelib.getmtime(args.cfgdir + '/template.html')

    # Set input and output directories
    inrootdir = args.indir if args.indir else abspath(dirs['in'])
    outrootdir = args.outdir if args.outdir else abspath(dirs['out'])

    # Compile exclude regular expression
    exclude = re.compile(config['files']['exclude'])

    # Inventory paths to sort them later on and make files and directories
    # which don't need to bear time changes.
    # It's fine to start at root because directories and static files don't
    # need to bear time changes, only templated files do.
    dirpaths = []
    for indir, _, filenames in os.walk(inrootdir):

        olddirpath = os.getcwd()
        os.chdir(indir)

        outpath = outrootdir + indir[len(inrootdir):]

        # Make directories
        if not os.path.lexists(outpath):
            print "Making %s/" % outpath
            os.mkdir(outpath)

        # Inventory templated files and copy static files
        for filename in filenames:
            if filename == config['files']['in']:
                dirpaths.append(os.getcwd())
            else:
                if not exclude.match(filename) and \
                    (not os.path.lexists(outpath + '/' + filename) or \
                    args.force or \
                    entwinelib.getmtime(filename) > \
                        entwinelib.getmtime(outpath + '/' + filename)):

                    print "Copying to %s/%s" % (outpath, filename)
                    if not args.dryrun:
                        shutil.copy(filename, outpath)

        os.chdir(olddirpath)

    # Sort paths deepest first to collect changed times, run Markdown on files
    for indir in sorted(dirpaths, key=lambda p: p.count('/'), reverse=True):
        markdown(args.cfgdir, globs, copy.deepcopy(config['meta']), tmplmtime,
                 inrootdir, outrootdir, indir,
                 config['files']['in'], config['files']['out'],
                 args.force, args.dryrun)

    # Write supporting files
    sitemaps(config, outrootdir, inrootdir, dirpaths, args.dryrun)
    styles(config, args.cfgdir, dirs, args.dryrun)

    # Remove Pandoc temporary files
    for filename in '/template.tmp.html', '/input.tmp.md':
        try:
            os.remove(args.cfgdir + filename)
        except OSError:
            pass

if __name__ == '__main__':
    sys.exit(main())
