# OBSAH SOUBORU:
# - pomocne funkce

import re

from openpyxl import Workbook
from openpyxl.cell import Cell
from openpyxl.worksheet.worksheet import Worksheet

from itertools import groupby, count
from enums import EBorder, EBorderStyle, EColor
import settings


def sheet_input(user_input: str, wb: Workbook):
    if not wb:
        raise RuntimeError('Soubor je neplatný nebo neexistuje')

    digit = re.search(r'\d{1,2}', user_input)
    if digit is not None and digit.group() == user_input:
        if 0 < int(user_input) < len(wb.sheetnames):
            return wb[wb.sheetnames[int(user_input)]]

    sheet_names = [sh.upper() for sh in wb.sheetnames]

    if user_input.upper() in sheet_names:
        return wb[user_input]

    return wb.active


# prevzato z https://codereview.stackexchange.com/questions/5196/grouping-consecutive-numbers-into-ranges-in-python-3-2
# byl jsem schopen vymyslet jen zpusob kdyz jdou vsechny po sobe, tohle je na me celkem masakr.
def as_range(iterable):
    lst = list(iterable)
    if len(lst) > 1:
        return f'{lst[0]}-{lst[-1]}'
    else:
        return f'{lst[0]}'


def make_ranges(iterable):
    """1 2 3 5 6 7 12 -> 1-3, 5-7, 12"""
    return ','.join(as_range(g) for _, g in groupby(iterable, key=lambda n, c=count(): n-next(c)))


def has_odd_nums_upto_n(all_nums, compared):
    """Funkce zjistuje zda cisla v seznamu 'compared' lze nahradit necim takovym:
        - n == 6: 1 2 3 5 6 -> odd, 2, 6
        - n == 8: 1 2 3 5 6 -> False, None
        Neboli, zda-li vsechna licha cisla v seznamu 'compared' odpovidaji vsem lichym cislum v seznamu 'all_nums',
        ktery odpovida indexum shodnych radku/sloupcu/okraju

        Parameters:
            all_nums: list[int] - seznam cisel, u kterych zkoumam zda obsahuji vsechna licha cisla do n
            compared: list[int] - seznam lichych cisel do n"""

    if len(all_nums) < 4:
        return False, all_nums

    # nejprve musim ze vstupniho (ten ktery me zajima) vytahnout vsecky liche cisla
    only_odd = []
    other = []

    for i in all_nums:
        if i % 2 != 0:
            only_odd.append(i)
        else:
            other.append(i)

    # pak oba seznamy prevedu na set a zkoumam zda jsou totozne. nemuze nastat ze by se nekde prvky opakovaly
    if set(only_odd) == set(compared):
        return True, other

    return False, None


# totez, ale pro suda cisla
# neboli k tomu, abych mohl udelat napr. 2 3 4 6 -> even, 3
def has_even_nums_upto_n(all_nums: 'list[int]', compared: 'list[int]'):
    """Funkce zjistuje zda cisla v seznamu 'compared' lze nahradit necim takovym:
        - n == 6: 1 2 3 4 6 -> even, 1, 3
        - n == 8: 1 2 3 4 6 -> False, None
       Neboli, zda-li vsechna suda cisla v seznamu 'compared' odpovidaji vsem lichym cislum v seznamu 'all_nums',
       ktery odpovida indexum shodnych radku/sloupcu/okraju

       Parameters:
            all_nums: list[int] - seznam cisel, u kterych zkoumam zda obsahuji vsechna suda cisla do n
            compared: list[int] - seznam suda cisel do n"""
    if len(all_nums) < 4:
        return False, all_nums

    only_even = []
    other = []

    for i in all_nums:
        if i % 2 == 0:
            only_even.append(i)
        else:
            other.append(i)

    if set(only_even) == set(compared):
        return True, other

    return False, None


def is_consecutive(iterable):
    """Funkce zkouma zda jdou cisla v posloupnosti 'iterable' po sobe, tj. jedna-li se o artimetickou posloupnost
    s diferenci 1"""

    if len(iterable) <= 1:
        return True

    # aby se skutecne jednalo o posloupnost s diferenci 1, musi byt rozdil mezi min a max clenem presne n - 1
    # zaroven nejmensi a nejvetsi clen musi byt prvni a posledni

    first = iterable[0]
    last = iterable[-1]

    if last - first != len(iterable) - 1:
        return False

    # pokud je ta matematicka podminka splnena, tak jen zkoumam zda se vsechny cleny lisi.
    # set() ma tu vyhodu, ze jeho prvky se nemuzou opakovat, tak toho vyuzijem.
    to_set = set(iterable)

    return len(to_set) == len(iterable)


def is_merged(cell: Cell, ws: Worksheet):
    """U konkterni bunky zjistuje zda se nachazi ve sloucene bunce. Pokud ano, vraci celou sloucenou bunku (MergedCell),
    jinak None."""

    if ws is None:
        return None

    # sloucene bunky jsou ulozene primo v .xlsx souboru. openpyxl si je uklada do tohoto pole.
    for merged_cell in ws.merged_cells.ranges:
        if cell.coordinate in merged_cell:
            return merged_cell

    return None


def col_to_num(text_part: str) -> int:
    # textovou cast prevedeme na cislo s vyuzitim ASCII tabulky
    # v ni je 'A' == 65, 'B' == 66, ...,  'Z' == 90
    # abychom pro A dostali index 1, odecteme 64
    # pro textovou cast tvaru AB vime, ze x souradnice musi byt ord(A) + 25 + ord(B),
    # podobne pro ABC, atd. => + (25*i)
    converted = 0
    for i in range(0, len(text_part)):
        converted += ord(text_part[i]) - 64 + (25 * i)
    return converted


def num_to_col(num_idx: int) -> str:
    # zde je podobny princip jako u col_to_num. vychazime z vysledku teto metody.
    converted = ''
    while num_idx > 0:
        num_idx, r = divmod(num_idx - 1, 26)
        converted = chr(r + ord('A')) + converted
    return converted


def BS(border_style: EBorderStyle, idx: 'list[int]'):
    """Slouzi ke zkraceni zapisu v pripade, ze u jedne bunky potrebuju vytvorit seznam 4 okraju kde se jich X opakuje
        (a potrebuju je mit na urcitych mistech, danymi listem 'idx')"""
    start_list = [EBorderStyle.NONE, EBorderStyle.NONE, EBorderStyle.NONE, EBorderStyle.NONE]
    for i in idx:
        start_list[i] = border_style
    return tuple(start_list)


def BC(clr: EColor, idx: 'list[int]'):
    """Slouzi ke zkraceni zapisu v pripade, ze u jedne bunky potrebuju vytvorit seznam 4 barev kde se jich X opakuje
        (a potrebuju je mit na urcitych mistech, danymi listem 'idx')"""
    start_list = [EColor.NOT_SET, EColor.NOT_SET, EColor.NOT_SET, EColor.NOT_SET]
    for i in idx:
        start_list[i] = clr
    return tuple(start_list)


def is_odd_table_col(idx, num_cols, even_w, odd_w, start_idx=1):
    """Nekdo v excelu si rekl ze pruhovat sloupce nepujde jen na sude a liche, ale bude mozne si nastavit sirku techto
    pruhu. Funkce z techto sirek tedy zjistuje, do ktere oblasti bunka patri."""

    if even_w == odd_w and even_w == 1:
        return (idx + start_idx - 1) % 2 == 1

    delta = odd_w
    j = start_idx
    is_odd = True

    # znam sirku jednotlivych typu sloupcu a vim ze vzdy zacinam od indexu 1 (tj. prvniho sloupce) ktery je povazovan
    # za 'lichy'. tak proste postupne prictitam a menim ty sirky dokud se nedostanu nad hledany index.
    for _ in num_cols:
        if (j + delta) > idx:
            return is_odd

        j += delta
        if delta == even_w:
            delta = odd_w
        else:
            delta = even_w

        is_odd = not is_odd

    return is_odd


def is_odd_table_row(idx, num_cols, even_h, odd_h, start_idx=1):
    """Nekdo v excelu si rekl ze pruhovat radky nepujde jen na sude a liche, ale bude mozne si nastavit vysku techto
    pruhu. Funkce z techto vysek tedy zjistuje, do ktere oblasti bunka patri."""

    if even_h == odd_h and even_h == 1:
        return (idx + start_idx - 1) % 2 == 1

    delta = odd_h
    j = start_idx
    is_odd = True

    for _ in num_cols:
        if (j + delta) > idx:
            return is_odd

        j += delta
        if delta == even_h:
            delta = odd_h
        else:
            delta = even_h

        is_odd = not is_odd

    return is_odd


def cell_to_index(cell_name: str):
    """Prevadi textovou reprezentaci polohy bunky na ciselnou, tedy B5 na [2,5]

    Parameters:
        cell_name (str): string ve kterem je textova poloha bunky

    Returns:
        list s XY souradnicemi teto bunky (souradnice v Excelu zacinaji od 1)"""

    # rozdelime cell_name na textovou a ciselnou cast pomoci regex (obe casti mohou mit ruznou delku)
    # [A-Z]{1,3} znamena, ze hledame posloupnost prave jednoho az prave 3 velkych pismen anglicke abecedy
    # \d+ znamena, ze hledame alespon jednu cislici (0-9)
    text_part_mtc = re.search(r'[A-Z]{1,3}', cell_name)
    num_part_mtc = re.search(r'\d+', cell_name)

    if text_part_mtc is not None:
        text_part = text_part_mtc.group()
    else:
        return None, None

    if num_part_mtc is not None:
        num_part = num_part_mtc.group()
    else:
        return None, None

    # ujistime se ze obe casti neco obsahuji
    if not text_part:
        settings.log_str += f'[VSTUP] Neplatný vstup \'{cell_name}\'. Použije se \'A1\''
        return [0, 0]
    if not num_part:
        settings.log_str += f'[VSTUP] Neplatný vstup \'{cell_name}\'. Použije se \'A1\''
        return [0, 0]

    return [col_to_num(text_part), int(num_part)]


def validate_input_range(ws: Worksheet, minx, miny, maxx, maxy):
    """Funkce pro kontrolu zadané vstupní oblasti. Ať uživatel zadal jakoukoli oblast, tato funkce to ořízne
    na oblast, ve které jsou data."""
    if not ws:
        return None, None, None, None

    if None in [minx, miny, maxx, maxy]:
        return None, None, None, None

    corr = False
    if minx < ws.min_column or minx > ws.max_column:
        if minx != maxx:
            minx = ws.min_column
        else:
            minx = ws.min_column
            maxx = ws.min_column

        corr = True

    if miny < ws.min_row or miny > ws.max_row:
        if miny != maxy:
            miny = ws.min_row
        else:
            miny = ws.min_row
            maxy = ws.min_row

        corr = True

    if maxy > ws.max_row:
        if maxy != miny:
            maxy = ws.max_row
        else:
            maxy = ws.max_row
            maxx = ws.max_row

        corr = True

    if maxx > ws.max_column:
        if maxx != minx:
            maxx = ws.max_column
        else:
            maxx = ws.max_column
            maxy = ws.max_column

        corr = True

    if corr:
        new_range = f'{num_to_col(minx)}{miny}:{num_to_col(maxx)}{maxy}'
        settings.log_str += f'[VSTUP] Zadaná oblast přesahuje oblast s daty. Změněno na \'{new_range}\'.\n'

    return minx, miny, maxx, maxy


def range_to_index(user_input: str):
    """Prevadi rozsah bunek zadany uzivatelem na indexy prvni a posledni bunky, tedy A1:C5 na [(1,1), (3,5)]

    Parameters:
        user_input (str): string ktery reprezentuje zadany rozsah bunek (predpokladame ho ve formatu X:Y)

    Returns:
        (start_x, start_y), (end_x, end_y)
    """

    # rozdelime cely vstup na jednotlive bunky
    parts = user_input.split(':')

    # zkontrolujeme ze byl ve spravnem formatu
    if len(parts) != 2:
        return (None, None), (None, None)

    # prevedeme jednotlive bunky na indexy
    start_indexes = cell_to_index(parts[0])
    end_indexes = cell_to_index(parts[1])

    if (None, None) in [start_indexes, end_indexes]:
        return (None, None), (None, None)

    # kontrola ze start < konec
    corr = False
    if start_indexes[0] > end_indexes[0]:
        start_indexes[0], end_indexes[0] = end_indexes[0], start_indexes[0]
        corr = True

    if start_indexes[1] > end_indexes[1]:
        start_indexes[1], end_indexes[1] = end_indexes[1], start_indexes[1]
        corr = True

    if start_indexes == end_indexes:
        settings.log_str += '[VSTUP] Zadaná oblast má velikost 1 buňky\n'
    elif corr:
        settings.log_str += f'[VSTUP] Zadané indexy jsou ve špatném pořadí. ' \
                            f'Použije se oblast {parts[1]}:{parts[0]}\n'

    return start_indexes, end_indexes

