#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from Utils.Check import Check
from typing import Any, Self, Optional
from Utils.Patterns import RE
from re import Match as REMatch
from inspect import stack as get_stack
from inspect import FrameInfo
from json import loads as json_decode
from json import dumps as json_encode
from random import random as math_random
from base64 import b64decode as base64_decode
from base64 import b64encode as base64_encode

class Utils:

    @classmethod
    def get_keys(cls:type[Self], *items:list[Any|None]) -> list[str]:
        return [key for item in items for key in (
            (item,) if Check.is_key(item) else 
            cls.get_keys(*item) if Check.is_array(item) else 
        tuple())]
    
    @classmethod
    def get_dictionaries(cls:type[Self], *items:list[Any|None]) -> list[dict[str, Any|None]]:
        return [dictionary for item in items for dictionary in (
            (item,) if Check.is_dictionary(item) else 
            cls.get_dictionaries(*item) if Check.is_array(item) else 
        tuple())]
    
    @classmethod
    def get_dictionary(cls:type[Self], *items:list[Any|None]) -> dict[str, Any|None]:
        return {key : value for item in items for key, value in (
            item if Check.is_dictionary(item) else 
            cls.get_dictionary(*item) if Check.is_array(item) else 
        {}).items()}
    
    @classmethod
    def get_values(cls:type[Self], keys:str|list|tuple, inputs:dict[str, Any|None]|list[Any|None]|tuple[Any|None], _default:Optional[Any] = None) -> Any|None:

        keys = cls.get_keys(keys)

        if len(keys):

            subinputs:dict[str, Any|None]

            for subinputs in cls.get_dictionaries(inputs):

                key:str 
                
                for key in keys:
                    if key in subinputs:
                        return subinputs[key]
        return _default
    
    @classmethod
    def string_variables(cls:type[Self], string:str, variables:dict[str, Any|None], _default:Optional[str] = None) -> str|None:
        
        variables = cls.get_dictionary(variables)

        def callback(matches:REMatch) -> str:

            key:str = matches.group(1)

            return (
                str(variables[key]) if key in variables else 
                str(_default) if _default is not None else 
            matches.group(0))

        return RE.STRING_VARIABLES.sub(callback, str(string))
        
    @staticmethod
    def get_action_data(i:int = 0) -> dict[str, str|int]:

        stack:FrameInfo = get_stack()[1 + i]

        return {
            "file" : stack.filename,
            "method" : stack.function,
            "line" : stack.lineno
        }
    
    @classmethod
    def get_strings(cls:type[Self], *items:list[Any|None]) -> list[str]:
        return [string for item in items for string in (
            cls.get_strings(*item) if Check.is_array(item) else (item,)
        ) if Check.is_string(string)]
    
    @staticmethod
    def json_encode(data:dict[str, Any|None]|list[Any|None]|tuple[Any|None]) -> str|None:
        try:
            return json_encode(data, ensure_ascii=False)
        except Exception as exception:
            pass
        return None
    
    @staticmethod
    def json_decode(data:str) -> dict[str, Any|None]|list[Any|None]|tuple[Any|None]|None:
        try:
            return json_decode(data)
        except Exception as exception:
            pass
        return None
    
    @staticmethod
    def to_snake(string:str) -> str:

        def callback(matches:REMatch) -> str:
            
            upper:str|None = matches.group(1)

            return "_" + upper.lower() if upper else "_"
        
        return RE.TO_SNAKE.sub(callback, string).lower()
    
    @staticmethod
    def recursive(data:str, callback:Any) -> str:

        cache:str = data

        while True:
            data = cache
            cache = callback(cache)
            if data == cache:
                break

        return cache
    
    @staticmethod
    def get_array(item:Any|None) -> list[Any|None]:
        return item if Check.is_array(item) else [item]
    
    @staticmethod
    def get_random(_from:Optional[int] = None, _to:Optional[int] = None) -> Any|None:
        return (
            math_random() if _from is None else 
            _from[int(math_random() * len(_from))] if Check.is_array(_from) else 
            (
                int(math_random() * _from) if _to is None else
                int(math_random() * (_to - _from) + _from) if Check.is_integer(_to) else 
                math_random() * (_to - float(_from)) + float(_from) if Check.is_float(_to) else
            None) if Check.is_integer(_from) else 
            (
                math_random() * _from if _to is None else 
                math_random() * (float(_to) - _from) + _from if Check.is_number(_to) else 
            None) if Check.is_float(_from) else 
        None)
        
    @classmethod
    def randomize_array(cls:type[Self], items:list[Any|None]) -> None:
        
        l:int = len(items)

        for i in range(l):

            j:int = cls.get_random(l)

            if i != j:
                items[i], items[j] = items[j], items[i]

    @staticmethod
    def base64_encode(data:str|bytes, encoding:str = "utf-8") -> str|None:
        try:
            return base64_encode(data.encode(encoding) if Check.is_string(data) else data).decode(encoding)
        except Exception as exception:
            pass
        return None
    
    @staticmethod
    def base64_decode(data:str|bytes, encoding:str = "utf-8", is_string:bool = True) -> str|bytes|None:
        try:

            decoded:bytes = base64_decode(data.encode(encoding) if Check.is_string(data) else data)
            
            return decoded.decode(encoding) if is_string else decoded
        except Exception as exception:
            pass
        return None