# coding: utf-8
"""
Module `chatette.parsing.lexing.lexing_rule`
Contains the class that represents a lexing rule, which is
in charge of checking if the rule it represents matces the input,
transforms it to lexical tokens (simply called tokens in the code) and
formulates the error if required. Therefore, each instance of such a class is
stateful.
"""

from abc import ABCMeta, abstractmethod
from future.utils import with_metaclass

from chatette.parsing.input_file_manager import InputFileManager


class LexingRule(with_metaclass(ABCMeta, object)):
    """
    Superclass for all lexing rules.
    Child classes each represent a concrete lexing rule (each instance keeping
    a state) and whose purpose will be to:
    - check whether the provided text matches the rule it represents
    - produce the sequence of lexical toekns if it matches
    - formulate the error and communicate it to the user if asked
    """
    _empty_match_allowed = False
    def __init__(self, text, start_index):
        self._text = text

        self._start_index = start_index
        self._next_index = start_index
        self._furthest_matched_index = start_index  # inclusive
        self._tokens = []

        self._matched = None
        self.error_msg = None


    def matches(self, **kwargs):
        """
        Applies the rule to `self._text`
        and returns `True` if the rule could be applied successfully.
        `kwargs` will be passed to the subsequent call to `self._apply_strategy`.
        @post: `self.get_lexical_tokens()` will return valid tokens
               iff this method returned `True`.
        """
        if self._matched is None:
            if self._start_index >= len(self._text):
                if self.__class__._empty_match_allowed:
                    self._matched = True
                else:
                    self._matched = False
                    self.error_msg = \
                        "Matching of rule '" + self.__class__.__name__ + "' " + \
                        "failed. Didn't expect an end of line there."
            else:
                self._matched = self._apply_strategy(**kwargs)
        return self._matched
    @abstractmethod
    def _apply_strategy(self, **kwargs):
        """
        Strategy to apply the rule to the text `self._text`.
        Implements the design pattern "Strategy method".
        Returns `True` iff the rule could be applied successfully.
        If the rule doesn't match, `self.error_msg` should be updated
        accordingly.
        `kwargs` can be used to pass arguments that are specific to
        the current strategy.
        After this method was called, the following variables should be
        up-to-date:
        - if it matched: `self._tokens`, `self._next_index`,
                         `self._furthest_matched_index`
        - if it didn't match: `self._furthest_matched_index`, `self.error_msg`
        """
        raise NotImplementedError()

    # Helpers for matching rules
    def _try_to_match_rule(self, rule_class, index=None, **kwargs):
        """
        Tries to match the rule represented by `rule_class`
        and updates `self._tokens` and `self._next_index` if the rule could be
        applied. If the rule couldn't be applied, `self.error_msg` is updated.
        `rule_class` is a subclass of `LexingRule`; `index` is an index in the
        text `self._text`.
        `kwargs` will be passed down to the subsequent `_matches`
        @returns: `True` iff the rule could be applied successfully.
        """
        if index is None:
            index = self._next_index
        rule = rule_class(self._text, index)
        if rule.matches(**kwargs):
            self._tokens.extend(rule.get_lexical_tokens())
            self._next_index = rule.get_next_index_to_match()
            self._update_furthest_matched_index(rule)
            return True
        else:
            self.error_msg = rule.error_msg
            self._update_furthest_matched_index(rule)
            return False

    def _match_one_of(self, rule_classes, index=None, **kwargs):
        """
        Finds which of the rules in `rule_classes` matches the text starting at
        index `index`.
        Try rules in order of appearance in `rule_classes`.
        `rule_classes` is a list of class of rules.
        Returns `True` iff one of the rules could apply.
        `kwargs` will be passed down to the subsequent `_matches`
        @pre: `rule_classes` contains at least one element.
        @post:
            - if a rule matched, `self._tokens` and `self._next_index`
              are updated.
            - if no rule matched, `self._matched` and `self.error_msg` are
              updated.
        @raises: `ValueError` if `rule_classes` does not contain any element.
        """
        if len(rule_classes) == 0:
            raise ValueError(
                "Lexer tried to apply no rule at all " + \
                "during application of rule '" + self.__class__.__name__ + \
                "'."
            )
        if index is None:
            index = self._next_index

        best_failed_rule = None
        longest_match_last_index = None
        for rule_class in rule_classes:
            rule = rule_class(self._text, index)
            if rule.matches(**kwargs):
                self._tokens.extend(rule.get_lexical_tokens())
                self._next_index = rule.get_next_index_to_match()
                return True
            else:
                match_last_index = rule._furthest_matched_index
                # match_last_index = rule.get_next_index_to_match() - index
                if best_failed_rule is None or match_last_index > longest_match_last_index:
                    best_failed_rule = rule
                    longest_match_last_index = match_last_index

        self._update_furthest_matched_index(best_failed_rule)

        # No rule matched
        self._matched = False
        if longest_match_last_index - index == 0:
            self.error_msg = \
                "Invalid token. Expected either of the following rules: '" + \
                "', '".join([c.__name__ for c in rule_classes]) + "'."
        else:
            self.error_msg = best_failed_rule.error_msg
        return False

    def _match_any_order(self, rule_classes, index=None):
        """
        Matches as many rules as possible from `rule_classes` and this in any
        order, but each rule can be matched only once.
        If `None` is present in `rule_classes`, it is allowed to match 0 rules.
        Returns `True` if at least one rule matched (or 0 if allowed).
        `rule_classes` is a list of class of rules.
        @pre: `rule_classes` contains at least one element.
        @post:
            - if at least one rule matched, `self._tokens` and
              `self._next_index` are updated.
            - if no rule matched and at least 1 must be matched,
              `self._matched` and `self.error_msg` are updated.
            - if no rule matched and 0 rules were allowed to be matched,
              simply returns `False`. `self._matched` and `self.error_msg` are
              NOT updated.
        @raises: `ValueError` if `rule_classes` does not contain any element.
        """
        if len(rule_classes) == 0:
            raise ValueError(
                "Lexer tried to apply no rule at all " + \
                "during application of rule '" + self.__class__.__name__ + \
                "'."
            )
        if index is None:
            index = self._next_index
        no_match_allowed = False
        matched_some_rule = False

        remaining_rules = list(rule_classes)
        last_nb_remaining_rules = len(remaining_rules)  # Checked everytime every rules have been tested
        i = 0
        best_failed_rule = None
        longest_match_last_index = None
        while True:
            if i >= len(remaining_rules):
                if (
                    len(remaining_rules) == 0
                    or len(remaining_rules) == last_nb_remaining_rules
                ):
                    break
                i = 0
                last_nb_remaining_rules = len(remaining_rules)
            if remaining_rules[i] is None:
                no_match_allowed = True
                remaining_rules.remove(None)
                i = 0
            else:
                rule = (remaining_rules[i])(self._text, index)
                if rule.matches():
                    matched_some_rule = True
                    self._tokens.extend(rule.get_lexical_tokens())
                    self._next_index = rule.get_next_index_to_match()
                    index = self._next_index
                    remaining_rules.remove(remaining_rules[i])
                else:
                    i += 1
                    match_last_index = rule._furthest_matched_index
                    # match_last_index = rule.get_next_index_to_match() - index
                    if best_failed_rule is None or match_last_index > longest_match_last_index:
                        best_failed_rule = rule
                        longest_match_last_index = match_last_index

        self._update_furthest_matched_index(best_failed_rule)

        if matched_some_rule:
            return True
        if no_match_allowed:
            return True
        # No rule matched and no match is not allowed
        self._matched = False
        if longest_match_last_index - index == 0:
            self.error_msg = \
                "Invalid token. Expected either of the following rules: '" + \
                "', '".join([c.__name__ for c in rule_classes]) + "'."
        else:
            self.error_msg = best_failed_rule.error_msg
        return False


    # Setters
    def _update_furthest_matched_index(self, rule=None):
        """
        Updates the value of `self._furthest_matched_index`
        so that it is the maximum between its current value and
        the value of `rule._furthest_matched_index`.
        If `rule` is `None`, updates `self._furthest_matched_index`
        with respect to `self._next_index` (i.e. the index that should
        be tested for a match next).
        The value of `self._furthest_matched_index` represents
        the maximum index that was matched by a rule
        called by the current rule (or by this rule itself).
        """
        if rule is None:
            self._furthest_matched_index = \
                max(self._furthest_matched_index, self._next_index - 1)
        else:
            self._furthest_matched_index = \
                max(self._furthest_matched_index, rule._furthest_matched_index)

    # Getters
    def get_lexical_tokens(self):
        """
        Returns the lexical tokens after th rule was successfully applied
        using `self.matches()`.
        If this method is called before the rule was applied,
        the rule will be applied, but a `ValueError` may be raised in case
        the rule doesn't match the text.
        @returns: a list of `LexicalToken`s
        """
        if self._matched is None:
            self.matches()
        if not self._matched:
            raise ValueError(
                "Asked for the lexical tokens of a rule that did not " + \
                "match: '" + self.__class__.__name__ + "'."
            )
        return self._tokens
    def get_next_index_to_match(self):
        """
        Returns the next index in `self._text` that corresponds to
        the rule next in line. This index may correspond to the end of the text
        if the current rule covered the whole text or to the same index as
        `self._start_index` if the rule didn't match anything at all.
        @returns: an integer index
        """
        if self._matched is None:
            self.matches()
        return self._next_index


    def print_error(self):
        """
        If the corresponding rule didn't match,
        prints the relevant error so the user is aware of what the lexing
        problem was.
        """
        if self._matched is None:
            raise ValueError(
                "Tried to print an error for rule '" + self.__class__.__name__ + \
                "' before it was applied."
            )
        if self._matched:
            raise ValueError(
                "Tried to print an error for rule '" + self.__class__.__name__ + \
                "' when no error occurred."
            )
        if self.error_msg is None:
            raise ValueError(
                "No error to print for rule '" + self.__class__.__name__ + \
                "'."
            )
        InputFileManager.get_or_create() \
            .syntax_error(self.error_msg, self._furthest_matched_index + 1)
