#! /usr/bin/env python3

"""\
emin - a static web gallery builder

emin makes static Web pages for presenting lots of imagey things:
photos, PDFs, graphs with thumbnails as well as links to the image/doc
file proper.

It's primarily intended for making Web photo galleries for the sorts
of people who don't want to install some PHP+MySQL+Node.js monstrosity
just to put their photos online. On the assumption that most people
will want to tweak their gallery's appearance, the output is fully
customisable using the Cheetah templating engine.

Supported image formats are JPEG, PNG, GIF, TIFF, PDF and EPS, with
the latter three being converted to PNG for Web display.  Image
resizing, renaming and thumbnailing is supported, as is building a zip
file to download the whole set. Large image sets can be split over
several pages.

As for the name, this is a program to make pretty simple galleries, so
it's named after a pretty crappy artist. And, thankfully, e-m-i-n is
not many characters to type.

TODO:
 * Copy Lightbox stuff into place
 * Make Cheetah templating optional, or use Genshi/Jinja/Mako?
 * Clean up image filenames to avoid/minimise duplicate extensions
 * Try to validate the HTML output
 * Add all on one page option
 * Resize option
 * Rename option
 * Crop-to-thumb option
 * Auto-rotate by EXIF orientation
 * Allow complete rollback if any failure (or on demand?)

"""

import emin, logging, argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("SRCDIR", default=".", help="folder of images to build into a gallery")
parser.add_argument("OUTDIR", nargs="?", default="gallery", help="output gallery directory")
parser.add_argument("-t", "--title", dest="TITLE", default=None,
                  help="title of this gallery")
parser.add_argument("--template", dest="TEMPLATE", default=None,
                  help="specify the template file to be used for the index pages")
parser.add_argument("--zipfile", dest="ZIPFILE", default=None,
                  help="name of zip archive file. Default is based on the title.")
parser.add_argument("--no-zipfile", action="store_false", dest="WRITE_ZIPFILE", default=True,
                  help="disable writing out of a zipped archive of photos from this gallery")
parser.add_argument("-1", "--one-page", dest="ONE_PAGE", action="store_true", default=False,
                  help="put all thumbnails on one page (default: %(default)s)")
parser.add_argument("-c", "--num-cols", "--cols", dest="NUM_COLS", default=5, type=int,
                  help="max number of thumbnail columns on one page (default: %(default)s)")
parser.add_argument("-r", "--num-rows", "--rows", dest="NUM_ROWS", default=6, type=int,
                  help="max number of thumbnail rows on one page (default: %(default)s). Set < 1 for unlimited (i.e. all on one page)")
parser.add_argument("--thumb-height", dest="THUMB_HEIGHT", default=350, type=int,
                  help="thumbnail height, in pixels (default: %(default)s)")
parser.add_argument("--max-imgsize", dest="MAX_IMGSIZE", default=800, type=int,
                  help="max large image dimension in pixels (default: %(default)s)")
parser.add_argument("--exclude", dest="EXCLUDE", default=None,
                  help="a regex pattern specifying image files to be excludes (default: %(default)s)")
parser.add_argument("--no-js", dest="USE_JS", action="store_false", default=True,
                  help="disable use of funky JavaScript display stuff")
parser.add_argument("--force", dest="FORCE", action="store_true", default=False,
                  help="force creation of gallery: regen thumbnails etc.")
parser.add_argument("--no-table", dest="USE_TABLE", action="store_false", default=True,
                  help="don't use an HTML table for thumbnail presentation: just let the thumbs flow into the browser window")
parser.add_argument("-v", "--verbose", action="store_const", const=logging.DEBUG, dest="LOGLEVEL", default=logging.INFO,
                    help="print debug (very verbose) messages")
parser.add_argument("-q", "--quiet", action="store_const", const=logging.WARNING, dest="LOGLEVEL", default=logging.INFO,
                    help="be very quiet")
parser.add_argument("--version", action="version", version=emin.__version__)
args = parser.parse_args()
logging.basicConfig(level=args.LOGLEVEL, format="%(message)s")


## More imports
import sys, os, glob, re, math, shutil, fnmatch
try:
    from Cheetah.Template import Template
except Exception as e:
    logging.error("Couldn't import required Cheetah3 / CT3 package")
    exit(1)
try:
    import PIL.Image as PILI
except Exception as e:
    logging.error("Couldn't import required Python Imaging Library / Pillow package")
    exit(1)

## Deal with consequences of interacting optional settings
if args.ONE_PAGE:
    args.NUM_ROWS = -1
# TODO: These should be options...
args.RENAME = False
args.CONVERT = False


def safeencode(s):
    """Encode a string for use as a filename."""
    newstr = s.replace(" ", "-").replace(",", "").replace("/", "").replace(".", "")
    return newstr

## Better title autodetection
if not args.TITLE:
    args.TITLE = os.path.basename(os.path.abspath(args.SRCDIR))

logging.debug(f"Title: {args.TITLE}")
logging.debug(f"Thumb height: {args.THUMB_HEIGHT}")


class ImageInfo:
    def __init__(self):
        self.name = None
        self.path = None
        self.thumbname = None
        self.thumbpath = None
        self.thumbx = None
        self.thumby = None

    def setsize(self, sizetuple):
        self.thumbx = sizetuple[0]
        self.thumby = sizetuple[1]

    def _getsize(self):
        return self.thumbx, self.thumby

    thumbsize = property(_getsize, setsize)


## Go to the gallery directory and test if it's writeable
if not os.access(args.OUTDIR, os.W_OK):
    try:
        logging.debug(f"Making output dir in {args.OUTDIR}")
        os.makedirs(args.OUTDIR)
    except Exception as e:
        logging.error(f"Problem when making output dir {args.OUTDIR}... exiting")
        exit(1)


## Make thumbnail directory if needed
args.THUMBDIR = "thumbs"
THUMBDIR = os.path.join(args.OUTDIR, args.THUMBDIR)
try:
    if not os.path.isdir(THUMBDIR):
        logging.debug(f"Making thumbs dir in {THUMBDIR}")
        os.makedirs(THUMBDIR)
except Exception as e:
    logging.error("Problem when making thumbnails dir... exiting")
    exit(1)


## Build the list of pictures to display
# TODO: types: PNG/GIF, JPEG, PDF
# TODO: match formats & store thumb filenames
EXTENSIONS = \
    ["*.jpg", "*.jpeg"] + \
    ["*.png", "*.gif"]  + \
    ["*.tif", "*.tiff"] + \
    ["*.eps", "*.pdf"]
imgs = []
for img in os.listdir(args.SRCDIR):
    for e in EXTENSIONS:
        if not fnmatch.fnmatch(img.lower(), e):
            continue
        if args.EXCLUDE and re.search(args.EXCLUDE, img):
            continue
        imgpath = os.path.join(args.SRCDIR, img)
        imgs.append(imgpath)
        break

## Count the pictures
logging.debug("Number of pictures = %d" % len(imgs))
if len(imgs) == 0:
    logging.debug("No pictures from which to build a gallery...")
    exit(2)
logging.debug("Images: " + str(sorted(imgs)))


## Rename/move if needed
outimgs = []
for n, imgpath in enumerate(imgs):
    imgname = os.path.basename(imgpath)
    imgnameparts = os.path.splitext(imgname)
    targetname = imgname
    if args.RENAME:
        targetname = "%s-%03d%s" % (safename(args.OUTDIR), n, imgnameparts[1])
    targetpath = os.path.join(args.OUTDIR, targetname)
    outimgs.append(targetpath)
    if imgpath != targetpath:
        logging.debug("Copying %s -> %s" % (imgpath, targetpath))
        import shutil
        shutil.copy(imgpath, targetpath)


## Process images
logging.info("Processing {:d} images...".format(len(outimgs)))
imgsinfo = {}
for picpath in outimgs:
    picname = os.path.basename(picpath)
    picbase = os.path.splitext(picname)[0]

    ## Convert EPS, PDF, TIFF to Web-viewable formats
    picversions = {}
    picpathparts = os.path.splitext(picpath)
    picnameparts = os.path.splitext(picname)
    extn = picnameparts[1].lower()
    pictype = extn[1:].upper(); pictype = "TIFF" if pictype == "TIF" else pictype
    picversions[pictype] = picname
    convcmd = None
    if pictype in ["TIFF", "EPS"]:
        newpictype = "PNG" if pictype == "EPS" else "JPG"
        newextn = "." + newpictype.lower()
        newpicname = picname + newextn
        newpicpath = picpath + newextn
        picversions[newpictype] = newpicname
        logging.debug("Converting {} to {}".format(picpath, newpicpath))
        img = PILI.open(picpath, "r")
        img.save(newpicpath)
        picname = newpicname
        picpath = newpicpath
    elif extn in [".pdf"]:
        newpicname = picname + ".png"
        newpicpath = picpath + ".png"
        picversions["PNG"] = newpicname
        logging.debug("Converting {} to {}".format(picpath, newpicpath))
        try:
            import pdf2image, tempfile
            with tempfile.TemporaryDirectory() as tmppath:
                pdfimgs = pdf2image.convert_from_path(picpath, output_folder=tmppath, first_page=0, last_page=1) #, fmt="png")
                #pdfimgs = pdf2image.convert_from_path(picpath, output_folder=tmppath)
                pdfimg = pdfimgs[0].save(newpicpath)
                if len(pdfimgs) > 1:
                    logging.warning("Multi-page PDF {}: showing only page 1".format(picpath))
        except ImportError:
            import subprocess
            try:
                convcmd = ["convert", "-density", "200", "-resize", "800x700", picpath, newpicpath]
                subprocess.check_call(convcmd)
            except:
                logging.error("PDF conversion failed for {}... skipping".format(picpath))
                continue
        picname = newpicname
        picpath = newpicpath

    ## Main pic info
    info = ImageInfo()
    info.name = picname
    info.path = picpath
    info.versions = picversions

    ## Thumb info
    ## TODO: Un-hard-code PNG thumb format
    thumbname = picname + ".png"
    thumbpath = os.path.join(THUMBDIR, thumbname)
    info.thumbname = thumbname
    info.thumbpath = thumbpath

    ## Make thumbnail
    ## TODO: Be lazy!
    #if args.FORCE or not os.access(thumbpath, os.R_OK) or os.stat(thumbpath).st_mtime > os.stat(pic).st_mtime:
    try:
        logging.debug(f"Making new thumbnail {thumbpath} for {picname} (max height {args.THUMB_HEIGHT:d})")
        thumbimg = PILI.open(picpath, "r")
        thumbimg.thumbnail((100000000, args.THUMB_HEIGHT)) #, resample=PILI.ANTIALIAS)
        thumbimg.save(thumbpath)
        info.thumbsize = thumbimg.size
    except Exception as e:
        logging.warning(f"Problem when making thumbnail from {picpath}:\n{e}\n... exiting")
        exit(1)

    ## Store info
    imgsinfo[picname] = info


#####################


## Calculate how many pages will be needed
if args.NUM_ROWS >= 1:
    NUM_PER_PAGE = args.NUM_ROWS * args.NUM_COLS
    NUM_PAGES = int(math.ceil( len(imgs)/float(NUM_PER_PAGE) ))
else:
    NUM_PER_PAGE = len(imgs)
    NUM_PAGES = 1


if NUM_PAGES > 1:
    logging.warn("%d gallery pages will be made. If you just want one page, use the -1 or --one-page option" % NUM_PAGES)


## TODO: Move HTML extension-setting to option parser
## (or take from template name, e.g. page.html.template -> html)
args.EXTN = "html"


def getPageFilename(pagenum):
    if pagenum == 1:
        pagefile = "index.%s" % args.EXTN
    else:
        pagefile = "index%02d.%s" % (pagenum, args.EXTN)
    return pagefile


def mkPageLinkStr(pagenum):
    "Write the linked page list"
    global NUM_PAGES
    out = ''
    if NUM_PAGES > 1:
        out += ""
        ## Previous
        prev = pagenum - 1
        if prev > 0:
            out += '<a href="%s">prev</a>' % getPageFilename(prev)
        else:
            out += 'prev'
        out += '&nbsp;'
        ## Numbers
        for n in range(1, NUM_PAGES+1):
            if n != pagenum:
                out += '<a href="%s">%d</a>' % (getPageFilename(n), n)
            else:
                out += "%d" % n
            out += '&nbsp;'
        ## Next
        next = pagenum + 1
        if next <= NUM_PAGES:
            out += '<a href="%s">next</a>' % getPageFilename(next)
        else:
            out += 'next'
    return out


## Make a zip archive
ZIPFILE = "photo-album.zip"
if True: #args.WRITE_ZIPFILE:
    logging.debug("Making zipped picture archive")
    if args.ZIPFILE is not None:
        ZIPFILE = args.ZIPFILE
    elif args.TITLE is not None or len(args.TITLE) > 0:
        ZIPFILE = safeencode(args.TITLE)
        #ZIPFILE = safename(args.OUTDIR)
    if not "." in ZIPFILE or os.path.splitextn(ZIPFILE)[1] != ".zip":
        ZIPFILE += ".zip"
    ## Do the zipping
    if ZIPFILE:
        from zipfile import ZipFile
        zf = ZipFile(os.path.join(args.OUTDIR, ZIPFILE), "w")
        for img in imgs:
            zf.write(img, os.path.basename(img))
        zf.close()
    else:
        logging.warning("No zip file made because zip filename is empty")


## Copy Lightbox stuff into ZIP
#if args.USE_JS:
#    from zipfile import ZipFile
#    zf = ZipFile(os.path.join(args.OUTDIR, "lightbox.zip"), "r")
#    for img in imgs:
#        zf.write(img, os.path.basename(img))
#    zf.close()


## Default template
tmplstr = \
'''<!DOCTYPE html>
<html>
  <head>
    #set title = $PAGETITLE
    #if $NUM_PAGES > 1
    #set title = $title + " (page %s)" % $PAGENUM
    #end if
    <title>$title</title>
    <style>
      html { font-size: larger; }
      img { border:0; padding:10 10 0 0; }
      body { padding:1em; color:#446; background:white; font-family:sans-serif; }
      h1 { font-family:sans-serif; }
      a.format { text-decoration:none; font-variant:small-caps; color:grey; font-size:small; }
      a.format:hover { color:deeppink; }
      a.format:active { color:deeppink; }
      .pagelinks { text-decoration:none; font-variant:small-caps; color:grey; margin-top:1em; margin-bottom:1em; }
      .pagelinks a:link { color:#22c; text-decoration:none; }
      .pagelinks a:hover { color:#55c; text-decoration:none; }
      .pagelinks a:active { color:#55c; text-decoration:none; }
    </style>
    #if $ARGS.USE_JS:
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lightbox3@1/dist/lightbox3.css">
    <script src="https://cdn.jsdelivr.net/npm/lightbox3@1/dist/lightbox3.min.js"></script>
    #end if
  </head>
  <body>
    <h1>$PAGETITLE</h1>
    #if $NUM_PAGES > 1
    <div class="pagelinks">Pages: $LINKSTR</div>
    #end if

    <table>
    <tr>
    #set jsrel = ''
    #if $ARGS.USE_JS:
    #set jsrel = 'data-lightbox="emin"'
    #end if
    #for n, thumb in enumerate($PAGEPICS)
      #if $n % $NUM_COLS == 0 and $n not in (0, len($PAGEPICS)-1)
      <tr/><tr>
      #end if
      #set info = $PICINFO[$thumb]
      <td style="text-align:right;">
        <a href="$info.relpath" $jsrel><img alt="$thumb" src="$info.relthumbpath" style="border:0;" width="$info.thumbx" height="$info.thumby" /></a><br/>
        #for fmt, name in $info.versions.items()
        <a class="format" href="$name">$fmt.lower()</a>
        #end for
      </td>
    #end for
    </tr>
    </table>

    #if $NUM_PAGES > 1
    <div class="pagelinks">Pages: $LINKSTR</div>
    #end if

    #if $ARGS.WRITE_ZIPFILE and $ZIPFILE:
    <p>All zipped up: <a href="$ZIPFILE">$ZIPFILE</a></p>
    #end if
  </body>
</html>
'''


## Override default template with a template file
if args.TEMPLATE is not None:
    logging.info("Using index template file %s" % args.TEMPLATE)
    tf = open(args.TEMPLATE, "r")
    tmplstr = tf.read()
    tf.close()


## Make each index page
for n in range(NUM_PAGES):
    PAGENUM = n + 1

    ## Choose and open page file
    PAGEFILE = getPageFilename(PAGENUM)
    PAGEPATH = os.path.join(args.OUTDIR, PAGEFILE)

    ## Write the title
    PAGETITLE = args.TITLE

    ## Write the linked page list
    LINKSTR = mkPageLinkStr(PAGENUM)

    ## Work out the picture offsets for this page
    pics_start = n * NUM_PER_PAGE
    pics_end = (n+1) * NUM_PER_PAGE - 1
    if pics_end >= len(imgs):
        pics_end = len(imgs) - 1

    PAGEPICS = sorted(imgsinfo.keys())[pics_start: pics_end+1]
    PAGEPICNUMS = range(len(PAGEPICS))
    relthumbdir = args.THUMBDIR
    #relthumbdir = os.path.relpath(THUMBDIR, OUTDIR)
    reloutdir = "."
    for k in imgsinfo.keys():
        imgsinfo[k].relthumbpath = os.path.normpath(os.path.join(relthumbdir, imgsinfo[k].thumbname))
        imgsinfo[k].relpath = os.path.normpath(os.path.join(reloutdir, imgsinfo[k].name))
    PICINFO = imgsinfo
#     THUMBNAMES = [t.name for t in thumbsinfo.values()[pics_start : pics_end]]
#     print THUMBNAMES
#     THUMBPATHS = [os.path.join(relthumbdir, name) for name in THUMBNAMES]
#     print THUMBPATHS
#     THUMBDIMS = [t.size for t in thumbsinfo.values()[pics_start : pics_end]]

    logging.debug("Writing to index file %s" % PAGEPATH)
    f = open(PAGEPATH, "w")
    logging.debug("Images on page: %s" % PAGEPICS)
    tdict = {}
    tdict["NUM_PAGES"] = NUM_PAGES
    tdict["NUM_PER_PAGE"] = NUM_PER_PAGE
    tdict["NUM_COLS"] = args.NUM_COLS
    tdict["PAGEPICS"] = PAGEPICS
    tdict["PAGENUM"] = PAGENUM
    tdict["PAGETITLE"] = PAGETITLE
    tdict["LINKSTR"] = LINKSTR
    tdict["PAGEPICNUMS"] = PAGEPICNUMS
    tdict["PICINFO"] = imgsinfo
#     tdict["THUMBS"] = thumbsinfo
#     tdict["THUMBNAMES"] = THUMBNAMES
#     tdict["THUMBPATHS"] = THUMBPATHS
#     tdict["THUMBDIMS"] = THUMBDIMS
    tdict["ZIPFILE"] = ZIPFILE
    tdict["ARGS"] = args
    indexstr = Template(tmplstr, searchList=[tdict])
    #print indexstr
    f.write(str(indexstr))
    f.close()

## It's over. Nothing to see here.
logging.debug("All done!")
