#!/usr/bin/env python3
#
# Copyright (c) 2018 Henrik Brix Andersen <henrik@brixandersen.dk>
#
# SPDX-License-Identifier: Apache-2.0

import argparse
import sys

from PIL import ImageFont
from PIL import Image
from PIL import ImageDraw

PRINTABLE_MIN = 32
PRINTABLE_MAX = 126

def generate_element(image, charcode):
    """Generate CFB font element for a given character code from an image"""
    blackwhite = image.convert("1", dither=Image.NONE)
    pixels = blackwhite.load()

    width, height = image.size
    if args.dump:
        blackwhite.save("{}_{}.png".format(args.name, charcode))

    if PRINTABLE_MIN <= charcode <= PRINTABLE_MAX:
        char = " ({:c})".format(charcode)
    else:
        char = ""

    args.output.write("""\t/* {:d}{} */\n\t{{\n""".format(charcode, char))

    glyph = []
    if args.hpack:
        for row in range(0, height):
            packed = []
            for octet in range(0, int(width / 8)):
                value = ""
                for bit in range(0, 8):
                    col = octet * 8 + bit
                    if pixels[col, row]:
                        value = value + "0"
                    else:
                        value = value + "1"
                packed.append(value)
            glyph.append(packed)
    else:
        for col in range(0, width):
            packed = []
            for octet in range(0, int(height / 8)):
                value = ""
                for bit in range(0, 8):
                    row = octet * 8 + bit
                    if pixels[col, row]:
                        value = value + "0"
                    else:
                        value = value + "1"
                packed.append(value)
            glyph.append(packed)
    for packed in glyph:
        args.output.write("\t\t")
        bits = []
        for value in packed:
            bits.append(value)
            if not args.msb_first:
                value = value[::-1]
            args.output.write("0x{:02x},".format(int(value, 2)))
        args.output.write("   /* {} */\n".format(''.join(bits).replace('0', ' ').replace('1', '#')))
    args.output.write("\t},\n")

def extract_font_glyphs():
    """Extract font glyphs from a TrueType/OpenType font file"""
    font = ImageFont.truetype(args.input, args.size)

    # Figure out the bounding box for the desired glyphs
    fw_max = 0
    fh_max = 0
    for i in range(args.first, args.last + 1):
        fw, fh = font.getsize(chr(i))
        if fw > fw_max:
            fw_max = fw
        if fh > fh_max:
            fh_max = fh

    # Round the packed length up to pack into bytes.
    if args.hpack:
        width = 8 * int((fw_max + 7) / 8)
        height = fh_max + args.y_offset
    else:
        width = fw_max
        height = 8 * int((fh_max + args.y_offset + 7) / 8)

    # Diagnose inconsistencies with arguments
    if width != args.width:
        raise Exception('text width {} mismatch with -x {}'.format(width, args.width))
    if height != args.height:
        raise Exception('text height {} mismatch with -y {}'.format(height, args.height))

    for i in range(args.first, args.last + 1):
        image = Image.new('1', (width, height), 'white')
        draw = ImageDraw.Draw(image)

        fw, fh = draw.textsize(chr(i), font=font)

        xpos = 0
        if args.center_x:
            xpos = (width - fw) / 2 + 1
        ypos = args.y_offset

        draw.text((xpos, ypos), chr(i), font=font)
        generate_element(image, i)

def extract_image_glyphs():
    """Extract font glyphs from an image file"""
    image = Image.open(args.input)

    x_offset = 0
    for i in range(args.first, args.last + 1):
        glyph = image.crop((x_offset, 0, x_offset + args.width, args.height))
        generate_element(glyph, i)
        x_offset += args.width

def generate_header():
    """Generate CFB font header file"""

    caps = []
    if args.hpack:
        caps.append('MONO_HPACKED')
    else:
        caps.append('MONO_VPACKED')
    if args.msb_first:
        caps.append('MSB_FIRST')
    caps = ' | '.join(['CFB_FONT_' + f for f in caps])

    clean_cmd = []
    for arg in sys.argv:
        if arg.startswith("--bindir"):
            # Drop. Assumes --bindir= was passed with '=' sign.
            continue
        if args.bindir and arg.startswith(args.bindir):
            # +1 to also strip '/' or '\' separator
            striplen = min(len(args.bindir)+1, len(arg))
            clean_cmd.append(arg[striplen:])
            continue

        if args.zephyr_base is not None:
            clean_cmd.append(arg.replace(args.zephyr_base, '"${ZEPHYR_BASE}"'))
        else:
            clean_cmd.append(arg)


    args.output.write("""/*
 * This file was automatically generated using the following command:
 * {cmd}
 *
 */

#include <zephyr/kernel.h>
#include <zephyr/display/cfb.h>

static const uint8_t cfb_font_{name:s}_{width:d}{height:d}[{elem:d}][{b:.0f}] = {{\n"""
                      .format(cmd=" ".join(clean_cmd),
                              name=args.name,
                              width=args.width,
                              height=args.height,
                              elem=args.last - args.first + 1,
                              b=args.width / 8 * args.height))

    if args.type == "font":
        extract_font_glyphs()
    elif args.type == "image":
        extract_image_glyphs()
    elif args.input.name.lower().endswith((".otf", ".otc", ".ttf", ".ttc")):
        extract_font_glyphs()
    else:
        extract_image_glyphs()

    args.output.write("""
}};

FONT_ENTRY_DEFINE({name}_{width}{height},
		  {width},
		  {height},
		  {caps},
		  cfb_font_{name}_{width}{height},
		  {first},
		  {last}
);
""" .format(name=args.name, width=args.width, height=args.height,
            caps=caps, first=args.first, last=args.last))

def parse_args():
    """Parse arguments"""
    global args
    parser = argparse.ArgumentParser(
        description="Character Frame Buffer (CFB) font header file generator",
        formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False)

    parser.add_argument(
        "-z", "--zephyr-base",
        help="Zephyr base directory")

    parser.add_argument(
        "-d", "--dump", action="store_true",
        help="dump generated CFB font elements as images for preview")

    group = parser.add_argument_group("input arguments")
    group.add_argument(
        "-i", "--input", required=True, type=argparse.FileType('rb'), metavar="FILE",
        help="TrueType/OpenType file or image input file")
    group.add_argument(
        "-t", "--type", default="auto", choices=["auto", "font", "image"],
        help="Input file type (default: %(default)s)")

    group = parser.add_argument_group("font arguments")
    group.add_argument(
        "-s", "--size", type=int, default=10, metavar="POINTS",
        help="TrueType/OpenType font size in points (default: %(default)s)")

    group = parser.add_argument_group("output arguments")
    group.add_argument(
        "-o", "--output", type=argparse.FileType('w'), default="-", metavar="FILE",
        help="CFB font header file (default: stdout)")
    group.add_argument(
        "--bindir", type=str,
        help="CMAKE_BINARY_DIR for pure logging purposes. No trailing slash.")
    group.add_argument(
        "-x", "--width", required=True, type=int,
        help="width of the CFB font elements in pixels")
    group.add_argument(
        "-y", "--height", required=True, type=int,
        help="height of the CFB font elements in pixels")
    group.add_argument(
        "-n", "--name", default="custom",
        help="name of the CFB font entry (default: %(default)s)")
    group.add_argument(
        "--first", type=int, default=PRINTABLE_MIN, metavar="CHARCODE",
        help="character code mapped to the first CFB font element (default: %(default)s)")
    group.add_argument(
        "--last", type=int, default=PRINTABLE_MAX, metavar="CHARCODE",
        help="character code mapped to the last CFB font element (default: %(default)s)")
    group.add_argument(
        "--center-x", action='store_true',
        help="center character glyphs horizontally")
    group.add_argument(
        "--y-offset", type=int, default=0,
        help="vertical offset for character glyphs (default: %(default)s)")
    group.add_argument(
        "--hpack", dest='hpack', default=False, action='store_true',
        help="generate bytes encoding row data rather than column data (default: %(default)s)")
    group.add_argument(
        "--msb-first", action='store_true',
        help="packed content starts at high bit of each byte (default: lsb-first)")

    args = parser.parse_args()

def main():
    """Parse arguments and generate CFB font header file"""
    parse_args()
    generate_header()

if __name__ == "__main__":
    main()
