#!/usr/bin/env python

# Copyright 2003, 2005, 2006, 2010 David Fifield
# This program is free software; you may use, modify and redistribute it
# without restriction.
# See http://www.bamsoftware.com/hacks/gallery/index.html.

import codecs, errno, os, shutil, sys, re, getopt
import Image

class options (object):
    pass

options.indir = u"."
options.outdir = u"."
options.force = False
options.dry_run = False

class image_record (object):
    def __init__(self, filename, caption):
        self.filename = filename
        self.caption = caption

def chomp(s):
    if s.endswith("\n"):
        s = s[:-1]
    return s

def backslash_lines(f):
    """An iterator that returns the lines of a file. Lines may be continued with
    a backslash."""
    parts = []
    while True:
        line = f.readline()
        if not line:
            break
        line = chomp(line)
        if line.endswith("\\"):
            line = line[:-1]
            parts.append(line)
        else:
            yield "".join(parts + [line])
            parts = []
    if parts:
        yield "".join(parts)

def older_or_nonexistent(filename, base_filename):
    base_mtime = os.stat(base_filename).st_mtime
    try:
        return os.stat(filename).st_mtime < base_mtime
    except OSError, e:
        if e.errno != errno.ENOENT:
            raise
        return True

class configuration (object):
    def set_variable(self, varname, value):
        if varname in (u"filename", u"title", u"prologue", u"epilogue"):
            setattr(self, varname, value)
        elif varname in (u"table_width", u"cell_width", u"cell_height",
            u"cell_padding", u"cell_spacing", u"medium_size"):
            setattr(self, varname, int(value))
        else:
            raise ValueError(u"Unknown variable name \"%s\"" % varname)

    def __init__(self, conf_filename, reldir, parent = None):
        self.conf_filename = conf_filename
        self.reldir = reldir
        if parent:
            self.filename = parent.filename
            self.title = parent.title
            self.table_width = parent.table_width
            self.cell_width = parent.cell_width
            self.cell_height = parent.cell_height
            self.cell_padding = parent.cell_padding
            self.cell_spacing = parent.cell_spacing
            self.medium_size = parent.medium_size
            self.parent = parent
        else:
            self.filename = u"index.html"
            self.title = u"Gallery"
            self.table_width = 700
            self.cell_width = 200
            self.cell_height = 200
            self.cell_padding = 0
            self.cell_spacing = 5
            self.medium_size = 800
            self.parent = None
        self.prologue = None
        self.epilogue = None
        self.images = []
        self.links = []

        f = codecs.open(os.path.join(options.indir, reldir, conf_filename), "r", "utf8")
        for line in backslash_lines(f):
            # Skip comments and blank lines.
            if re.match(r"\s*($|#)", line):
                continue
            # Assign variables.
            m = re.match(r"set\s+(\w+)\s*=\s*(.*)", line)
            if m:
                (name, value) = m.groups()
                self.set_variable(name, value)
                continue
            # Define an image.
            m = re.match(r"image\s+(?:\"([^\"]+)\"|(\S+))\s*(.*)", line)
            if m:
                (quotedname, name, caption) = m.groups()
                image = image_record(quotedname or name, caption)
                self.images.append(image)
                continue
            # Define a link.
            m = re.match(r"link\s+(.*)", line)
            if m:
                (dest,) = m.groups()
                self.links.append(dest)
                continue
            raise ValueError(u"Invalid line in %s: %s" % (conf_filename, line))
        f.close()

    def outdated(self, reldir):
        return older_or_nonexistent(os.path.join(options.outdir, reldir, self.filename),
            os.path.join(options.indir, reldir, self.conf_filename))

    def image_rows(self):
        row = []
        width = 0
        for image in self.images:
            next_width = width + self.cell_width
            if next_width > self.table_width:
                yield row
                row = []
                next_width = self.cell_width
            row.append(image)
            width = next_width
        if row:
            yield row

def makedirs(dir):
    try:
        os.makedirs(dir)
    except OSError, e:
        if e.errno != errno.EEXIST:
            raise

def constrain_dims(xy, wh):
    (x, y) = xy
    (w, h) = wh
    x_ratio = float(x) / w
    y_ratio = float(y) / h
    if x_ratio > 1.0 and x_ratio >= y_ratio:
        x = w
        y = int(round(y / x_ratio))
    elif y_ratio > 1.0 and y_ratio >= x_ratio:
        x = int(round(x / y_ratio))
        y = h
    return (x, y)

def image_resize(image, filename, suff, reldir, size):
    """Resizes the given image if necessary and writes a copy to outdir/reldir.
    Returns a 2-tuple: file name of the output file (relative to outdir/reldir)
    and the size of the output image."""
    size = constrain_dims(image.size, size)
    if image.size == size:
        out_filename = filename
    else:
        name, ext = os.path.splitext(filename)
        out_filename = name + suff + ext

    abs_in_filename = os.path.join(options.indir, reldir, filename)
    abs_out_filename = os.path.join(options.outdir, reldir, out_filename)
    if not options.force and not older_or_nonexistent(abs_out_filename, abs_in_filename):
        try:
            out = Image.open(abs_out_filename)
            if out.size == size:
                return out_filename, size
        except IOError, e:
            pass

    makedirs(os.path.dirname(abs_out_filename))
    print abs_out_filename
    if image.size == size:
        if not options.dry_run:
            shutil.copyfile(os.path.join(options.indir, reldir, filename), abs_out_filename)
    else:
        resized = image.resize(size, Image.ANTIALIAS)
        if not options.dry_run:
            resized.save(abs_out_filename)

    return out_filename, size

def format_imagecount(count):
    if count == 1:
        return u"%d image" % count
    else:
        return u"%d images" % count

def relpath(base, target):
    base_split = os.path.split(base)
    target_split = os.path.split(target)
    b = 0
    t = 0
    path = []
    while b < len(base_split) and t < len(target_split) and base_split[b] == target_split[t]:
        b += 1
        t += 1
    while b < len(base_split):
        path.append(u"..")
        b += 1
    while t < len(target_split):
        path.append(target_split[t])
        t += 1
    return os.path.join(*path)

def html(str):
    str = str.replace(u"&", u"&amp;")
    str = str.replace(u"<", u"&lt;")
    str = str.replace(u">", u"&gt;")
    str = str.replace(u"\"", u"&quot;")
    return str

def gallery(conf_filename, reldir = "", parent = None):
    sub_confs = []

    conf = configuration(conf_filename, reldir, parent)
    outdated = options.force or conf.outdated(reldir)

    for sub_conf_filename in conf.links:
        assert(not os.path.isabs(sub_conf_filename))
        sub_conf_basename = os.path.basename(sub_conf_filename)
        sub_conf_dirname = os.path.dirname(sub_conf_filename)
        sub_conf, old = gallery(sub_conf_basename, os.path.join(reldir, sub_conf_dirname), conf)
        if old:
            outdated = True
        sub_confs.append(sub_conf)

    if not outdated:
        return conf, outdated

    images = []
    for image in conf.images:
        assert(not os.path.isabs(image.filename))

        im_filename = os.path.join(options.indir, reldir, image.filename)
        im = Image.open(im_filename)

        image.sm_filename, image.sm_size = image_resize(im, image.filename, "-sm", reldir, (conf.cell_width, conf.cell_height))
        image.md_filename, image.md_size = image_resize(im, image.filename, "-md", reldir, (conf.medium_size, conf.medium_size))

    if not options.dry_run:
        makedirs(os.path.join(options.outdir, reldir))
    output_filename = os.path.join(options.outdir, reldir, conf.filename)
    print output_filename

    if options.dry_run:
        f = codecs.open(u"/dev/null", "w", "utf8")
    else:
        f = codecs.open(output_filename, "w", "utf8")
    print >> f, u"""\
<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"
 \"http://www.w3.org/TR/html4/loose.dtd\">
<html>
<head>
<title>%(title)s</title>
</head>
<body>
<h1 align=center>%(title)s</h1>""" % {"title": html(conf.title)}

    if conf.prologue:
        print >> f, u"""\
<table align=center width=%(table_width)d cellpadding=0 cellspacing=0 border=0>
  <tr>
    <td align=center>
      <table cellpadding=5 cellspacing=0 border=2>
        <tr><td>%(prologue)s</td></tr>
      </table>
    </td>
  </tr>
</table>""" % {"table_width": conf.table_width, "prologue": conf.prologue}

    if sub_confs:
        print >> f, u"""\
<table align=center width=%(table_width)d cellpadding=0 cellspacing=0 border=0>
  <tr>
    <td>
      <ul>""" % {"table_width": conf.table_width}
        for sub_conf in sub_confs:
            print >> f, u"""\
        <li><a href="%(link)s">%(title)s</a> (%(imagecount)s)</li>\
""" % {"link": html(os.path.join(sub_conf.reldir, sub_conf.filename)),
    "title": html(sub_conf.title),
    "imagecount": html(format_imagecount(len(sub_conf.images)))}
        print >> f, u"""\
      </ul>
    </td>
  </tr>
</table>"""

    if conf.images:
        print >> f, u"""\
<table align=center cellpadding=%(cell_padding)d cellspacing=%(cell_spacing)d border=0>\
""" % {"cell_padding": conf.cell_padding, "cell_spacing": conf.cell_spacing}
        for row in conf.image_rows():
            print >> f, u"""\
  <tr>"""
            for image in row:
                print >> f, u"""\
    <td align=center valign=bottom width=%(cell_width)d><a href="%(md_filename)s"><img src="%(sm_filename)s" width=%(sm_width)d height=%(sm_height)d border=0></a></td>\
""" % {"cell_width": conf.cell_width,
    "md_filename": image.md_filename, "sm_filename": image.sm_filename,
    "sm_width": image.sm_size[0], "sm_height": image.sm_size[1]}
            print >> f, u"""\
  </tr>
  <tr>"""
            for image in row:
                print >> f, u"""\
    <td align=center valign=top width=%(cell_width)d>%(caption)s</td>\
""" % {"cell_width": conf.cell_width, "caption": image.caption}
            print >> f, u"""\
  </tr>"""
        print >> f, u"""\
</table>"""

    if conf.parent:
        print >> f, u"""\
<table align=center width=%(table_width)d cellpadding=0 cellspacing=0 border=0>
  <tr>
    <td>
      <a href="%(parent_path)s">%(parent_title)s</a>
    </td>
  </tr>
</table>""" % {"table_width": conf.table_width,
    "parent_path": relpath(reldir, os.path.join(parent.reldir, parent.filename)),
    "parent_title": html(parent.title)}

    if conf.epilogue:
        print >> f, u"""\
<table align=center width=%(table_width)d cellpadding=0 cellspacing=0 border=0>
  <tr>
    <td align=center>
      <table cellpadding=5 cellspacing=0 border=0>
        <tr><td>%(epilogue)s</td></tr>
      </table>
    </td>
  </tr>
</table>""" % {"table_width": conf.table_width, "epilogue": conf.epilogue}

    print >> f, u"""\
</body>
</html>"""

    f.close()

    return conf, outdated


opts, filenames = getopt.getopt(sys.argv[1:], "i:fo:n", ["input=", "force", "output=", "dry-run"])

for opt, arg in opts:
    if opt in ("-i", "--input"):
        options.indir = arg
    elif opt in ("-o", "--output"):
        options.outdir = arg
    elif opt in ("-f", "--force"):
        options.force = True
    elif opt in ("-n", "--dry-run"):
        options.dry_run = True

if not filenames:
    filenames = [u"gallery.def"]

for filename in filenames:
    gallery(filename)

