import torch
import logging
from pyparsing import *
from collections.abc import Iterable
import json
from typing import List, Any, Tuple, Union
import re
import numpy as np

# Configure logging level
logging.basicConfig(level=logging.DEBUG)  # Change to logging.INFO to suppress debug messages


escaped_char = Suppress("\\") + oneOf("{}:`")
LBRACE, RBRACE, COLON, BACKTICK = map(Suppress, '{}:`')
value = CharsNotIn('{}:`')
value_w_colon = CharsNotIn('{}`')
value_backticked = CharsNotIn('`')
key = CharsNotIn('{}', min=1)  # Ensure key has at least one character

# Redefining unnamed string patterns with improved backtick handling
unnamed_string1 = (LBRACE + value("value") + (Optional(COLON) ^ StringEnd()) + RBRACE)
unnamed_string2 = (LBRACE + BACKTICK + value("value") + BACKTICK + COLON + RBRACE)
unnamed_string3 = (LBRACE + BACKTICK + value("value") + BACKTICK + RBRACE)
unnamed_string4 = (LBRACE + BACKTICK + value("value") + BACKTICK + COLON + RBRACE)
empty_unnamed_string1 = (LBRACE + RBRACE).setParseAction(lambda t: '')
empty_unnamed_string2 = (BACKTICK + BACKTICK).setParseAction(lambda t: '')
unnamed_string5 = value_w_colon("value")
unnamed_string6 = BACKTICK + value_backticked("value") + BACKTICK

# Redefining the named string patterns with improved backtick handling
# Patterns for named strings

named_string1 = Group(value("value") + LBRACE + COLON + key("key") + RBRACE)
named_string2 = Group(BACKTICK + value("value") + BACKTICK + LBRACE + COLON + key("key") + RBRACE)
named_string3 = Group(LBRACE + value("value") + COLON + key("key") + RBRACE)
named_string4 = Group(LBRACE + BACKTICK + value("value") + BACKTICK + COLON + key("key") + RBRACE)
named_string5 = Group(value("value") + LBRACE + BACKTICK + BACKTICK + COLON + RBRACE)
empty_named_string = (LBRACE + COLON + RBRACE).setParseAction(lambda t: ('',''))

# Patterns for empty strings and backticked content
backticked_empty_key = Group(LBRACE + BACKTICK + BACKTICK + COLON + key("key") + RBRACE)

# Grouping the unnamed string patterns with the new patterns
unnamed_string = (unnamed_string1
                  | unnamed_string2
                  | unnamed_string3
                  | unnamed_string4
                  | empty_unnamed_string1
                  | unnamed_string5
                  | unnamed_string6
                  | empty_unnamed_string2)

# Grouping the named string patterns with the new pattern for backticked empty key
named_string = (empty_named_string
                | named_string1
                | named_string2
                | named_string3
                | named_string4
                | backticked_empty_key
                | named_string5)

# Constructing the final parser pattern with preference for unnamed strings
TextParser = ZeroOrMore(named_string | unnamed_string) + StringEnd()



class Text(str):
    parser = TextParser
    allowed_keys = None

    @classmethod
    def parse(cls, arg):
        parsed_result = cls.parser.parseString(arg)
        arg = [(res.key if "key" in res else "", res.value if "value" in res else "") if isinstance(res,
                                                                                                    ParseResults) else res
               for res in parsed_result]
        return arg


    def __new__(cls, *substrings, parse: Union[str, bool] = "auto", **named_substrings):
        """
        Construct a new Text instance. Allows for various input formats.

        Args:
            *substrings (Union[str,Tuple[str, str], List[str]): Flexible input data. Can be a parsable string, string sequences, key-value pairs, dicts...
                                    If None is passed, it will be replaced with a Text instance with empty content.
            parse (Union[bool, str], optional): Enable or disable the automatic parsing of string content.
                                    The default behavior is to automatically detect language and parse it only
                                    if the texts is written in the default langtorch syntax. Set to True to
                                    also enable parsing of the detected languages using pandoc.
            **named_substrings (str): Additional named textual data entries.

        Returns:
            (Text): A structured textual instance.

        Raises:
            ParseException: If automatic parsing fails. Consider disabling parsing if this occurs.
            ValueError: When an unsupported input format is provided e.g. a TextTensor is passed.
        """
        if len(substrings) == 0 or (len(substrings) == 1 and substrings[0] is None):
            instance = super().__new__(cls, "")
            instance._content = tuple()
            return instance
        content = [c if c is not None else cls.identity for c in (list(substrings) + list(named_substrings.items()))]
        # cast TextTensors to strings
        for i in range(len(content)):
            if isinstance(content[i], torch.Tensor) and hasattr(content[i], "content"):
                content[i] = content[i].sum().item()
                assert isinstance(content[i], str) or (0 < len(content[i]) <= 2)
        # returns the final content tuple
        if len(content) == 1 and cls._is_valid_tree(content, is_tuple=True):
            content = content[0]
        else:
            content = cls.to_ast(*content, parse = parse, is_tuple=True)

        assert cls._is_valid_tree(content, is_tuple=True), f"Invalid tree: {content}"

        instance = super().__new__(cls, cls._concatenate_terminals(content))
        instance._content = content
        return instance

    @classmethod
    def to_ast(cls, *args, parse = True, is_tuple = False):
        """Reformats a wide array of construtor patterns into a unified AST-like format"""
        if len(args) == 1 and isinstance(args[0], list):
            args = args[0]
        if len(args) == 1 and ((isinstance(args[0], tuple) and len(args[0]) > 2) or isinstance(args[0], list)):
            # List or tuple of strings / named strings
            args = args[0]
        elif isinstance(args[0], dict):
            # Dictionary of named strings
            args = args[0].items()
        elif isinstance(args[0], Text) and len(args) == 1:
            # Passing cls instance to itself
            args = args[0].content
        elif all([isinstance(arg, str) for arg in args]):
            if parse:
                try:
                    result = []
                    for arg in args:
                        arg = cls.parse(arg)
                        result += arg
                    args = result
                except ParseException as E:
                    print(f"Last parsed string: {arg}")
                    raise ParseException(str(E) + "\nYou may want to disable string parsing, with parse = False")
            else:
                pass
        if any([isinstance(arg, torch.Tensor) for arg in args]):
            raise ValueError("You cannot initialise Text from a TextTensor. Use tensors.item() or otherwise transform the tensors to a string, list or dictionary.")
        def simplify(arg, parse = False):
            if isinstance(arg, tuple) and len(arg) == 2 and not (isinstance(arg[0], str) and isinstance(arg[1], str)):
                # Fix tuple types
                return (str(arg[0]), simplify(arg[1]))
            elif isinstance(arg, tuple) and len(arg) == 2 and isinstance(arg[0], str) and isinstance(arg[1], str):
                # CORRECT
                return arg
            elif isinstance(arg, tuple) and len(arg) == 1:
                # CORRECT though should be avoided
                return arg[0]
            elif isinstance(arg, tuple) and len(arg) > 2:
                # Assume a tuple of length != 2 was supposed to be a list
                logging.debug(f"Tuples of length 2 represent (key, value) in Text objects. When parsing a Text entry was a tuple of length {len(arg)},\nit was converted to a list and may lead to errors.")
                arg = list(arg)

            # Not a named string
            if isinstance(arg, list) and len(arg) == 1:
                return arg[0]
            elif isinstance(arg, (list, np.ndarray, torch.Tensor)):
                return [simplify(element, parse=parse) for element in arg]
            elif isinstance(arg, Text):
                if len(arg.items()) == 1:
                    return Text.to_ast(arg.items()[0], parse=parse)
                else:
                    return Text.to_ast(arg.items(), parse=parse)
            elif hasattr(arg, 'items'):
                return Text.to_ast(list(arg.items()), parse=parse)
            elif isinstance(arg, str):
                # CORRECT
                return arg
            else: # Cast to string
                print(arg)
                return str(arg)
            # Maybe consider: raise ParseException(f"Could not parse {arg} of type {type(arg).__name__}")

        content = [simplify(arg, parse=parse) for arg in args]

        if not is_tuple: # Recursive case: In these cases we are returning a node or tree with a single root node
            return content[0] if isinstance(content, list) and len(content) == 1  else content
        else: # Base case: In these cases we are returning a tuple of nodes for the ._content attribute
            def check_for_lists(tree): # A basic check for lists in the tree
                if isinstance(tree, list):
                    # Prune single-element lists
                    if len(tree) == 1:
                        raise ValueError(f"single-element list {tree}")
                    else:
                        return [check_for_lists(element) for element in tree]
                elif isinstance(tree, tuple) and len(tree) == 2:
                    key, value = tree
                    return key, check_for_lists(value)
                return tree

            content = tuple(check_for_lists(arg) for arg in content)

            return content

    @classmethod
    def is_terminal_node(cls, entry):
        if isinstance(entry, str) and not isinstance(entry, Text):
            return True
        if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[0], str) and isinstance(entry[1], str):
            return True
        return False

    @classmethod
    def _is_valid_tree(cls, entry, is_tuple = False):
        """
        Checks if an entry is a valid tree for a Text instance.

        """
        if isinstance(entry, cls):
            entry = entry.items()
        elif isinstance(entry, tuple) and is_tuple:
            if len(entry) == 0:
                return True
            entry = list(entry) if len(entry)>1 else entry[0]
        # Base cases
        if cls.is_terminal_node(entry):
            return True

        # Recursive cases
        if isinstance(entry, tuple) and len(entry) != 2:
            return False
        if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[0], str) and (
                isinstance(entry[1], list) or isinstance(entry[1], tuple)):
            return all(cls._is_valid_tree(child) for child in entry[1])
        if isinstance(entry, list) and len(entry)>1:
            return all(cls._is_valid_tree(child) for child in entry)
        if isinstance(entry, list) and len(entry)<=1:
            return False  # Single-element lists are not valid

        # If none of the above cases match, it's not valid
        return False

    @classmethod
    def str_formatter(cls, instance) -> str:
        """
        Formats the human-readable string of a Text instance. Subclasses of Text can reimplement this method!

        Args:
            instance (Text): An instance of the Text class.

        Returns:
            (str): A string representation of the instance.
        """
        return cls._concatenate_terminals(instance.items())

    @classmethod
    def _concatenate_terminals(cls, entry) -> str:
        result = ""

        # Base cases
        if cls.is_terminal_node(entry):
            return entry if isinstance(entry, str) else entry[1]

        # Recursive cases
        if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[0], str) and (
                isinstance(entry[1], list) or isinstance(entry[1], tuple)):
            return ''.join(cls._concatenate_terminals(child) for child in entry[1])
        if isinstance(entry, list):
            return ''.join(cls._concatenate_terminals(child) for child in entry)

        return result

    def __str__(self):
        return self.__class__.str_formatter(self)

    @classmethod
    def from_messages(cls, *messages):
        """Text from a list of dicts with keys 'role' and 'content'"""
        if isinstance(messages[0], list):
            messages = messages[0]
        text = []
        for m in messages:
            if isinstance(m, list):
                for mm in m:
                    text.append(mm["role"],m["content"])
            else:
                text.append((m["role"],m["content"]))
        return cls(*text)

    @classmethod
    def from_api_response(cls, *response_dicts):
        """Text from a list of dicts with keys 'role' and 'content'"""
        if isinstance(messages[0], list):
            messages = messages[0]
        text = []
        for m in messages:
            if isinstance(m, list):
                for mm in m:
                    text.append(mm["role"],m["content"])
            else:
                text.append((m["role"],m["content"]))
        return cls(*text)


    def keyed_print(self):
        """
        Prints the Text instance with keys aligned over their corresponding values.
        """
        # Extract the key-value pairs from the _content attribute
        key_value_pairs = [pair  if isinstance(pair, tuple) else ('', pair) for pair in self.items()]

        # Find the longest key and value for formatting purposes
        longest_key = max(len(key) for key, _ in key_value_pairs)
        longest_value = max(len(value) for _, value in key_value_pairs)

        # Create the top and bottom lines for the keys and values
        top_line = ""
        bottom_line = ""
        for key, value in key_value_pairs:
            # Calculate padding to center the key
            total_padding = longest_key - len(key)
            left_padding = total_padding // 2
            right_padding = total_padding - left_padding

            # Add the formatted key and value to the respective lines
            top_line += f"{' ' * left_padding}{key}{' ' * right_padding}|"
            bottom_line += f"{value.ljust(longest_value)}|"

        # Remove the trailing '|' character
        top_line = top_line.rstrip('|')
        bottom_line = bottom_line.rstrip('|')

        # Print the formatted lines
        print(top_line)
        print(bottom_line)

    def __repr__(self):
        return self.__str__()

    def to_tensor(self):
        from langtorch import TextTensor
        return TextTensor(self.content)

    @property
    def identity(self):
        return self.__class__()

    @property
    def content(self):
        return [self.__class__(m, parse = False) for m in self._content]

    @content.setter
    def content(self, content):
        if isinstance(content, tuple) and len(content) == 2 and (isinstance(content[0], str) and isinstance(content[1], str)):
            AttributeError(
            f"When setting the .content attribute, passing a tuple of strings is ambiguous. Pass either a list with a (key, value) tuple or a list with two strings."
            )

        if isinstance(content, list):
            content = tuple(content)
        assert isinstance(content, tuple)
        self._content = self.to_ast(content, parse=False, is_tuple=True)

    def items(self):
        """
        Retrieves key-value pairs from the Text object, allowing for structured data extraction
        and further processing.

        Returns:
            (List[Tuple[str, Union[str, Tuple[...]]]]): A list of key-value pairs representing the Text's content.
        """
        return [(arg[0], arg[1]) if isinstance(arg, tuple) else ('', arg) for arg in self._content]
        # return [(arg[0], str(self.__class__(arg[1])))) if isinstance(arg, tuple) else ('', str(self.__class__(arg))) for arg in self._content]

    def keys(self):
        return [s[0] for s in self.items()]

    def values(self):
        return [s[1] for s in self.items()]

    def set_key(self, key, inplace = False):
        """
        Override keys for the textual entries, used for restructuring the content.
        Useful for substituting the key right before passing TextTensor to a Module.

        Args:
            key (Union[Text, str, List[str]]): The new key or keys to apply
            inplace bool: .

        Returns:
            (Text): A new Text instance with updated keys.
        """
        #The use of Text.str_formatter(t) instead of str(t) here and elsewhere is for subclasses of Text to reimplement __str__
        if isinstance(key, Text):
            key = key.values()
            if len(key) == 1:
                key = key[0]

        if isinstance(key, list):
            assert len(key) == len(self.values()), f"Number of keys ({len(key)}) must match number of values ({len(self.values())})"
            content = tuple((k,v) for k,v in zip(key, self.values()))
        elif isinstance(key, str):
            content = ((key, Text.str_formatter(self)),)
        if inplace:
            self.content = content
            return self
        else:
            return self.__class__(*content)

    def set_key_(self, keys):
        self.set_key(keys, inplace=True)

    def add_key(self, key, inplace=False):
        """
        Add a top-level  key, placing the items of the original as a value under the new key.
        Useful for working with nested keys like in Chat prompts.

        Args:
            key (Union[Text, str, List[str]]): The new key to add
            inplace bool: .

        Returns:
            (Text): A new Text instance with updated keys.
        """
        #The use of Text.str_formatter(t) instead of str(t) here and elsewhere is for subclasses of Text to reimplement __str__
        if isinstance(key, Text):
            key = key.values()
            if len(key) == 1:
                key = key[0]

        if isinstance(key, list) and len(key)>1:
            assert len(key) == len(self.content), f"Number of keys ({len(key)}) must match number of entries ({len(self.content)})"
            content = tuple((k,v) for k,v in zip(key, self.content))
        elif isinstance(key, str):
            content = ((key, self.to_ast(self.items())),)

        if inplace:
            self.content = content
            return self
        else:
            return self.__class__(*content)

    def add_key_(self, keys):
        return self.add_key(keys, inplace=True)

    @property
    def iloc(self):
        """
        Order-based indexing. e.g. .loc[2:5]
        """
        class IlocIndexer:
            def __init__(self, text_items, text_class):
                self.text_items = text_items
                self.text_class = text_class

            def __getitem__(self, index):
                # Handle single integer index
                if isinstance(index, int):
                    try:
                        return self.text_class(self.text_items[index], parse=False)
                    except IndexError:
                        raise IndexError("Index out of range")

                # Handle slices
                elif isinstance(index, slice):
                    return self.text_class(self.text_items[index], parse=False)

                # Handle a list of indices
                elif isinstance(index, list):
                    # Check if all elements in the list are integers
                    if not all(isinstance(idx, int) for idx in index):
                        raise IndexError("All indices must be integers")
                    try:
                        return self.text_class([self.text_items[idx] for idx in index], parse=False)
                    except IndexError:
                        raise IndexError("List index out of range")

                else:
                    raise TypeError("Invalid index type")

            # TODO implement __setitem__ for iloc

        # Assuming self._content stores the structured content in a list or tuple format
        if isinstance(self._content, (list, tuple)):
            items = self._content
        else:
            raise TypeError("Content type unsupported for iloc indexing.")

        return IlocIndexer(items, self.__class__)


    @property
    def loc(self):
        """
        Key-based indexing. You can access entries with nested keys using dot notation, e.g. .loc["key1.key2"]
        """
        class LocIndexer:
            def __init__(self, text_instance):
                self.text_instance = text_instance
                self.text_class = text_instance.__class__

            def __getitem__(self, key):
                # Function to recursively get sub-items

                def get_subitems(items, keys):
                    if keys and isinstance(items, list):
                        next_key = keys.pop(0)
                        sub_items = [v for k, v in items if k == next_key]
                        if not keys:  # No more subkeys, return what we have found
                            return sub_items
                        else:  # We have more keys to search for
                            nested_items = []
                            for _, v in sub_items:
                                # Assuming v is a list of tuples as per Text structure
                                nested_items.extend(get_subitems(v, keys.copy()))
                            return nested_items
                    if isinstance(items, list):
                        return items
                    else:
                        return [items]

                if isinstance(key, str):
                    # Handle key with dot notation for nested access
                    if '.' in key:
                        keys = key.split('.')
                        return self.text_class(get_subitems(self.text_instance.items(), keys), parse=False)
                    # Single key, simple lookup
                    return self.text_class([v for k, v in self.text_instance.items()  if k == key], parse=False)

                # If the key is a list, return a subset with keys preserved.
                elif isinstance(key, list):
                    return self.text_class([(k, v) for k, v in self.text_instance.items() if k in key], parse=False)

                else:
                    raise KeyError(f"Invalid key type: {type(key)}")

            def __setitem__(self, key, value):
                def set_items_in_tuple(tup, keys, value, replace=True):
                    value = self.text_instance.to_ast(value) # format value

                    if isinstance(keys, str):
                        keys = [keys]
                        replace = False

                    # Reconstruct the tuple with replacements
                    rebuilt = []
                    for item in tup:
                        k, v = item if isinstance(item, tuple) else ('', item)
                        if k in keys:
                            # Replace the entire key-value pair
                            new_item = value if replace else (k, value)
                            rebuilt.append(new_item)
                        else:
                            if replace:
                                raise KeyError(f"Key {keys} not found in texts items {tup}. If you wanted to use .loc to append with key, use a string key not a list.")
                            else:
                                rebuilt.append(self.text_instance.to_ast(item))
                    return tuple(rebuilt)

                self.text_instance._content = set_items_in_tuple(self.text_instance._content, key, value)



        return LocIndexer(self)

    def split(self, sep=" ", mode = "auto"):
        modes = ["auto","words", "sentances","paragraphs"]
        assert mode in modes, f"Mode must be one of {modes}"
        from langtorch import TextTensor
        return TextTensor(str(self).split() if sep == "" else str(self).split(sep), parse=False)


    def __getitem__(self, index):
        if isinstance(index, str):
            return self.loc[index]
        return self.iloc[index]

    def __or__(self, other):
        return

    def __iter__(self):
        for s in self.content:
            yield s

    def __add__(self, other):
        if isinstance(other, str) and not isinstance(other, Text):
            return self.__class__(*self.content, other, parse = False)
        elif isinstance(other, Text):
            return self.__class__(*self.content, *other.content, parse = False)
        else:
            raise TypeError(f'Cannot add {type(other)}')

    def __mul__(self, other, strict = False):
        if isinstance(other, str) and not isinstance(other, Text):
            try:
                other = Text(other)
            except ParseException:
                other = Text(other, parse = False)
        content = self.items()
        result = content
        formatted_indices = []
        indices_to_delete = []
        positional_j = 0
        for i, (k, v) in enumerate(content):
            if v == "*":
                logging.debug(f"Wildcard {(k, v)} ::filled with:: {other.items()}")
                if k == "":
                    result = result[:i] + list(other.items()) + result[i+1:]
                else:
                    result[i] = (k,other.items())
                return self.__class__(*result, parse = False) # TODO the case of the other having a re-key pattern
        for j, (k_, v_) in enumerate(other.items()):
            # Wildcard case -> Adding a key
            if v_ == "*":
                for i in indices_to_delete:
                    result.pop(i)
                result = [(k_,result)]
                indices_to_delete, formatted_indices = [],[]
            elif k_ == "" and j not in formatted_indices: # Positional arguments
                for i, (k, v) in enumerate(content):
                    if v == str(positional_j):
                        logging.debug(f"Place at numbered spot: {(k,v_)} ::at:: {i}")
                        result[i] = (k,v_)
                        positional_j += 1
                        formatted_indices.append(i)
                        break
                else:
                    for i, (k, v) in enumerate(content):
                        if v == "":
                            logging.debug(f"Place: {(k,v_)} ::at:: {i}")
                            result[i] = (k,v_)
                            formatted_indices.append(i)
                            if v_ == k:
                                indices_to_delete.append(i)
                            break
                    else:
                        logging.debug(f"Append {(k_,v_)} ::to:: {content}")
                        result += [(k_,v_)]
            else:
                for i, (k, v) in enumerate(content):
                    if i not in formatted_indices:
                        if (k,v) == (v_,k_):
                            logging.debug(f"Identity: {(k_,v_)} ::with:: {content[i]}")
                            formatted_indices.append(i)
                            indices_to_delete.append(i)
                            break
                        elif v == k_:
                            logging.debug(f"Replace content: {(k_,v_)} ::with:: {content[i]}")
                            result[i] = (k,v_)
                            formatted_indices.append(i)
                            break
                        elif k == v_:
                            logging.debug(f"Replace key: {(k_,v_)} ::with:: {content[i]}")
                            result[i] = (k_,v)
                            formatted_indices.append(i)
                            break
                        elif k == k_:
                            logging.debug(f"Concatenate: {(k_,v_)} ::with:: {content[i]}")
                            result[i] = (k,v+v_)
                            break
                else:
                    logging.debug(f"Append {(k_, v_)} ::to:: {content}")
                    # Append the ones that didn't have a {key}
                    # If you don't want this consider using | operation
                    result += [(k_, v_)]
        for i in indices_to_delete:
            result.pop(i)
        return self.__class__(*result, parse = False)


    def format(self, *args, **kwargs):
        other = Text(*args, parse = False) + Text(kwargs, parse = False)
        return self.__mul__(other, strict = True)

    def inv(self):
        return self.__class__(*[(v,k) for k,v in self.items()], parse = False)

    def __pow__(self, power):
        if power == -1:
            return self.inv()
        else:
            raise ValueError("Can only use power -1")

    def method_apply(self, method: str, *args, to = "values", **kwargs):
        assert to in ["values","keys","both"]
        if to == "values":
            return self.__class__(*list((k, getattr(v, method)(*args, **kwargs)) for k,v in self.items()), parse = False)
        elif to == "keys":
            return self.__class__(*list((getattr(k, method)(*args, **kwargs), v) for k,v in self.items()), parse = False)
        elif to == "both":
            return self.__class__(*list((getattr(k, method)(*args, **kwargs), getattr(v, method)(*args, **kwargs)) for k,v in self.items()), parse = False)

    def inspect(self):
        return "|".join(f"{v} "+"{"+k+"}, " for k,v in self.items())

    def upper(self):
        return self.method_apply("upper")

    def lower(self):
        return self.method_apply("lower")

    @classmethod
    def from_pandoc_json(cls, ast_json: str) -> 'Text':
        """
        Creates a Text object from a Pandoc AST JSON string.

        Args:
            ast_json (str): A string containing a Pandoc AST in JSON format.

        Returns:
            (Text): An instance of Text representing the parsed content.
        """
        ast = json.loads(ast_json)
        content = cls.parse_elements(ast['blocks'])
        return cls(content)

    @classmethod
    def _parse_elements(cls, elements: List[Any]) -> List[Tuple[str, Any]]:
        """
        Recursively parses Pandoc AST elements into a list of tuples.

        Args:
            elements (List[Any]): A list of Pandoc AST elements, which could be blocks or inlines.

        Returns:
            (List[Tuple[str, Any]]): A list of tuples representing the content structure.
        """
        result = []
        for element in elements:
            type_ = element['t']
            if type_ == 'Header':
                level, _, inlines = element['c']
                text_content = ' '.join(cls.parse_elements(inlines))
                result.append((f'header_h{level}', text_content))
            elif type_ == 'Para':
                text_content = ' '.join(cls.parse_elements(element['c']))
                result.append(('p', text_content))
            elif type_ == 'Str':
                result.append(element['c'])
            elif type_ == 'Space':
                result.append(' ')
            # Add more cases here for other types of blocks and inlines
        return result

        content = parse_blocks(ast['blocks'])
        return Text(content)

    @classmethod
    def dict_of_dicts_to_abstract_syntax_tree(cls, dicts):
        import ast
        return ast.literal_eval(str(dicts).replace(":",": ").replace("{","{ ").replace("}"," }"))

    @classmethod
    def guess_format(cls, text):
        # Patterns for markup languages
        patterns = {
            'html': r'<!DOCTYPE html>|<html>',
            'markdown': r'^# .+|^- |\*\*[^*]+\*\*|__[^\_]+__',
            'latex': r'\\documentclass',
            # Add more patterns for other markup languages
        }

        # Check for markup language patterns
        for format_name, pattern in patterns.items():
            if re.search(pattern, text, re.MULTILINE):
                return format_name

        # Check for custom language
        if Text.detect_custom_language(text):
            return 'custom_language'

        # Default to plain texts if no patterns match
        return 'plain'

    @classmethod
    def detect_custom_language(cls, text):
        named_string_pattern = r'(\w+\{\:\w+\})|(`\w+`\{\:\w+\})|(\{\w+\:\w+\})|(\{\`\w+\`\:\w+\})|(\w+\{\`\`\:\})|(\{\:\w+\})'
        unnamed_string_pattern = r'(\{\w+\[\:\]\})|(\{\`\w+\`\:\})|(\{\`\w+\`\})|(\{\`\w+\`\:\})|\{\}|\`\`'
        full_pattern = fr'({named_string_pattern}|{unnamed_string_pattern})'
        # Match the pattern exactly; ^ and $ are the start and end of the string anchors respectively
        return bool(re.fullmatch(full_pattern, text))

    @classmethod
    def guess_language(cls, text):
        return Text.guess_format(text)


class String(Text):
    allowed_keys = [""]
    @classmethod
    def str_formatter(cls, instance):
        return instance.values()[0] if len(instance.values())>0 else ""

    @classmethod
    def to_ast(*args, parse = False, is_tuple = False):
        return (("","".join(args)),)


