#! /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 Cheetah3/CT3 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:
 * Crop-to-thumb (option), with fixed thumb-dimensions
 * Auto-rotate by EXIF orientations

"""

import os, re, shutil
import emin, logging, argparse
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("SRCPATHS", nargs="*", default=["."], help="files and dirs to build into a gallery")
parser.add_argument("OUTDIR", nargs="?", default="gallery", help="output gallery directory")
parser.add_argument("-r", "--recurse", dest="DIR_RECURSE", action="store_true", default=False,
                  help="recurse to find all image formats in the given directory tree(s) (default: %(default)s)")
parser.add_argument("-t", "--title", dest="TITLE", default=None,
                  help="title of this gallery")
parser.add_argument("-T", "--template", dest="TEMPLATE", default=None,
                  help="specify the template file to be used for the index pages")
parser.add_argument("-S", "--style", dest="STYLE", default=None,
                  help="specify the CSS style file to be used for the index pages")
parser.add_argument("-z", "--zipfile", nargs="?", dest="ZIPFILE", const=None, default=False,
                  help="name of zip archive file. Default is based on the title.")
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=4, type=int,
                  help="max number of thumbnail columns in table mode (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 in table mode (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("--cdn", dest="USE_CDN", action="store_true", default=False,
                  help="disable use of standard CDNs for Lightbox JS, etc.")
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")


## 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

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


## Go to the gallery-output 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
RE_IMG = re.compile(r"^.*\.(jpg|jpeg|png|gif|tif|tiff|eps|pdf)$", re.I)
RE_EXC = re.compile(args.EXCLUDE) if args.EXCLUDE else None
## Get all files
filepaths = []
for srcpath in args.SRCPATHS:
    if os.path.isfile(srcpath):
        filepaths += [srcpath]
    elif os.path.isdir(srcpath):
        if args.DIR_RECURSE:
            for root, dirs, files in os.walk(srcpath):
                filepaths += [os.path.join(root, f) for f in files]
        else:
            filepaths += [os.path.join(srcpath, f) for f in os.listdir(srcpath) if os.path.isfile(os.path.join(srcpath, f))]

## Filter the list by extensions and exclusions
imgs = [fp for fp in filepaths if RE_IMG.match(fp)]
if RE_EXC is not None:
    imgs = [im for im in imgs if not RE_EXC.search(im)]

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

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


## Process images
logging.debug(f"Processing {len(outimgs):d} image{'s' if len(outimgs) != 1 else ''}...")
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()
    if pictype == "TIF":
        pictype = "TIFF"
    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 = emin.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 = emin.ImageInfo()
    info.name = picname
    info.path = picpath
    info.versions = picversions

    ## Thumb info
    # TODO: make thumb format configurable
    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 = emin.PILI.open(picpath, "r")
        thumbimg.thumbnail((100000000, args.THUMB_HEIGHT), resample=emin.PILI.LANCZOS)
        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:
    import math
    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)


## Page template (from pkg if no slashes, otherwise user path)
if args.TEMPLATE is None:
    args.TEMPLATE = "default.html.templ"
logging.debug(f"Using index template file = {args.TEMPLATE}")
if "/" not in args.TEMPLATE:
    tmplstr = emin.loadresource(args.TEMPLATE)
else:
    with open(args.TEMPLATE, "r") as tf:
        tmplstr = tf.read()

## Take page extension from template name
args.EXTN = os.path.splitext(args.TEMPLATE.replace(".templ", ""))[1].lstrip(".") or "html"

## Copy style file into the output dir
if args.STYLE is None:
    args.STYLE = "emin.css"
logging.debug(f"Using style file = {args.STYLE}")
if "/" not in args.STYLE:
    emin.copyresource(args.STYLE, args.OUTDIR)
else:
    shutil.copy(args.STYLE, args.OUTDIR)

## Copy lightbox files into the output dir if not using the CDN
if not args.USE_CDN:
    emin.copyresource("lightbox3.css", args.OUTDIR)
    emin.copyresource("lightbox3.min.js", args.OUTDIR)


def getPageFilename(pagenum):
    return f"index.{args.EXTN}" if pagenum == 1 else f"index{pagenum:02d}.{args.EXTN}"

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


## Choose an automatic zip name if enabled
if args.ZIPFILE is None:
    if args.TITLE:
        zipfilename = emin.safename(args.TITLE.lower())
    else:
        zipfilename = "gallery.zip"
else:
    zipfilename = args.ZIPFILE

## Make a zip archive if enabled (not false)
if zipfilename:
    logging.debug("Making zipped picture archive")
    if os.path.splitext(zipfilename)[1] != ".zip":
        zipfilename += ".zip"
    ## Do the zipping
    if zipfilename:
        from zipfile import ZipFile
        zf = ZipFile(os.path.join(args.OUTDIR, zipfilename), "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")
ZIPFILE = zipfilename


## 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()


## 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

    from datetime import datetime
    TIMESTAMP = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

    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"] = PICINFO
    tdict["ZIPFILE"] = ZIPFILE
    tdict["TIMESTAMP"] = TIMESTAMP
    tdict["EVERSION"] = emin.__version__
    tdict["ARGS"] = args
    indexstr = emin.Template(tmplstr, searchList=[tdict])
    f.write(str(indexstr))
    f.close()

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