"""BitFieldDraw
By Al Sweigart al@inventwithpython.com

A simple module for creating bit field art for strings or screens."""

#from __future__ import print_function  # TODO - chr() in this file doesn't work in python 2 currently.

__version__ = '0.1.2'


import shutil

TOP_BLOCK = chr(9600)
BOTTOM_BLOCK = chr(9604)
FULL_BLOCK = chr(9608)
EMPTY = ' '


def main():
    """
    Run through a quick demo of several bit fields generated by different functions.
    """
    functions = [
        lambda x, y: (x ^ y) % 5,
        lambda x, y: (x ^ y) % 9,
        lambda x, y: (x ^ y) % 17,
        lambda x, y: (x ^ y) % 33,
        lambda x, y: (x ^ y) % 2,
        lambda x, y: (x ^ y) % 4,
        lambda x, y: (x ^ y) % 8,
        lambda x, y: (x | y) % 7,
        lambda x, y: (x | y) % 17,
        lambda x, y: (x | y) % 29,
        lambda x, y: (x * y) & 64,
        lambda x, y: (x * y) & 24,
        lambda x, y: (x * y) & 47,
        lambda x, y: (x ^ y) < 77,
        lambda x, y: (x ^ y) < 214,
        lambda x, y: (x ^ y) < 120,
        lambda x, y: (x * 2) % y,
        lambda x, y: (x * 64) % y,
        lambda x, y: (x * 31) % y,
        lambda x, y: ((x-128) * 64) % (y-128),
        lambda x, y: (x ^ y) & 32,
        lambda x, y: (x ^ y) & 72,
        lambda x, y: (x ^ y) & 23,
        lambda x, y: ((x * y) ** 4) % 7,
        lambda x, y: ((x * y) ** 5) % 99,
        lambda x, y: ((x * y) ** 9) % 3,
        lambda x, y: (x % y) % 4,
        lambda x, y: 40 % (x % y),
        lambda x, y: x & y,
        lambda x, y: x % y,
        lambda x, y: x & 9,
        lambda x, y: (x & y)   &   (x ^ y) % 19,
        lambda x, y: ((x ^ y) & 32)   *   ((x ^ y) % 9),
        lambda x, y: (x * 64) % y   *   ((x ^ y) < 77),
    ]

    for fn in functions:
        print(getBitFieldStr(fn, invertBit=True))
        input('Press Enter to continue...')


def _getDimensions(width=None, height=None):
    """
    Returns what the width and height should be set to depending on the current terminal's size, if no width or height
    is already set.

    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    WIDTH, HEIGHT = shutil.get_terminal_size()
    # We can't print to the last column on Windows without it adding a
    # newline automatically, so reduce the width by one:
    WIDTH -= 1
    HEIGHT *= 2  # We'll use squares for creating the height.

    if width is None:
        width = WIDTH
    if height is None:
        height = HEIGHT

    return width, height

def getBitField(func, invertBit=False, left=0, bottom=0, width=None, height=None):
    """
    Returns a frozenset of (x, y) tuples describing the set bits of the bit field produced by func.

    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    width, height = _getDimensions(width, height)

    canvas = []
    for x in range(left, left + width):
        for y in range(bottom, bottom + height):
            try:
                bit = func(x, y)
            except:
                bit = False
            if invertBit:
                bit = not bit
            if bit:
                canvas.append((x, y))
    return frozenset(canvas)


def getBitFieldStrFromSet(canvas):
    raise NotImplementedError


def getBitFieldImg(func, invertBit=False, left=0, bottom=0, width=None, height=None, trueColor='white', falseColor='black'):
    """
    Returns a Pillow Image object of the bit field produced by func.

    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    # TODO - be able to specify None for trueColor and falseColor to have transparency.
    try:
        from PIL import Image, ImageDraw, ImageColor
    except:
        raise Exception('The getBitFieldImg() function requires the Pillow module to be installed first.')

    width, height = _getDimensions(width, height)
    trueColor = ImageColor.getcolor(trueColor, 'RGB')
    falseColor = ImageColor.getcolor(falseColor, 'RGB')

    im = Image.new('RGB', (width, height), falseColor)
    draw = ImageDraw.Draw(im)
    pixels = []
    for xy in getBitField(func, invertBit, left, bottom, width, height):
        pixels.append((xy[0] - left, height - (xy[1] - bottom) - 1))
    draw.point(list(pixels), fill=trueColor)

    return im


def saveBitFieldImg(filename, func, invertBit=False, left=0, bottom=0, width=None, height=None, trueColor='white', falseColor='black'):
    """
    Saves an image of the bit field produced by func.

    :param filename: The filename of the image. Can be either png, jpg, gif, or bmp.
    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    getBitFieldImg(func, invertBit, left, bottom, width, height, trueColor, falseColor).save(filename)


def getBitFieldStr(func, invertBit=False, left=0, bottom=0, width=None, height=None):
    """
    Returns a string describing the set bits of the bit field produced by func.

    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    width, height = _getDimensions(width, height)

    rows = []
    for y in range(bottom + height - 1, bottom - 1, -2):
        row = []
        for x in range(left, left + width):
            try:
                topBit = func(x, y)  # top-half of text cell
            except:
                topBit = False

            if y - 1 <= bottom - 1:
                bottomBit = False  # This bottom row should always be empty.
            else:
                try:
                    bottomBit = func(x, y - 1)  # bottom-half of text cell
                except:
                    bottomBit = False

            if invertBit:
                if topBit and bottomBit:
                    row.append(EMPTY)
                elif topBit and not bottomBit:
                    row.append(BOTTOM_BLOCK)
                elif not topBit and bottomBit:
                    row.append(TOP_BLOCK)
                else:
                    row.append(FULL_BLOCK)
            else:
                if topBit and bottomBit:
                    row.append(FULL_BLOCK)
                elif topBit and not bottomBit:
                    row.append(TOP_BLOCK)
                elif not topBit and bottomBit:
                    row.append(BOTTOM_BLOCK)
                else:
                    row.append(EMPTY)
        rows.append(''.join(row))
    return '\n'.join(rows)


def printBitFieldStr(func, invertBit=False, left=0, bottom=0, width=None, height=None):
    """
    Display the bit field produced by func on the screen.

    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """
    print(getBitFieldStr(func, invertBit, left, bottom, width, height))


def saveBitFieldStr(filename, func, invertBit=False, left=0, bottom=0, width=None, height=None):
    """
    Save the text characters of the bit field produced by func to a file.

    :param filename: The filename to save the text to.
    :param func: A function that accepts integer x and y coordinates, and returns True if this bit in the field should be visible and False if not.
    :param invertBit: Whether to toggle the return Boolean of func. Defaults to False.
    :param left: The x coordinate of the left edge of the bit field region. Defaults to 0.
    :param bottom: The y coordinate of the bottom edge of the bit field region. Defaults to 0.
    :param width: The desired width. If this is None, the returned width is set to the terminal's width in text cells.
    :param height: The desired height. If this is None, the returned height is set to twice the terminal's height in
    text cells because text cells are twice as tall as they are wide.
    """

    bitFieldStr = getBitFieldStr(func, invertBit, left, bottom, width, height)
    bitFieldFileObj = open(filename, 'w', encoding='utf-8')
    bitFieldFileObj.write(bitFieldStr)
    bitFieldFileObj.close()



if __name__ == '__main__':
    main()
