#!/usr/bin/env python3

# Python utils

# dennis(a)yurichev.com, 2017-2022

from typing import List
from typing import Any
import math, os

def read_lines_from_file (fname):
    f=open(fname)
    new_ar=[item.rstrip() for item in f.readlines()]
    f.close()
    return new_ar

# reverse list:
def rvr(i:List[Any]) -> List[Any]:
    return i[::-1]

def reflect_vertically(a:List[List[Any]]) -> List[List[Any]]:
    return [rvr(row) for row in a]

def reflect_horizontally(a:List[List[Any]]) -> List[List[Any]]:
    return rvr(a)

# N.B. must work on arrays of arrays of objects, not on arrays of strings!
def rotate_rect_array_90_CCW(a_in:List[List[Any]]) -> List[List[Any]]:
    a=reflect_vertically(a_in)
    rt=[]
    # reflect diagonally:
    for row in range(len(a[0])):
        #rt.append("".join([a[col][row] for col in range(len(a))]))
        rt.append([a[col][row] for col in range(len(a))])

    return rt

# angle: 0 - leave as is; 1 - 90 CCW; 2 - 180 CCW; 3 - 270 CCW
# FIXME: slow
def rotate_rect_array(a:List[List[Any]], angle:int) -> List[List[Any]]:
    if angle==0:
        return a
    assert (angle>=1)
    assert (angle<=3)

    for i in range(angle):
        a=rotate_rect_array_90_CCW(a)
    return a

# yet unused
# TODO: test rectangles
def rotate_rect_array_test():
    rnd=[[1,2,3],[4,5,6],[7,8,9]]
    rotate_rect_array(rnd, 1)==[[3, 6, 9], [2, 5, 8], [1, 4, 7]]
    rotate_rect_array(rnd, 2)==[[9, 8, 7], [6, 5, 4], [3, 2, 1]]
    rotate_rect_array(rnd, 3)==[[7, 4, 1], [8, 5, 2], [9, 6, 3]]

def adjacent_coords(X1:int, Y1:int, X2:int, Y2:int) -> bool:
    # return True if pair of coordinates laying adjacently: vertically/horizontally/diagonally:
    return any([X1==X2   and Y1==Y2+1,
    		X1==X2   and Y1==Y2-1,
    		X1==X2+1 and Y1==Y2,
    		X1==X2-1 and Y1==Y2,
    		X1==X2-1 and Y1==Y2-1,
    		X1==X2-1 and Y1==Y2+1,
    		X1==X2+1 and Y1==Y2-1,
    		X1==X2+1 and Y1==Y2+1])

def ANSI_set_normal_color(color):
    return '\033[%dm' % (color+31)

def ANSI_set_background_color(color):
    return '\033[%dm' % (color+41)

def ANSI_reset():
    return '\033[0m'

# by 5 parts
# print (my_utils.partition (list(range(20)), 5))
# return -> [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15], [16, 17, 18, 19]]
def partition(lst:List[Any], n:int) -> List[Any]:
    division = len(lst) / float(n)
    return [ lst[int(round(division * i)): int(round(division * (i + 1)))] for i in range(n) ]

def is_in_range_incl (v, low, high):
    if v>=low and v<=high:
        return True
    return False

def find_1st_elem_GE (array, v):
    for a in array:
        if a>=v:
            return a
    return None # not found

def find_1st_elem_LE (array, v):
    #print (array)
    for a in array[::-1]:
        if a<=v:
            return a
    return None # not found

def list_of_strings_to_list_of_ints (l):
    return list(map(lambda x: int(x), l))

# I use this to convert f##king JSON keys from strings to ints
def string_keys_to_integers(d):
    rt={}
    for k in d:
        if type(d[k]) == dict:
            rt[int(k)]=string_keys_to_integers(d[k]) # recursively
        else:
            rt[int(k)]=d[k]
    return rt

def element_in_array_is_in_range (array:List[Any], low, high) -> bool:
    for a in array:
        if is_in_range_incl(a, low, high):
            return True
    return False

def ceil_binlog(x:int) -> int:
    return math.ceil(math.log(x, 2))

# SageMath style
# for example, poly=0x1EDC6F41 (CRC-32C (Castagnoli))
# output = "a^28 + a^27 + a^26 + a^25 + a^23 + a^22 + a^20 + a^19 + a^18 + a^14 + a^13 + a^11 + a^10 + a^9 + a^8 + a^6 + 1"
def poly_to_str(poly:int) -> str:
    rt=[]
    size=ceil_binlog(poly)
    for i in range(size, -1, -1):
        if ((poly>>i)&1)==1:
            if i==0:
                rt.append("1")
            elif i==1:
                rt.append("a")
            else:
                rt.append("a^"+str(i))
    return " + ".join(rt)

def human(seconds:int) -> str:
    if seconds==0:
        return "0s"

    minutes=0
    hours=0
    days=0

    if seconds>=60:
        minutes = seconds // 60
        seconds = seconds - minutes*60

    if minutes>=60:
        hours = minutes // 60
        minutes = minutes - hours*60

    if hours>=24:
        days = hours // 24
        hours = hours - days*24

    rt=""

    if days>0:
        rt=rt+str(days)+"d"
    if hours>0:
        rt=rt+str(hours)+"h"
    if minutes>0:
        rt=rt+str(minutes)+"m"
    if seconds>0:
        rt=rt+str(seconds)+"s"

    return rt

def transpose_matrix(m):
    """
    # longer and verbose. but equivalent:
    rt=[]
    for x in zip(*m):
        rt.append(list(x))
    return rt
    """
    return [list(x) for x in zip(*m)]

# yet unused
# TODO: test rectangles
def transpose_matrix_test():
    rnd=[[1,2,3],[4,5,6],[7,8,9]]
    assert transpose_matrix(rnd)==[[1, 4, 7], [2, 5, 8], [3, 6, 9]]

# NOT USED: begin vvv
def pad_list_of_strings_by_max_string (l:List[str]) -> List[str]:
    return list(map(lambda s: s.ljust(max(map(len, l))), l))

def pad_list_of_strings_by_max_string_test():
    lst=["x", "y", "foo", "bar", "asdf"]
    # this will print: ['x   ', 'y   ', 'foo ', 'bar ', 'asdf']
    print (pad_list_of_strings_by_max_string(lst))

def concat_list_of_strings_side_by_side(lst:List[List[str]]) -> List[str]:
    t=transpose_matrix(lst)
    return ["".join(x) for x in t]

"""
this will print:
column 1 | 2nd column
foo      | x
bar      | y
asd      | ______
0        | 01234
1        |
         | 20
"""
def concat_list_of_strings_side_by_side_test():
    l1=["column 1", "foo", "bar", "asd", "0", "1", ""]
    l2=[" | "]*len(l1)
    l3=["2nd column", "x", "y", "______", "01234", "", "20"]
    l1_padded=pad_list_of_strings_by_max_string (l1)
    l2_padded=pad_list_of_strings_by_max_string (l2)
    l3_padded=pad_list_of_strings_by_max_string (l3)
    for l in concat_list_of_strings_side_by_side([l1_padded, l2_padded, l3_padded]):
        print (l)
# NOT USED: end ^^^

# gen random 32 bits
def get_32_bits_from_urandom():
    return int.from_bytes(os.urandom(4), byteorder='big')&0xffffffff

def uniq_list(lst):
    return list(set(lst))

