import sys
import argparse
import os.path as op
from math import ceil
from datetime import datetime

from PIL import Image, UnidentifiedImageError


verbose = False

def parse_args():
    parser = argparse.ArgumentParser(
        description='Convert an image into a byte array c++ code.')
    parser.add_argument('image_path',
        help='Path of image to convert, use `--` to read from stdin.')
    parser.add_argument('-r', '--resize', type=int,
        help='Resize image to specified width, keeping aspect ratio.')
    parser.add_argument('-s', '--show', action='store_true',
        help='Only show converted image.')
    parser.add_argument('-o', '--output',
        help='Store to specified file (default: image_name.h), use `--` to output on stdout.')
    parser.add_argument('-v', '--verbose', action='store_true', default=False,
        help='Deactivate information messages.')
    parser.add_argument('-d', '--dither', action='store_true',
        help='Activate dithering.')
    parser.add_argument('-t', '--threshold', type=int, default=128,
        help='Threshold level when converting to black and white, from 0 to 255 (default: 128).')

    return parser.parse_args()

def _print(*_args, **_kwargs):
    if verbose:
        print(*_args, **_kwargs)

def get_stdin():
    _print('Reading image from stdin...')

    import tempfile
    with tempfile.NamedTemporaryFile('w+b') as temp_file:
        temp_file.write(sys.stdin.buffer.read())
        return temp_file.name

def get_file(image_path):
    _print('Reading image from %s...' % image_path)
    try:
        return image_path
    except FileNotFoundError:
        _print('Error: file not found.')
        sys.exit(1)

def get_input(input: str):
    return get_stdin() if input == '--' else get_file(input)

def load_image(image_file):
    _print('Loading image...')
    try:
        return Image.open(image_file)
    except UnidentifiedImageError:
        _print('Error: can not read an image from the file %s.' % image_file)
        sys.exit(1)

def resize(image, new_width):
    new_heigth = ceil( image.height / image.width * new_width)
    _print('Resizing original image from %dx%d to %dx%d...'
        % (image.width, image.height, new_width, new_heigth))
    image = image.resize((new_width, new_heigth))

def dither(image):
    _print('Converting into grayscale: dithering...')
    return image.convert(mode='1')

def black_and_white(image, threshold):
    _print('Converting into grayscale: applying threshold...')
    threshold = lambda x : 255 if x > threshold else 0
    return image.convert(mode='L').point(threshold, '1')

def show(image):
    _print('Displaying image with default system image viewer...')
    image.show()
    sys.exit(0)

def get_bytes(image):
    _print('Reading image data...')
    pixel_id = 0
    byte = 0
    bytes = []

    data = image.load()
    for x in range(image.height): # iterate over pixel columns
        for y in range(image.width): # iterate over pixels in the column
            px_is_black = data[y, x] == 0
            byte += (1 << (7 - pixel_id)) if px_is_black else 0

            pixel_id += 1
            if pixel_id >= 8:
                pixel_id = 0
                bytes.append(byte)
                byte = 0

        if pixel_id > 0: # width is not divisible by 8
            pixel_id = 0
            bytes.append(byte)
            byte = 0
    return bytes

def get_image_name(image_path):
    path_to_name = lambda path: op.splitext(op.basename(path))[0].replace(' ', '_')
    return path_to_name(image_path) if image_path else 'image'

def get_cpp_code(image_name, bytes, width, height):
    _print('Converting image data to c++ code...')

    byte_to_str = lambda px_id, px: '%s0x%02x' % ('\n' if px_id % 16 == 0 else '', px)
    now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")

    return '// generated by %s on %s\n' % (sys.argv[0], now) \
        +  '#define %s_width %d\n' % (image_name , width) \
        +  '#define %s_height %d\n' % (image_name, height) \
        +  'const unsigned char %s_data [] PROGMEM = {' % image_name \
        +  ','.join([ byte_to_str(px_id, px) for px_id, px in enumerate(bytes) ]) \
        +  '\n};\n'

def get_output_path(output_path, image_name):
    return op.join(op.basename(__file__), output_path) if output_path else (image_name + '.h')

def write(cpp_code, output_path):
    if output_path:
        _print('Writing content to %s...' % output_path)
        with open(output_path, 'w') as h:
            h.write(cpp_code)
    else:
        _print('Writing content to stdout...')
        sys.stdout.write(cpp_code)
    _print('Done.')

def main():
    global verbose
    args = parse_args()

    if args.verbose:
        verbose = True

    image = load_image( get_input(args.image_path) )
    if args.resize:
        resize(image, args.resize)
    image = dither(image) if args.dither else black_and_white(image, args.threshold)

    if args.show:
        show()

    image_name = get_image_name(args.image_path)
    cpp_code = get_cpp_code(image_name, get_bytes(image), image.width, image.height)
    output = get_output_path(args.output, image_name) if args.output == '--' else ''
    write(cpp_code, output)

if __name__ == '__main__':
    main()
