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

from Interfaces.OpoTestsInterface import OpoTestsInterface
from typing import Self, Any
from Utils.Patterns import RE
from Utils.Check import Check
from Utils.Utils import Utils
from re import Match as REMatch
from Interfaces.FormatModeInterface import FormatModeInterface
from Modules.Format.CapitalizeFormat import CapitalizeFormat
from Modules.Format.MixFormat import MixFormat
from Modules.Format.PlainFormat import PlainFormat
from Modules.Format.RandomFormat import RandomFormat
from Modules.Format.RangeFormat import RangeFormat
from Modules.Format.SelectFormat import SelectFormat
from Modules.Format.SerieFormat import SerieFormat

class FormatModule:

    def __init__(self:Self, op:OpoTestsInterface) -> None:
        self.op:OpoTestsInterface = op
        self.modes:dict[str, FormatModeInterface] = {}

        for keys, mode in (
            (("capitalize", "cap", "capital", "Cap", "Capitalize", "Capital"), CapitalizeFormat(self)), 
            (("mix", "mixin"), MixFormat(self)), 
            (("Mix", "Mixin"), MixFormat(self, {"capitalized" : True})), 
            (("plain",), PlainFormat(self)), 
            (("rand", "random"), RandomFormat(self)), 
            (("range",), RangeFormat(self)), 
            (("select",), SelectFormat(self)), 
            (("Select",), SelectFormat(self, {"capitalized" : True})), 
            (("serie",), SerieFormat(self)), 
        ):
            for key in keys:
                self.modes[key] = mode

    def start(self:Self) -> None:
        pass

    @staticmethod
    def set_fragments_level(string:str, fragments:list[str]) -> str:
        return RE.FRAGMENT_PATTERN.sub(lambda matches:fragments[int(matches.group(1))], string)
    
    @staticmethod
    def __build_fragment(matches:REMatch, fragments:list[str]) -> str:
        fragments += [matches.group(1)]
        return "$$$" + str(len(fragments) - 1) + "$$$"
    
    @classmethod
    def build_fragments(cls:type[Self], string:str, fragments:list[str] = []) -> str:
        return cls.set_fragments_level(Utils.recursive(string, lambda string:(
            RE.FRAGMENT_VARIABLES.sub(lambda matches:(
                cls.__build_fragment(matches, fragments)
            ), string)
        )), fragments)
    
    def __process_variable(self:Self, 
        i:int, 
        original:str, 
        key:str, 
        shared:dict[str, Any|None], 
        fragments:list[str]
    ) -> str:
        if Check.is_key(key):
            if key in shared:
                return str(shared[key])
            
            variable:list[Any|None] = self.op.questions.get_value(key, i)
            method:str
            data:list[Any|None]

            if variable is not None:
                method, *data = variable
                if method in self.modes:
                    return self.modes[method].get(i, data, shared, fragments)
        else:

            matches:REMatch|None = RE.FORMAT_SPLIT.match(key)

            if matches:
                method, *data = matches.groups()[1:3]
                if method in self.modes:
                    return self.modes[method].get(i, data, shared, fragments)
        return original
    
    def __execute_varibles(self:Self, 
        i:int, 
        string:str, 
        shared:dict[str, Any|None], 
        fragments:list[str]
    ) -> str:
        try:
            return RE.FRAGMENT_VARIABLES.sub(lambda matches:(
                self.__process_variable(i, *matches.groups()[0:2], shared, fragments)
            ), string)
        except Exception as exception:
            print(exception)
        return string

    def execute(self:Self, 
        i:int, 
        string:str, 
        shared:dict[str, Any|None] = {}, 
        fragments:list[str] = []
    ) -> str:
        return Utils.recursive(self.build_fragments(string, fragments), lambda string:(
            self.__execute_varibles(i, string, shared, fragments)
        ))
    
    @staticmethod
    def prepare_result(string:str, option:str, shared:dict[str, Any|None]) -> int:
        if string.lower().startswith(option.lower()):
            if "capitalized" not in shared or shared["capitalized"]:
                shared["capitalized"] = False
            return len(option)
        return -1
    
    def check(self:Self, 
        i:int, 
        string:str, 
        options:str|list[str], 
        shared:dict[str, Any|None] = {},
        fragments:list[str] = [], 
        check_full:bool = True
    ) -> tuple[bool, int]:
        
        length:int = 0
        option:str
        ok:bool = True

        string = string.lower()

        for option in Utils.get_array(options):

            clone:str = "" + string
            option_fragmented:str = self.build_fragments(option, fragments)
            i:int = 0

            length = 0
            while True:

                matches:REMatch|None = RE.FORMAT_PARTS.search(option_fragmented)

                if not matches:
                    break
            
                part:str = matches.group(0)
                m:int = -1
                key:str = matches.group(1)
                method:str
                data:list[Any|None]

                if key:
                    if Check.is_key(key):
                        method, *data = self.op.questions.get_value(key, i)
                        if method in self.modes:
                            m = self.modes[method].check(i, clone, data, shared, fragments, clone, check_full)
                    
                        # JS FormatModule lines 174-177.

                    else:

                        submatches:REMatch|None = RE.FORMAT_SPLIT.match(key)

                        if submatches:

                            method, *data = submatches.groups()[1:3]

                            if method in self.modes:
                                m = self.modes[method].check(i, clone, data, shared, fragments, clone, check_full)

                if m == -1:
                    m = self.prepare_result(clone, part, shared)

                if m != -1:
                    clone = clone[m:]
                    length += m
                    continue
                ok = False
                break

            if ok:
                ok = not check_full or not len(clone)
                if ok:
                    break
        
        return (ok, length)
    
    def get_check_length(self:Self, 
        i:int, 
        string:str, 
        options:str|list[str], 
        shared:dict[str, Any|None] = {}, 
        fragments:list[str] = [], 
        check_full:bool = True
    ) -> int:

        has:bool
        length:int

        has, length = self.check(i, string, options, shared, fragments, check_full)

        return length if has else -1
    
    def check_select(self:Self, 
        i:int, 
        string:str, 
        options:tuple[tuple[int, int], str, list[str]], 
        shared:dict[str, Any|None] = {}, 
        fragments:list[str] = []
    ) -> str|None:

        length:int = 0
        k:int = 0
        check_last:bool = False
        minimum:int
        maximum:int
        separator:str
        items:list[str]
        separator_length:int

        (minimum, maximum), separator, items = options
        separator_length = len(separator)
        
        while k < maximum:

            ok:bool = False
            l:int = len(items)
            j:int
            item:str

            if check_last or (k and k == l - 1):
                if not string.startswith(" " + separator + " "):
                    return -1
                
                length += separator_length + 2
                string = string[0, separator_length + 2]

            for j, item in enumerate(items):

                has_variables:bool = RE.FRAGMENT_VARIABLES.match(
                    self.set_fragments_level(item, shared, fragments)
                ) is not None
                item_length:int = (
                    self.get_check_length(i, string, [item], shared, fragments, False) if has_variables else 
                self.prepare_result(string, item, shared))
                
                if item_length != -1:
                    string = string[:item_length]
                    items = items[:j] + items[j + 1:]
                    length += item_length
                    ok = True
                    break

            if ok:
                if "capitalized" not in shared or shared["capitalized"]:
                    shared["capitalized"] = False
            elif check_last or not k:
                return -1
            else:
                check_last = True
                k -= 1

        return -1 if k < minimum else length
    
    def __get_list_items(self:Self, i:int, item:str) -> list[str]:
        matches:REMatch|None = RE.LIST_ITEM.match(item)

        if matches:

            key:str = matches.group(1)
            method:str
            list_items:list[str]

            method, *list_items = self.op.questions.get_value(key, i)

            if method == "list":
                return list_items
        return [item]
    
    def get_list(self:Self, i:int, items:list[str]) -> list[str]:
        return [subitem for item in items for subitem in self.__get_list_items(i, item)]
    
    @staticmethod
    def check_range(string:str, inputs:str|list[str]) -> int:

        number_matches:REMatch|None = RE.ABSOLUTE_INTEGER.match(string)

        if number_matches:

            number:int = int(string)

            for range in (
                [
                    int(value) for value in str(Utils.get_random(inputs.split("|"))).split("-")
                ] if Check.is_string(inputs) else 
                inputs if Check.is_array(inputs) else
            []):
                if Check.is_number(range):
                    if number == range:
                        return len(string)
                elif len(range) == 1:
                    if number == range[0]:
                        return len(string)
                else:
                    if number >= range[0] and number <= range[1]:
                        return len(string)
        return -1