import gc
import random
import sys
from math import floor, ceil


if not hasattr(sys, "_called_from_test"):
    from .settings import KEYS
    from .rotated_list import forward, backward
    import display
else:
    from settings import KEYS
    from rotated_list import forward, backward

# ttb, rtl, btt  -> left to right, top to bottom, right to left. perhaps later diagonal.
DIRECTION_LTR = 0
DIRECTION_RTL = 1
DIRECTION_TTB = 2
DIRECTION_BTT = 3

RED = 0xFF0000
GREEN = 0x00FF00
BLUE = 0x0000FF
WHITE = 0xFFFFFF
BLACK = 0x000000

width = height = [0, 1, 2, 3]

state = {
    "frame": 0,
    "current_animation": None,
    "sweep": {"position": 0, "direction": DIRECTION_LTR, "limit_frames": 2},
    "flash": {"limit_frames": 8},
    "debug": {"limit_frames": 100, "limit_subframes": 10},
    "angle": {"direction": 0},
    "shapes": {"limit_frames": 5}
}

# can only write the the first level of a dictionary.
# sounds crazy, but it seems to be the case.
smooth_color_transition_directions = {"r": 1, "g": 1, "b": 1}
sweep_current_color = {'color': WHITE}

opposites = {3: 0, 2: 1, 1: 2, 0: 3}

happy_colors = [0xFF6E6E, 0xFFA36E, 0xFFC06E, 0x59FFAC, 0x59A9FF, 0x8E59FF, 0xF530FF]

# This is the first color of the display, and it will be continuously adjusted.
smooth_transition_current_color = {'color': 0xFF6E6E}
state["current_animation"] = default_animation = "smooth_color_transition"


def darken_color_part(hex_color, color_name):
    bits = 16 if color_name == "r" else 8 if color_name == "g" else 0
    current_color = hex_color >> bits & 0b11111111
    new_color = ceil(current_color * 0.9)
    # don't go completely dark, so the hex value still has something to <<
    return 17 if new_color < 1 else new_color


def lighten_color_part(hex_color, color_name):
    bits = 16 if color_name == "r" else 8 if color_name == "g" else 0
    current_color = hex_color >> bits & 0b11111111
    new_color = ceil(current_color * 1.1)
    # don't go completely dark, so the hex value still has something to <<
    return 255 if new_color > 255 else new_color


def make_new_color_part(hex_color, color_name: str):
    global smooth_color_transition_directions

    bits = 16 if color_name == "r" else 8 if color_name == "g" else 0
    # https://stackoverflow.com/questions/45220959/python-how-do-i-extract-specific-bits-from-a-byte
    current_color = hex_color >> bits & 0b11111111

    # 255 == max
    if current_color > 249:
        smooth_color_transition_directions[color_name] = 0

    # don't make it too dark
    if current_color < 100:
        smooth_color_transition_directions[color_name] = 1

    return (
        current_color + random.randint(0, 2)
        if smooth_color_transition_directions[color_name]
        else current_color - random.randint(0, 2)
    )


def smooth_transition_color(hex_color):
    """
    # slowly adjust the color based on random values. This will slowly change the color of the display.
    # the base color can be changed by hitting a button. How can we do some bit magic that changes one
    # of the colors slightly? This can be done per pixel even, which is nicer. But let's start with one color and
    # fading.

    # Colors are always represented in 24-bit from within Python, in the 0xRRGGBB format.
    # This matches HTML/CSS colors which are #RRGGBB as well. 6 bits.

    :param hex_color:
    :return:
    """

    return (
        make_new_color_part(hex_color, "r") << 16
        | make_new_color_part(hex_color, "g") << 8
        | make_new_color_part(hex_color, "b")
    )


def darken_color(hex_color):
    darker = (
        darken_color_part(hex_color, "r") << 16
        | darken_color_part(hex_color, "g") << 8
        | darken_color_part(hex_color, "b")
    )
    return darker


def lighten_color(hex_color):
    lighter = (
        lighten_color_part(hex_color, "r") << 16
        | lighten_color_part(hex_color, "g") << 8
        | lighten_color_part(hex_color, "b")
    )
    return lighter


def get_random_color():
    global happy_colors
    return happy_colors[random.randint(0, len(happy_colors) - 1)]


def full_sceen_feedback():
    global state, smooth_transition_current_color

    new_color = get_random_color()
    # x, y, width, height, filled, color
    display.drawFill(new_color)

    # as an extra, alter the smooth color transition, so during this animation the animation
    # continues from this random color, which looks more cool
    smooth_transition_current_color['color'] = new_color


def bootscreen():
    display.drawFill(WHITE)
    display.flush()


def animation_wifi_searching():
    # waiting for wifi is a thing. The 'wifi' samples are probably not available. So visualize without deps.

    # draw wifi signal thingy, the top left corner is a status color, that changes color every connection attempt.
    display.drawFill(WHITE)

    display.drawPixel(0, 2, 0x333333)
    display.drawPixel(1, 1, 0x333333)
    display.drawPixel(2, 1, 0x333333)
    display.drawPixel(3, 2, 0x333333)

    display.flush()


def animation_wifi_searching_progress():
    # used as a progress bar indicator, also helps with feedback when crashing during (WIFI) setup.
    display.drawPixel(0, 0, get_random_color())
    display.flush()


def draw_buttons():
    # previous and next buttons for music and animations,

    lighten_button(KEYS["previous_track"])
    darken_button(KEYS["previous_animation"])
    darken_button(KEYS["next_animation"])
    lighten_button(KEYS["next_track"])


def key_to_xy(key_number):
    return key_number % 4, floor(key_number / 4)


def lighten_button(key_number):
    x, y = key_to_xy(key_number)
    display.drawPixel(x, y, lighten_color(swap_rb(display.getPixel(x, y))))


def darken_button(key_number):
    x, y = key_to_xy(key_number)
    display.drawPixel(x, y, darken_color(swap_rb(display.getPixel(x, y))))


def sweep_frame():
    global state, opposites, sweep_current_color
    """
    Draws a line in a random direction: left to right, up to down etc.
    Together with fade_out this will create an interesting loading effect.

    todo: i can just reorient the frame buffer... which is much easier. But this will not allow 
    two animations at the same time in different directions. So no. Or then again re-pan?

    :return: 
    """
    angles = {
        DIRECTION_LTR: 0,
        DIRECTION_RTL: 180,
        DIRECTION_TTB: 90,
        DIRECTION_BTT: 270,
    }

    display.orientation(angles[state["sweep"]["direction"]])
    display.drawLine(state["sweep"]["position"], 0, state["sweep"]["position"], 3, sweep_current_color['color'])
    display.orientation(-angles[state["sweep"]["direction"]])

    # prepare for the next sweep animation step
    # It turns out, that for whatever reason, you can't write to global integer directly, but you can to a dict.
    if state["sweep"]["position"] > 2:
        state["sweep"]["position"] = 0
        # next sweep direction
        state["sweep"]["direction"] = random.choice(
            [DIRECTION_LTR, DIRECTION_TTB, DIRECTION_RTL, DIRECTION_BTT]
        )
        # next sweep color:
        sweep_current_color['color'] = get_random_color()
    else:
        state["sweep"]["position"] += 1


def get_frame():
    frame = []

    for x in width:
        for y in height:
            frame += [display.getPixel(x, y)]

    return frame


matrix_drops = []
matrix_blinks = []


def animate_matrix():
    # Creates a path and walks through it.
    global state, matrix_drops, matrix_blinks

    # animate drops
    new_drops = []
    for drop in matrix_drops:
        x, y = drop
        # disappearing drop
        if y == 4:
            continue
        if y < 3:
            display.drawPixel(x, y + 1, GREEN)
            new_drops.append((x, y + 1))
    matrix_drops = new_drops

    # add new drops:
    if random.randint(0, 10) < 5:
        matrix_drops.append([random.randint(0, 3), random.randint(-1, 2)])

    # animate blinks
    new_blinks = []
    for blink in matrix_blinks:
        x, y = blink
        if random.randint(0, 100) > 90:
            # blink disappears
            continue
        display.drawPixel(x, y, GREEN)
        if random.randint(0, 100) > 90:
            # blink disappears
            display.drawPixel(x, y, darken_color(GREEN))
        if random.randint(0, 100) > 90:
            # blink disappears
            display.drawPixel(x, y, lighten_color(GREEN))
        new_blinks.append((x, y))
    matrix_blinks = new_blinks

    if random.randint(0, 10) < 4:
        matrix_blinks.append([random.randint(0, 3), random.randint(0, 3)])

    fade_out_entire_display()


def animate_shapes():
    shapes = [
        # dash
        [1, 0, 0, 1,
         0, 1, 0, 0,
         0, 0, 1, 0,
         1, 0, 0, 1],
        # circle
        [0, 1, 1, 0,
         1, 0, 0, 1,
         1, 0, 0, 1,
         0, 1, 1, 0],
        # 4x line
        [0, 1, 0, 1,
         1, 1, 1, 1,
         0, 1, 0, 1,
         1, 1, 1, 1],
        # sideways line
        [0, 1, 0, 0,
         1, 0, 1, 0,
         0, 1, 0, 1,
         0, 0, 1, 0],
        # checkerboard
        [1, 0, 1, 0,
         0, 1, 0, 1,
         1, 0, 1, 0,
         0, 1, 0, 1],
        # arrow
        [0, 0, 0, 0,
         0, 1, 1, 0,
         1, 0, 0, 1,
         0, 0, 0, 0],
        # big line
        [0, 0, 0, 0,
         1, 1, 1, 1,
         1, 1, 1, 1,
         0, 0, 0, 0],
        # small circle
        [0, 1, 1, 1,
         0, 1, 0, 1,
         0, 1, 1, 1,
         0, 0, 0, 0],
        # large circle
        [1, 1, 1, 1,
         1, 0, 0, 1,
         1, 0, 0, 1,
         1, 1, 1, 1],
        # side bar
        [1, 1, 1, 1,
         1, 1, 1, 1,
         0, 0, 0, 0,
         0, 0, 0, 0],
        # blinds
        [1, 1, 1, 1,
         0, 0, 0, 0,
         1, 1, 1, 1,
         0, 0, 0, 0],
    ]

    # randomize direction, color and inversion
    orientation = random.choice([0, 90, 180, 270])
    shape = random.choice(shapes)
    inverse = random.choice([0, 1])
    color = get_random_color()

    if inverse:
        shape = [0x000000 if thing else color for thing in shape]
    else:
        shape = [color if thing else 0x000000 for thing in shape]

    if state["frame"] % state["shapes"]["limit_frames"] == 0:
        display.orientation(orientation)
        for i in range(0, 16):
            display.drawPixel(i // 4, i % 4, shape[i])
        display.orientation(-orientation)
    else:
        fade_out_entire_display()


def fade_out_entire_display():
    """
    Fades the entire screen with one step. This is used to create ghosting effects.

    Reads all pixels, and writes them with a darker variant. Probably also possible with a single mask
    operation... but oh welll..

    :return:
    """

    # makes it looks like the fading comes from the center, which is smoother
    for x in [1, 2, 3, 0]:
        for y in [1, 2, 3, 0]:
            display.drawPixel(x, y, swap_rb(darken_color(display.getPixel(x, y))))


def animate_sweep():
    if state["frame"] % state["sweep"]["limit_frames"] == 0:
        sweep_frame()
        fade_out_entire_display()


def animate_smooth_color_transition():  # noqa not using time because it's not needed. Provided for when you do.
    global smooth_transition_current_color
    smooth_transition_current_color['color'] = smooth_transition_color(
        smooth_transition_current_color['color']
    )
    display.drawFill(smooth_transition_current_color['color'])


def animate_sparkle():
    # a random pixel somewhere, which fades out.
    display.drawPixel(
        random.choice([0, 1, 2, 3]), random.choice([0, 1, 2, 3]), get_random_color()
    )
    fade_out_entire_display()


def animate_amateurstrobe():
    frames = {
        0: BLACK,
        1: WHITE,
    }

    display.drawFill(frames[state["frame"] % len(frames)])


def animate_professionalstrobe():
    frames = {
        0: RED,
        1: GREEN,
        2: BLUE,
    }

    display.drawFill(frames[state["frame"] % len(frames)])


def animate_chadstrobe():
    # only for chads

    frames = {
        0: WHITE,
        1: BLACK,
        2: RED,
        3: BLACK,
        4: WHITE,
        5: BLACK,
        6: GREEN,
        7: BLACK,
        8: WHITE,
        9: BLACK,
        10: BLUE,
        11: BLACK
    }

    display.drawFill(frames[state["frame"] % len(frames)])


def swap_rb(hex_color):
    """
    Swap R and B color, as an attempt to see if it's possible to work with the badget display code instead of
    my own python library (which is ofc much slower). Of course this is also a slow routine...

    :param hex_color:
    :return:
    """
    r = hex_color >> 16 & 0b11111111
    g = hex_color >> 8 & 0b11111111
    b = hex_color >> 0 & 0b11111111

    return b << 16 | g << 8 | r


def animate_debug():
    # fill the entire screen, which fades out, every 5 frames
    # the getPixel retrieves a sampled down value, and then writes that again, this causes colors to get lost.
    global state

    if state["frame"] % state["debug"]["limit_frames"] == 0:
        new_color = get_random_color()
        display.drawFill(new_color)
        print("New color: " + bin(new_color))

    if state["frame"] % state["debug"]["limit_subframes"] == 0:
        # Because getpixel does not return the value that has been drawn
        # the screen will do crazy stuff when writing the current value to it
        read = display.getPixel(0, 0)
        read = swap_rb(read)
        display.drawFill(read)
        print("The display is now: " + bin(read))


animations = {
    "smooth_color_transition": animate_smooth_color_transition,
    "swipe": animate_sweep,
    "sparkle": animate_sparkle,
    "matrix": animate_matrix,
    "shapes": animate_shapes,
    "debug": animate_debug,
    "amateur_strobe": animate_amateurstrobe,
    "professional_strobe": animate_professionalstrobe,
    "chad_strobe": animate_chadstrobe,
}

available_animations = ['smooth_color_transition', "shapes", 'swipe', 'sparkle','matrix',
                        'amateur_strobe', 'professional_strobe', 'chad_strobe']  # dict.keys() didn't work...


def get_previous_animation():
    global available_animations, state
    return backward(available_animations, state["current_animation"])


def get_next_animation():
    global available_animations, state
    return forward(available_animations, state["current_animation"])


def set_animation(animation_name):
    global state
    print("Setting animation to: " + animation_name)
    state["current_animation"] = animation_name


def progress_animation(
    mytimer,
):  # noqa not using time because it's not needed. Provided for when you do.
    """
    This is called with a timer, each call is a frame.

    :param mytimer:
    :return:
    """
    global animations, state

    # make sure it doesn't 'overflow'
    state["frame"] += 1

    if state["frame"] > 10000:
        state["frame"] = 0

    animation_frame = animations[state["current_animation"]]
    animation_frame()

    # possibly that the memory gets filled with above animation calling. make sure to clear some memory
    # otherwise the player will crash after a minute or so... This will impact the framerate a bit, but that's
    # better than a hard crash :)
    del animation_frame
    if state["frame"] % 100 == 0:
        show_memory_usage()
    gc.collect()

    draw_buttons()
    display.flush()


# the badge hangs when animations are used, probably because of memory issues
# it's probably leaking a lot
def show_memory_usage():
    # https://forum.micropython.org/viewtopic.php?t=3499
    import gc
    import os

    def df():
        s = os.statvfs('//')
        return '{0} MB'.format((s[0] * s[3]) / 1048576)

    def free():
        F = gc.mem_free()
        A = gc.mem_alloc()
        T = F + A
        P = '{0:.2f}%'.format(F / T * 100)
        return 'Total:{0} Free:{1} ({2})'.format(T, F, P)

    print(df())
    print(free())