#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Part of the Reusables package.
#
# Copyright (c) 2014-2025 - Chris Griffith - MIT License
_roman_dict = {
    "I": 1,
    "IV": 4,
    "V": 5,
    "IX": 9,
    "X": 10,
    "XL": 40,
    "L": 50,
    "XC": 90,
    "C": 100,
    "CD": 400,
    "D": 500,
    "CM": 900,
    "M": 1000,
}


_numbers = {
    0: "zero",
    1: "one",
    2: "two",
    3: "three",
    4: "four",
    5: "five",
    6: "six",
    7: "seven",
    8: "eight",
    9: "nine",
    10: "ten",
    11: "eleven",
    12: "twelve",
    13: "thirteen",
    14: "fourteen",
    15: "fifteen",
    16: "sixteen",
    17: "seventeen",
    18: "eighteen",
    19: "nineteen",
    20: "twenty",
    30: "thirty",
    40: "forty",
    50: "fifty",
    60: "sixty",
    70: "seventy",
    80: "eighty",
    90: "ninety",
}

_places = {
    1: "",
    2: "thousand",
    3: "million",
    4: "billion",
    5: "trillion",
    6: "quadrillion",
    7: "quintillion",
    8: "sextillion",
    9: "septillion",
    10: "octillion",
    11: "nonillion",
    12: "decillion",
}


def cut(string, characters=2, trailing="normal"):
    """
    Split a string into a list of N characters each.

    ... code:: python

        reusables.cut("abcdefghi")
        # ['ab', 'cd', 'ef', 'gh', 'i']

    trailing gives you the following options:

    * normal: leaves remaining characters in their own last position
    * remove: return the list without the remainder characters
    * combine: add the remainder characters to the previous set
    * error: raise an IndexError if there are remaining characters

    ... code:: python

        reusables.cut("abcdefghi", 2, "error")
        # Traceback (most recent call last):
        #     ...
        # IndexError: String of length 9 not divisible by 2 to splice

        reusables.cut("abcdefghi", 2, "remove")
        # ['ab', 'cd', 'ef', 'gh']

        reusables.cut("abcdefghi", 2, "combine")
        # ['ab', 'cd', 'ef', 'ghi']

    :param string: string to modify
    :param characters: how many characters to split it into
    :param trailing: "normal", "remove", "combine", or "error"
    :return: list of the cut string
    """
    split_str = [string[i : i + characters] for i in range(0, len(string), characters)]

    if trailing != "normal" and len(split_str[-1]) != characters:
        if trailing.lower() == "remove":
            return split_str[:-1]
        if trailing.lower() == "combine" and len(split_str) >= 2:
            return split_str[:-2] + [split_str[-2] + split_str[-1]]
        if trailing.lower() == "error":
            raise IndexError("String of length {0} not divisible by {1} to cut".format(len(string), characters))
    return split_str


def int_to_roman(integer):
    """
    Convert an integer into a string of roman numbers.

    ... code: python

        reusables.int_to_roman(445)
        # 'CDXLV'


    :param integer:
    :return: roman string
    """
    if not isinstance(integer, int):
        raise ValueError("Input integer must be of type int")
    output = []
    while integer > 0:
        for r, i in sorted(_roman_dict.items(), key=lambda x: x[1], reverse=True):
            while integer >= i:
                output.append(r)
                integer -= i
    return "".join(output)


def roman_to_int(roman_string):
    """
    Converts a string of roman numbers into an integer.

    ... code: python

        reusables.roman_to_int("XXXVI")
        # 36


    :param roman_string: XVI or similar
    :return: parsed integer
    """
    roman_string = roman_string.upper().strip()
    if "IIII" in roman_string:
        raise ValueError("Malformed roman string")
    value = 0
    skip_one = False
    last_number = None
    for i, letter in enumerate(roman_string):
        if letter not in _roman_dict:
            raise ValueError("Malformed roman string")
        if skip_one:
            skip_one = False
            continue
        if i < (len(roman_string) - 1):
            double_check = letter + roman_string[i + 1]
            if double_check in _roman_dict:
                if last_number and _roman_dict[double_check] > last_number:
                    raise ValueError("Malformed roman string")
                last_number = _roman_dict[double_check]
                value += _roman_dict[double_check]
                skip_one = True
                continue
        if last_number and _roman_dict[letter] > last_number:
            raise ValueError("Malformed roman string")
        last_number = _roman_dict[letter]
        value += _roman_dict[letter]
    return value


def int_to_words(number, european=False):
    """
    Converts an integer or float to words.

    ... code: python

        reusables.int_to_number(445)
        # 'four hundred forty-five'

        reusables.int_to_number(1.45)
        # 'one and forty-five hundredths'

    :param number: String, integer, or float to convert to words. The decimal
        can only be up to three places long, and max number allowed is 999
        decillion.
    :param european: If the string uses the european style formatting, i.e.
        decimal points instead of commas and commas instead of decimal points,
        set this parameter to True
    :return: The translated string
    """

    def ones(n):
        return "" if n == 0 else _numbers[n]

    def tens(n):
        teen = int("{0}{1}".format(n[0], n[1]))

        if n[0] == 0:
            return ones(n[1])
        if teen in _numbers:
            return _numbers[teen]
        else:
            ten = _numbers[int("{0}0".format(n[0]))]
            one = _numbers[n[1]]
            return "{0}-{1}".format(ten, one)

    def hundreds(n):
        if n[0] == 0:
            return tens(n[1:])
        else:
            t = tens(n[1:])
            return "{0} hundred {1}".format(_numbers[n[0]], "" if not t else t)

    def comma_separated(list_of_strings):
        if len(list_of_strings) > 1:
            return "{0} ".format("" if len(list_of_strings) == 2 else ",").join(list_of_strings)
        else:
            return list_of_strings[0]

    def while_loop(list_of_numbers, final_list):
        index = 0
        group_set = int(len(list_of_numbers) / 3)
        while group_set != 0:
            value = hundreds(list_of_numbers[index : index + 3])
            if value:
                final_list.append("{0} {1}".format(value, _places[group_set]) if _places[group_set] else value)
            group_set -= 1
            index += 3

    number_list = []
    decimal_list = []

    decimal = ""
    number = str(number)
    group_delimiter, point_delimiter = (",", ".") if not european else (".", ",")

    if point_delimiter in number:
        decimal = number.split(point_delimiter)[1]
        number = number.split(point_delimiter)[0].replace(group_delimiter, "")
    elif group_delimiter in number:
        number = number.replace(group_delimiter, "")

    if not number.isdigit():
        raise ValueError("Number is not numeric")

    if decimal and not decimal.isdigit():
        raise ValueError("Decimal is not numeric")

    if int(number) == 0:
        number_list.append("zero")

    r = len(number) % 3
    d_r = len(decimal) % 3
    number = number.zfill(len(number) + 3 - r if r else 0)
    f_decimal = decimal.zfill(len(decimal) + 3 - d_r if d_r else 0)

    d = [int(x) for x in f_decimal]
    n = [int(x) for x in number]

    while_loop(n, number_list)

    if decimal and int(decimal) != 0:
        while_loop(d, decimal_list)

        if decimal_list:
            name = ""
            if len(decimal) % 3 == 1:
                name = "ten"
            elif len(decimal) % 3 == 2:
                name = "hundred"

            place = int((str(len(decimal) / 3).split(".")[0]))
            number_list.append(
                "and {0} {1}{2}{3}ths".format(
                    comma_separated(decimal_list), name, "-" if name and _places[place + 1] else "", _places[place + 1]
                )
            )

    return comma_separated(number_list)
