#!/usr/bin/env python
"""Generate PNG images for each second of a countdown clock beginning just
under a given number of minutes and counting down to 0:00.  Files will be
created in the current working directory with names that derive from the
time values they contain and contain a sort-friendly initial numeric
index ("002-countdown-4_57.png", "290-countdown-0_09.png", etc.).

Usage:  __FILE__ [OPTIONS] NUM_MINUTES
  
Options:
   -h, --help        Display this help message
   --width=          Width (in pixels) of the generated images [1280]
   --height=         Height (in pixels) of the generated images [720]
   --font-file=      TrueType font file to use for countdown text [arial.ttf]
   --font-size=      Font size (in pixels) of countdown text [width / 10]
   --font-color=     Text color (as an RGB or RGBA hex value) [FFFFFFFF]
   --position=       Location of text (with proportional padding) [c]:
                        tl (top-left), t (top-centered), tr (top-right),
                        l (middle-left), c (middle-centered), r (middle-right),
                        bl (bottom-left), b (bottom-centered), br (bottom-right)
   --no-zeroes       Omit leading zeroes in minutes remaining
   --shadow-color=   Drop shadow color (as an RGB or RGBA hex value, if any)
   -v, --verbose     Be noisy about what we're doing

Example(s):
   # 5-minute countdown 640x480 with centered Arial 24-point font
   gen-countdown-images.py --width 640 --height 480 --font-size 24 5

   # 1-minute countdown 1280x720 with Century Gothic 120-point font
   # positioned in the bottom-right corner
   gen-countdown-images.py --font-file GOTHIC.TTF --position br 1  
"""
import sys
import getopt
import os
import re
from PIL import Image, ImageFont, ImageDraw


def usage_and_exit(msg=None):
    exit_code = 0
    stream = sys.stdout
    if msg:
        stream = sys.stderr
        stream.write("ERROR: %s\n\n" % msg)
        exit_code = 1
    stream.write(__doc__.replace("__FILE__", os.path.basename(sys.argv[0])))
    raise SystemExit(exit_code)


def gen_timestamp_image(
    width,
    height,
    font,
    font_color,
    shadow_color,
    shadow_offset,
    timestamp,
    position,
    file_name,
):
    # New image
    img = Image.new("RGBA", (width, height), (0, 0, 0, 0))

    # New drawing canvas
    draw = ImageDraw.Draw(img)

    # Calculate the size required by the timestamp text.  We're going
    # to use the actual width of the timestamp text, but for the height
    # we'll normalize on the height needed to represent all the characters
    # we might use.
    bbox = draw.textbbox((0, 0), timestamp, font)
    text_width = bbox[2] - bbox[0]
    bbox = draw.textbbox((0, 0), "0123456789:", font)
    text_height = bbox[3] - bbox[1]

    # Calculate the position of the text.  For a border on
    # edge-snapped positions, we'll use 5% of the smaller dimension.
    #
    # FIXME: Probably should do some bounds-checking here, handling
    #        rendered text that's too big to fit the image.
    border = min(width, height) * 0.05
    if position in ["tl", "l", "bl"]:
        # horizonally left-snapped
        text_x = border
    elif position in ["tr", "r", "br"]:
        # horizonally right-snapped
        text_x = width - text_width - border
    else:
        # horizontally centered
        text_x = (width - text_width) / 2
    if position in ["tl", "t", "tr"]:
        # vertically top-snapped
        text_y = border
    elif position in ["bl", "b", "br"]:
        # vertically bottom-snapped
        text_y = height - text_height - border
    else:
        # vertically centered
        text_y = (height - text_height) / 2

    # At least have the text begin in the frame...
    text_x = max(0, text_x)
    text_y = max(0, text_y)

    # Draw the shadow (if any).
    if shadow_color is not None:
        draw.text(
            (text_x + shadow_offset, text_y + shadow_offset),
            timestamp,
            shadow_color,
            font,
        )

    # Draw the text.
    draw.text((text_x, text_y), timestamp, font_color, font)

    # Save the file.
    img.save(file_name)


def color_hex_to_tuple(color):
    regexp = r"^#?(([0-9a-fA-F]{2}){3,4})$"
    match = re.match(regexp, color)
    hex = match.group(1)
    r = int(hex[0:2], 16)
    g = int(hex[2:4], 16)
    b = int(hex[4:6], 16)
    a = 255
    if len(hex) > 6:
        a = int(hex[6:8], 16)
    return (r, g, b, a)


def main():
    opts, args = getopt.getopt(
        sys.argv[1:],
        "hv",
        [
            "help",
            "width=",
            "height=",
            "font-file=",
            "font-size=",
            "font-color=",
            "no-zeroes",
            "position=",
            "shadow-color=",
            "verbose",
        ],
    )

    width = 1280
    height = 720
    font_file = "arial.ttf"
    font_size = None
    font_color = (255, 255, 255, 255)
    shadow_color = None
    position = "c"
    zeroes = True
    verbose = False

    # Parse options.
    for option, value in opts:
        if option in ["-h", "--help"]:
            usage_and_exit()
        elif option in ["--height"]:
            try:
                height = int(value)
            except Exception:
                usage_and_exit("Invalid value for --height")
        elif option in ["--width"]:
            try:
                width = int(value)
            except Exception:
                usage_and_exit("Invalid value for --width")
        elif option in ["--font-size"]:
            try:
                font_size = int(value)
            except Exception:
                usage_and_exit("Invalid value for --font-size")
        elif option in ["--font-file"]:
            font_file = value
        elif option in ["--font-color"]:
            try:
                font_color = color_hex_to_tuple(value)
            except Exception:
                usage_and_exit("Invalid value for --font-color")
        elif option in ["--no-zeroes"]:
            zeroes = False
        elif option in ["--position"]:
            if value not in ["tl", "t", "tr", "l", "c", "r", "bl", "b", "br"]:
                usage_and_exit("Invalid value for --position")
            position = value
        elif option in ["--shadow-color"]:
            try:
                shadow_color = color_hex_to_tuple(value)
            except Exception:
                usage_and_exit("Invalid value for --shadow-color")
        elif option in ["-v", "--verbose"]:
            verbose = True
        else:
            usage_and_exit(f"Unexpected option '{option}'")

    # Only non-optional argument is the number of minutes.
    if len(args) != 1:
        usage_and_exit("Not enough arguments")
    try:
        num_minutes = int(args[0])
        if num_minutes > 60:
            usage_and_exit("Maximum number of minutes is 60")
    except Exception:
        usage_and_exit("Invalid value for number of minutes")

    # If there's no font size, we use text that's 10% of the width.
    if font_size is None:
        font_size = int(width / 10)

    # Calculate the would-be drop shadow offset.
    shadow_offset = int(font_size * 0.03)

    # Try to load the font.
    try:
        font = ImageFont.truetype(font_file, font_size)
    except Exception:
        usage_and_exit(f"Unable to load font file '{font_file}' and size {font_size}")

    format = "%d:%02d"
    if num_minutes >= 10 and zeroes:
        format = "%02d:%02d"
    num_images = num_minutes * 60
    filename_fmt = "%%0%dd-countdown-%%s.png" % (len(str(num_images)))
    index = 0
    for minute in range(num_minutes - 1, -1, -1):
        for second in range(59, -1, -1):
            timestamp = format % (minute, second)
            filename = filename_fmt % (index, timestamp.replace(":", "_"))
            if verbose:
                sys.stdout.write(f"Create file '{filename}' ... ")
            gen_timestamp_image(
                width,
                height,
                font,
                font_color,
                shadow_color,
                shadow_offset,
                timestamp,
                position,
                filename,
            )
            if verbose:
                sys.stdout.write("done.\n")
            index += 1
    if verbose:
        sys.stdout.write("Finished!\n")


if __name__ == "__main__":
    main()
