from pathlib import Path
from numpy import ndarray
from typing import Any, Literal


def text_float(text: str | int | float | Any):
    """Checks if a string can be safely converted to float. [Integers are accepted here too]

    Args:
        text (str): string to be checked if its a convertable to float.

    Returns:
        bool: Returns true if it can, otherwise false
    """
    if not isinstance(text, (int, float, str)):
        return False
    try:
        float(text)
        return True
    except:
        return False


def text_integer(text: str | int | float | Any):
    """Checks if a string can be converted to integer.

    Args:
        text (str): string to be checked if its a convertable to integer.

    Returns:
        bool: True if it can be safely converted to integer, otherwise False.
    """
    if not isinstance(text, (int, float, str)):
        return False
    try:
        int(text)
        return True
    except:
        return False


def keys_in_text(
    sources: str,
    has_any: str | list[str],
    force_lower_case: bool = False,
):
    """Basicaly the reverse of **in** function, it can be used to locate if **sources** contains anything from the **has_any** into it.

    Args:
        sources (str):
            Target string to be checked if has any of the provided keys.
        has_any (str | list[str]):
            The string or list of strings to be checked if they are or not in the text.
            If its a string each letter will be checked, if its a list of string, then each word in the list will be checked instead.
        force_lower_case (bool, optional):
            If true will set everything to lower-case (both source and has_any).
            This is useful for tasks that dont require a case-sensitive scan. Defaults to False.

    Returns:
        bool: If any key was found will be returned as true, otherwise False.
    """
    if not is_string(sources) or (not is_string(has_any) and not is_array(has_any)):
        return False

    if is_array(has_any):
        has_any = [
            x.lower() if force_lower_case else x for x in has_any if is_string(x)
        ]
        assert has_any, "has_any had no valid string!"

    if force_lower_case:
        sources = sources.lower()
        if is_string(has_any):
            has_any = has_any.lower()  # type: ignore

    for elem in has_any:
        if elem in sources:
            return True
    return False


def is_string_number(entry: Any | str):
    if not is_string(entry):
        return False
    return text_float(entry) or text_integer(entry)


def is_int(entry: Any, check_string: bool = False) -> bool:
    """Checks if a number is a valid integer.

    Args:
        entry (Any): The item to be check if is a valid integer
        check_string (bool, optional): To check strings to see if its a possible number. Defaults to False.

    Returns:
        bool: True if its a True integer otherwise False.
    """
    if not check_string:
        return isinstance(entry, int)
    return isinstance(entry, int) or (check_string and text_integer(entry))


def is_float(entry: Any, check_string: bool = False) -> bool:
    """
    Checks if the entry provided is a valid float. It can check if a string can be converted to float if check_string is True.

    Args:
        entry (Any): The value to be checked.
        check_string (bool, optional): If True, it will check if the value can be converted to float.

    Returns:
        bool: If True, means that is a valid float otherwise false.
    """
    if not check_string:
        return isinstance(entry, float)
    return isinstance(entry, (int, float)) or (check_string and text_integer(entry))


def is_number(entry: Any, check_string: bool = False) -> bool:
    """Check if the entry is a number (being either a int or float). It also check if in case its a string (and check_string is True) if its a valid number if converted.

    Args:
        entry (Any): The value to be checked.
        check_string (bool, optional): If True will consider strings to possible be non-converted numbers. Defaults to False.

    Returns:
        bool: True if the value is either a float or a integer, otherwise False.
    """
    return is_int(entry, check_string) or is_float(entry, check_string)


def is_numpy_array(entry: Any, allow_empty: bool = True) -> bool:
    return isinstance(entry, ndarray) and (
        allow_empty or bool(len(entry.flatten().tolist()))
    )


def is_string(
    entry: Any,
    allow_empty: bool = False,
) -> bool:
    """Check if a value is a string or a valid Path object."""
    return isinstance(entry, (str, Path)) and (allow_empty or bool(str(entry).strip()))


def is_list(entry: Any, allow_empty: bool = False):
    """Check if the provided entry is a list and if its not empty.

    Args:
        entry (Any): _description_
        allow_empty (bool, optional): If true, will return True if the item is a list, regardless if its empty or not. Defaults to False.

    Returns:
        bool: True if the entry is a list and if either the allow_empty is true or the list is not empty.
    """
    return isinstance(entry, list) and (allow_empty or bool(entry))


def is_tuple(entry: Any, allow_empty: bool = False):
    """Check if the provided entry is a valid dictionary and if it has content or not (if allow_empty is False).

    Args:
        entry (Any): The value to be checked if its True.
        allow_empty (bool, optional): If True it allow empty dictionaries to be evaluated, otherwise it requires it to be a dictionary and have at least some content there. Defaults to False.

    Returns:
        bool: True if valid dictionary and (if allow_empty is False) if it has some content in it.
    """
    return isinstance(entry, tuple) and (allow_empty or bool(entry))


def is_dict(entry: Any, allow_empty: bool = False) -> bool:
    """Check if the provided entry is a valid dictionary and if it has content or not (if allow_empty is False).

    Args:
        entry (Any): The value to be checked if its True.
        allow_empty (bool, optional): If True it allow empty dictionaries to be evaluated, otherwise it requires it to be a dictionary and have at least some content there. Defaults to False.

    Returns:
        bool: True if valid dictionary and (if allow_empty is False) if it has some content in it.
    """
    return isinstance(entry, dict) and (allow_empty or bool(entry))


def is_array(entry: Any, allow_empty: bool = False, check_dict: bool = False):
    """Checks if the entry is either a list, tuple, or dictionary it also check if its empty if allow_empty is False.

    Args:
        entry (Any): Value to be analised.
        allow_empty (bool, optional): If True will allow empty arrays to be returned as True. Defaults to False.
        check_dict (bool, optional): If True will check for dictionary types too. Defaults to False.

    Returns:
        bool: If True the value is a valid (non-empty if allow_empty is False else it returns true just for being a list or tuple).
    """
    if (
        (not check_dict and not isinstance(entry, (list, tuple)))
        or check_dict
        and not isinstance(entry, (list, tuple, dict))
    ):
        return False
    return allow_empty or bool(entry)


def compare_none(arg1: Any | None, arg2: Any) -> Any:
    """
    arg1 if its not None or arg2.

    Useful to allow a different aproach than 'or' operator in strings, for example:

    Consider that the arguments as:
    ```py
    arg1 = 0
    arg2 = 3
    ```
    If using or operator directly the following would happen:

    ```python
    results = arg1 or arg2
    # results = arg2 (3)
    ```
    It checks for Falsely data in the first item, but sometimes that value would be valid even if falsely like: `0`, `""`, `[]`, `{}`, `()` and `False`.

    So, it was made to check if the first value is None or non-None if None it uses the arg2, otherwise it returns the arg1 even if falsely.

    example:
    ```
    from gr1336_toolbox import compare_none
    
    results = compare_none(arg1, arg2)
    # results = arg1 (0)
    ```

    """
    return arg1 if arg1 is not None else arg2


def valid_path(
    entry: str | Path, expected_dir: Literal["file", "path", "any"] = "any"
) -> bool:
    """Checks if `entry` is a valid existent path"""
    if not is_string(entry) or not Path(entry).exists():
        return False
    entry = Path(entry)
    if expected_dir == "any":
        return True
    elif expected_dir == "file":
        return entry.is_file()
    else:
        return entry.is_dir()


__all__ = [
    "is_int",
    "is_string",
    "is_dict",
    "is_float",
    "is_number",
    "is_tuple",
    "is_list",
    "is_array",
    "is_numpy_array",
    "keys_in_text",
    "is_string_number",
    "compare_none",
    "valid_path",
]
