from copy import deepcopy
from collections import defaultdict

from typing import List, Dict, Tuple

split_word_to_chr_list = lambda y: [w for w in y]
split_word_to_int_list = lambda y: [int(w) for w in y]
split_line_to_int_list = lambda y: [int(w) for w in y.split(" ") if w]


class colors:
    # HEADER = '\033[95m'
    BLUE = "\033[94m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    RED = "\033[91m"
    ENDC = "\033[0m"


def apply_to_all(mx, func):
    for row_num, row in enumerate(mx):
        for col_num, val in enumerate(row):
            mx[row_num][col_num] = func(val)


def rotate(m, right=True):  # -90
    """
    Takes a matrix, and rotates all of the values 90 degrees to the left
    """
    x = list(zip(*m[::-1]))
    if right:
        return x
    return [list(reversed(y)) for y in x]


def load_matrix_file(name, func=None):
    """
    Open a file and split all space separated word lists to integers as a matrix
    """
    with open(name, "r") as f:
        my_file = []
        for line in f:
            my_file.append(line.rstrip())
    if func:
        return [func(x) for x in my_file]
    return [split_word_to_int_list(x) for x in my_file]


def find_in_matrix(mx, what, one=True):
    coords = []
    for row_num, row in enumerate(mx):
        for col_num, val in enumerate(row):
            if val == what:
                coord = (row_num, col_num)
                if one is True:
                    return coord
                else:
                    coords.append(coord)
    return coords


def get_neighbors(matrix, x, y, _dict=False):
    neighbors = []
    # left
    try:
        if x - 1 >= 0:
            if _dict:
                neighbors.append({"x": x - 1, "y": y, "value": matrix[y][x - 1]})
            else:
                neighbors.append([(x - 1, y), matrix[y][x - 1]])
    except IndexError:
        pass
    # right
    try:
        if _dict:
            neighbors.append({"x": x + 1, "y": y, "value": matrix[y][x + 1]})
        else:
            neighbors.append([(x + 1, y), matrix[y][x + 1]])
    except IndexError:
        pass

    # up
    try:
        if y - 1 >= 0:
            if _dict:
                neighbors.append({"x": x, "y": y - 1, "value": matrix[y - 1][x]})
            else:
                neighbors.append([(x, y - 1), matrix[y - 1][x]])
    except IndexError:
        pass

    # down
    try:
        if _dict:
            neighbors.append({"x": x, "y": y + 1, "value": matrix[y + 1][x]})
        else:
            neighbors.append([(x, y + 1), matrix[y + 1][x]])
    except IndexError:
        pass

    return neighbors


def valid_neighbors(matrix, x, y, criteria=None):
    if criteria is None:
        raise Exception("Please pass in a lambda for criteria")
    cur = matrix[y][x]
    neighbors = get_neighbors(matrix, x, y, _dict=True)
    valid = []
    for neighbor in neighbors:
        if criteria(cur, neighbor['value']):
            valid.append(neighbor)
    return valid


def sum_matrix(mtx):
    total = 0
    for row in mtx:
        total += sum(row)
    return total


M_UL, M_U, M_UR = (-1, -1), (0, -1), (1, -1)
M_L, M_R = (-1, 0), (1, 0)
M_DL, M_D, M_DR = (-1, 1), (0, 1), (1, 1)


def get_neighbor_coords(matrix, c, r, diagonals=True):
    height = len(matrix)
    width = len(matrix[0])
    if diagonals:
        coords = (M_UL, M_U, M_UR, M_L, M_R, M_DL, M_D, M_DR)
    else:
        coords = (M_U, M_L, M_R, M_D)
    neighbors = []

    for _c, _r in coords:
        try:
            value = matrix[r + _r][c + _c]  # Try to get a value error
            if r + _r >= 0 and c + _c >= 0:
                neighbors.append(
                    [{"c": c + _c, "r": r + _r}, value]
                )  # woo, no error, this coord is valid
        except IndexError:
            pass  # okay we out of bounds boizzzz
    return neighbors


def line_of_sight_coords(matrix, row, col) -> Dict[str, List[Tuple[int, int]]]:
    """
    Takes a matrix, a row, and a column
    calculates the coordinates to the edge for all four cardinal directions

    returns a dict with a list  of tuple coordes TRAVELING AWAY from the
    requested coordinate
    """
    height, width = get_size(matrix)

    col_ids = list(range(0, height))
    row_ids = list(range(0, width))

    up_ids, down_ids = list(reversed(col_ids[:col])), col_ids[col + 1 :]
    left_ids, right_ids = list(reversed(row_ids[:row])), row_ids[row + 1 :]

    left = [(r, col) for r in left_ids]
    right = [(r, col) for r in right_ids]
    up = [(row, c) for c in up_ids]
    down = [(row, c) for c in down_ids]

    return {
        "U": up,
        "L": left,
        "D": down,
        "R": right,
    }


def line_of_sight(mx, row, col):
    """
    renders a line of sight coord calculation, into the values
    """
    coords = line_of_sight_coords(mx, row, col)
    los = defaultdict(list)
    for k, ids in coords.items():
        for _row, _col in ids:
            los[k].append(mx[_row][_col])
    return los


def get_size(matrix):
    height = len(matrix)
    width = len(matrix[0])
    return height, width


def row_col_from_int(matrix, x):
    h, w = get_size(matrix)
    col = x % w
    row = x // h
    return row, col


def matrix_of_size(width, height, default=0):
    return [[default] * width for x in range(height)]


def set_matrix_dict(m):
    for x in range(len(m)):
        for y in range(len(m[x])):
            m[x][y] = {}
    return m


def pmx(*matrices, pad=True, space=True):
    """
    print a matrix of integers, zero turns to `.` for clarity
    """
    if len(matrices) > 1:
        matrices = list(zip(*matrices))
        for row in matrices:
            r = []
            for col in row:
                r.append("".join([f"{int(x)or '.'}".rjust(3) for x in col]))
            print(" ".join(r))
    else:
        for row in matrices:
            for c in row:
                if pad:
                    f = lambda x: f"{int(x)or '.'}".rjust(2)
                    if space:
                        f = lambda x: f"{int(x)or '.'}".rjust(3)
                else:
                    f = lambda x: f"{int(x)or '.'}"
                    if space:
                        f = lambda x: f"{int(x)or '.'} "
                print("".join([f(x) for x in c]))


def ppmx(*matrices, pad=True, space=True):
    """
    print a matrix of anything, Falsy values turns to `.` for clarity
    """
    if len(matrices) > 1:
        matrices = list(zip(*matrices))
        for row in matrices:
            r = []
            for col in row:
                r.append("".join([f"{x or '.'}".rjust(3) for x in col]))
            print(" ".join(r))
    else:
        for row in matrices:
            for c in row:
                if pad:
                    f = lambda x: f"{x or '.'}".rjust(2)
                    if space:
                        f = lambda x: f"{x or '.'}".rjust(3)
                else:
                    f = lambda x: f"{x or '.'}"
                    if space:
                        f = lambda x: f"{x or '.'} "
                print("".join([f(x) for x in c]))


def highlight(matrix, red=[], green=[], blue=[]):
    """
    print a matrix of anything, Falsy values turns to `.` for clarity
    """
    mx = deepcopy(matrix)
    for (y, x) in red:
        try:
            new = f"{colors.RED}{mx[y][x]}{colors.ENDC}"
            mx[y][x] = new
        except IndexError:
            breakpoint()
    for (y, x) in green:
        new = f"{colors.GREEN}{mx[y][x]}{colors.ENDC}"
        mx[y][x] = new
    for (y, x) in blue:
        new = f"{colors.BLUE}{mx[y][x]}{colors.ENDC}"
        mx[y][x] = new
    ppmx(mx, pad=False, space=False)
