# -*- coding: utf-8 -*-
# Copyright 2024, SERTIT-ICube - France, https://sertit.unistra.fr/
# This file is part of sertit-utils project
#     https://github.com/sertit/sertit-utils
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Tools concerning strings """

import argparse
import logging
import re
import uuid
from datetime import date, datetime
from typing import Any, Union

from sertit.logs import SU_NAME

LOGGER = logging.getLogger(SU_NAME)
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"


def str_to_bool(bool_str: str) -> bool:
    """
    Convert a string to a bool.

    Accepted values (compared in lower case):

    - :code:`True` <=> :code:`yes`, :code:`true`, :code:`t`, :code:`1`
    - :code:`False` <=> :code:`no`, :code:`false`, :code:`f`, :code:`0`

    Args:
        bool_str: Bool as a string

    Returns:
        bool: Boolean value

    Examples:
        >>> str_to_bool("yes") == True  # Works with "yes", "true", "t", "y", "1" (accepted with any letter case)
        True

        >>> str_to_bool("no") == False  # Works with "no", "false", "f", "n", "0" (accepted with any letter case)
        True
    """

    if isinstance(bool_str, bool):
        return bool_str

    true_str = ("yes", "true", "t", "y", "1")
    false_str = ("no", "false", "f", "n", "0")

    if bool_str.lower() in true_str:
        bool_val = True
    elif bool_str.lower() in false_str:
        bool_val = False
    else:
        raise ValueError(
            f"Invalid true or false value, "
            f"should be {true_str} if True or {false_str} if False, not {bool_str}"
        )
    return bool_val


def str_to_verbosity(verbosity_str: str) -> int:
    """
    Return a logging level from a string (compared in lower case).

    - :code:`DEBUG`   <=> {:code:`debug`, :code:`d`, :code:`10`}
    - :code:`INFO`    <=> {:code:`info`, :code:`i`, :code:`20`}
    - :code:`WARNING` <=> {:code:`warning`, :code:`w`, :code:`warn`}
    - :code:`ERROR`   <=> {:code:`error`, :code:`e`, :code:`err`}

    Args:
        verbosity_str (str): String to be converted

    Returns:
        logging level: Logging level (INFO, DEBUG, WARNING, ERROR)

    Examples:
        >>> str_to_bool("d") == logging.DEBUG  # Works with 'debug', 'd', 10 (accepted with any letter case)
        True

        >>> str_to_bool("i") == logging.INFO  # Works with 'info', 'i', 20 (accepted with any letter case)
        True

        >>> str_to_bool("w") == logging.WARNING  # Works with 'warning', 'w', 'warn', 30 (accepted with any letter case)
        True

        >>> str_to_bool("e") == logging.ERROR  # Works with 'error', 'e', 'err', 40 (accepted with any letter case)
        True
    """
    debug_str = ("debug", "d", 10)
    info_str = ("info", "i", 20)
    warn_str = ("warning", "w", "warn", 30)
    err_str = ("error", "e", "err", 40)

    if isinstance(verbosity_str, str):
        verbosity_str = verbosity_str.lower()

    if verbosity_str in info_str:
        verbosity = logging.INFO
    elif verbosity_str in debug_str:
        verbosity = logging.DEBUG
    elif verbosity_str in warn_str:
        verbosity = logging.WARNING
    elif verbosity_str in err_str:
        verbosity = logging.ERROR
    else:
        raise argparse.ArgumentTypeError(
            f"Incorrect logging level value: {verbosity_str}, "
            f"should be {info_str}, {debug_str}, {warn_str} or {err_str}"
        )

    return verbosity


def str_to_list(
    list_str: Union[str, list], additional_separator: str = "", case: str = None
) -> list:
    """
    Convert str to list with :code:`,`, :code:`;`, :code:`\x20` separators.

    Args:
        list_str (Union[str, list]): List as a string
        additional_separator (str): Additional separators. Base ones are :code:`,`, :code:`;`, :code:`\\\x20` (space).
        case (str): {none, 'lower', 'upper'}

    Returns:
        list: A list from split string

    Example:
        >>> str_to_list("A, B; C D")
        ["A", "B", "C", "D"]
    """
    if isinstance(list_str, str):
        # Concatenate separators
        separators = ",|;| "
        if additional_separator:
            separators += "|" + additional_separator

        # Split
        listed_str = re.split(separators, list_str)
    elif isinstance(list_str, list):
        listed_str = list_str
    else:
        raise ValueError(
            f"List should be given as a string or a list of string: {list_str}"
        )

    out_list = []
    for item in listed_str:
        # Check if there are null items
        if item:
            if case == "lower":
                item_case = item.lower()
            elif case == "upper":
                item_case = item.upper()
            else:
                item_case = item

            out_list.append(item_case)

    return out_list


def str_to_date(
    date_str: Union[str, datetime], date_format: str = DATE_FORMAT
) -> datetime:
    """
    Convert string to a :code:`datetime.datetime`.

    Also accepted date formats:

    - :code:`now`: datetime.today()
    - Usual JSON date format: :code:`%Y-%m-%d`
    - Already formatted datetimes and dates

    Args:
        date_str (str): Date as a string
        date_format (str): Format of the date (as ingested by strptime)

    Returns:
        datetime.datetime: A date as a python datetime object

    Examples:
        # Default date format (isoformat)
        >>> str_to_date("2020-05-05T08:05:15")
        datetime(2020, 5, 5, 8, 5, 15)

        # This usual JSON format is also accepted
        >>> str_to_date("2019-08-06")
        datetime(2019, 8, 6)

        # User date's format
        >>> str_to_date("20200909105055", date_format="%Y%m%d%H%M%S")
        datetime(2020, 9, 9, 10, 50, 55)
    """
    if isinstance(date_str, datetime):
        dtm = date_str
    elif isinstance(date_str, date):
        dtm = datetime.fromisoformat(date_str.isoformat())
    else:
        try:
            if date_str.lower() == "now":
                # Now with correct format (no microseconds if not specified and so on)
                dtm = datetime.strptime(
                    datetime.today().strftime(date_format), date_format
                )
            else:
                dtm = datetime.strptime(date_str, date_format)
        except ValueError:
            # Just try with the usual JSON format
            json_date_format = "%Y-%m-%d"
            try:
                dtm = datetime.strptime(date_str, json_date_format)
            except ValueError as ex:
                raise ValueError(
                    f"Invalid date format: {date_str}; should be {date_format} "
                    f"or {json_date_format}"
                ) from ex
    return dtm


def str_to_list_of_dates(
    date_str: Union[list, str],
    date_format: str = DATE_FORMAT,
    additional_separator: str = "",
) -> list:
    """
    Convert a string containing a list of dates to a list of :code:`datetime.datetime`.

    Also accepted date formats:

    - :code:`now`: datetime.today()
    - Usual JSON date format: :code:`%Y-%m-%d`
    - Already formatted datetimes and dates

    Args:
        date_str (Union[list, str]): Date as a string
        date_format (str): Format of the date (as ingested by strptime)
        additional_separator (str): Additional separator

    Returns:
        list: A list containing datetimes objects

    Example:
        >>> # Default date format (isoformat)
        >>> str_to_list_of_dates("20200909105055, 2019-08-06;19560702121212\t2020-08-09",
        >>>                      date_format="%Y%m%d%H%M%S",
        >>>                      additional_separator="\t")
        [datetime(2020, 9, 9, 10, 50, 55), datetime(2019, 8, 6), datetime(1956, 7, 2, 12, 12, 12), datetime(2020, 8, 9)]
    """
    # Split string to get a list of strings
    list_of_dates_str = str_to_list(date_str, additional_separator)

    # Convert strings to date
    list_of_dates = [str_to_date(dt, date_format) for dt in list_of_dates_str]

    return list_of_dates


def to_cmd_string(unquoted_str: Any) -> str:
    """
    Add quotes around the string in order to make the command understand it's a string
    (useful with tricky symbols like :code:`&` or white spaces):

    Args:
        unquoted_str (str): String to update

    Returns:
        str: Quoted string

    Example:
        >>> # This str wont work in the terminal without quotes (because of the &)
        >>> pb_str = r"D:/Minab_4-DA&VHR/Minab_4-DA&VHR.shp"
        >>> to_cmd_string(pb_str)
        "\"D:/Minab_4-DA&VHR/Minab_4-DA&VHR.shp\""
    """
    if not isinstance(unquoted_str, str):
        unquoted_str = str(unquoted_str)

    cmd_str = unquoted_str
    if not unquoted_str.startswith('"'):
        cmd_str = '"' + cmd_str
    if not unquoted_str.endswith('"'):
        cmd_str = cmd_str + '"'
    return cmd_str


def snake_to_camel_case(snake_str: str) -> str:
    """
    Convert a :code:`snake_case` string to :code:`CamelCase`.

    Args:
        snake_str (str): String formatted in snake_case

    Returns:
        str: String formatted in CamelCase

    Example:
        >>> snake_to_camel_case("snake_case")
        "SnakeCase"
    """
    return "".join((w.capitalize() for w in snake_str.split("_")))


def camel_to_snake_case(snake_str: str) -> str:
    """
    Convert a :code:`CamelCase` string to :code:`snake_case`.

    Args:
        snake_str (str): String formatted in CamelCase

    Returns:
        str: String formatted in snake_case

    Example:
        >>> camel_to_snake_case("CamelCase")
        "camel_case"
    """
    return "".join(["_" + c.lower() if c.isupper() else c for c in snake_str]).lstrip(
        "_"
    )


def is_uuid(uuid_string, version=4):
    """
    Check if uuid_string is a valid UUID

    Args:
        uuid_string (str): UUID to be tested
        version (int): 4 (default), UUID version

    Returns:
        bool: Valid UUID or not
    """
    if not 1 <= version <= 5:
        raise ValueError(
            f"Illegal version number: {version} (should be between 1 and 5)"
        )

    try:
        uid = uuid.UUID(str(uuid_string), version=version)
        return uid.hex == uuid_string.replace("-", "")
    except ValueError:
        return False
