# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/00_EditOperations.ipynb (unless otherwise specified).

__all__ = ['InsertOperation', 'DeleteOperation', 'ReplaceOperation', 'EditOperation', 'CompoundOperation',
           'EditOperation']

# Cell
#hide
from typing import List, Union, Tuple, Type

import re

# Cell
class InsertOperation:
    """
    Creates an `InsertOperation` which defines the insertion of a token into an `AbstractMethod`. Applying the
    operation will insert token `newToken` at index `tokenIndex`.
    """

    def __init__(self, tokenIndex: int, newToken: str) -> None:
        self.__tokenIndex = tokenIndex
        self.__newToken = newToken

    def __eq__(self, other: "InsertOperation") -> bool:
        return (
            type(other) is InsertOperation and
            self.__tokenIndex == other.__tokenIndex and
            self.__newToken == other.__newToken
        )

    def __str__(self) -> str:
        return "INSERT {} -> '{}'".format(self.__tokenIndex, self.__newToken)

    def __repr__(self) -> str:
        return str(self)

    def getIndex(self) -> int:
        """
        Returns the index of the insertion.
        """
        return self.__tokenIndex

    def getToken(self) -> str:
        """
        Returns the token to be inserted.
        """
        return self.__newToken

    def applyToTokens(self, tokens: List[str]) -> None:
        """
        Applies the insertion to the given list of tokens. Raises an error if the token index is out of bounds.
        """

        if self.__tokenIndex > len(tokens):
            raise IndexError("InsertOperation: cannot insert at index {} (out of bounds)".format(self.__tokenIndex))

        tokens.insert(self.__tokenIndex, self.__newToken)

# Cell
class DeleteOperation:
    """
    Creates a `DeleteOperation` which defines the deletion of a token from an `AbstractMethod`. Applying the
    operation will delete the token at index `tokenIndex`.
    """

    def __init__(self, tokenIndex: int) -> None:
        self.__tokenIndex = tokenIndex

    def __eq__(self, other: "DeleteOperation") -> bool:
        return (
            type(other) is DeleteOperation and
            self.__tokenIndex == other.__tokenIndex
        )

    def __str__(self) -> str:
        return "DELETE {}".format(self.__tokenIndex)

    def __repr__(self) -> str:
        return str(self)

    def getIndex(self) -> int:
        """
        Returns the index of the deletion.
        """
        return self.__tokenIndex

    def getToken(self) -> None:
        """
        Returns the token to be inserted. Since there is never an inserted token, return None.
        """
        return None

    def applyToTokens(self, tokens: List[str]) -> None:
        """
        Applies the deletion to the given list of tokens. Raises an error if the token index is out of bounds.
        """

        if self.__tokenIndex >= len(tokens):
            raise IndexError("DeleteOperation: cannot delete at index {} (out of bounds)".format(self.__tokenIndex))

        del tokens[self.__tokenIndex]

# Cell
class ReplaceOperation:
    """
    Creates a `ReplaceOperation` which defines the replacement of a token in an `AbstractMethod`. Applying the
    operation will replace the token at index `tokenIndex` with token `newToken`.
    """

    def __init__(self, tokenIndex: int, newToken: str) -> None:
        self.__tokenIndex = tokenIndex
        self.__newToken = newToken

    def __eq__(self, other: "ReplaceOperation") -> bool:
        return (
            type(other) is ReplaceOperation and
            self.__tokenIndex == other.__tokenIndex and
            self.__newToken == other.__newToken
        )

    def __str__(self) -> str:
        return "REPLACE {} -> '{}'".format(self.__tokenIndex, self.__newToken)

    def __repr__(self) -> str:
        return str(self)

    def getIndex(self) -> int:
        """
        Returns the index of the replacement.
        """
        return self.__tokenIndex

    def getToken(self) -> str:
        """
        Returns the replacing token.
        """
        return self.__newToken

    def applyToTokens(self, tokens: List[str]) -> None:
        """
        Applies the replacement to the given list of tokens. Raises an error if the token index is out of bounds.
        """

        if self.__tokenIndex >= len(tokens):
            raise IndexError("ReplaceOperation: cannot replace at index {} (out of bounds)".format(self.__tokenIndex))

        tokens[self.__tokenIndex] = self.__newToken

# Cell
#hide
# Define this here first with a forward reference to CompoundOperation
EditOperation = Union[InsertOperation, DeleteOperation, ReplaceOperation, "CompoundOperation"]

# Cell
class CompoundOperation:
    """
    Creates a `CompoundOperation`, which represents a combination of EditOperations starting the given `operation`. More
    operations can later be added. Applying the `CompoundOperation` to an `AbstractMethod` will apply all successfully
    added operations, in order.
    """

    def __init__(self, operation: EditOperation) -> None:
        """
        Creates the compound operation from the given operation.

        Members:

        `__beginIndex`: start of the range of deleted tokens
        `__endIndex`: end of the range of deleted tokens
        `__newTokens`: list of tokens to add
        `__type`: the type of the compound operation; can either be InsertOperation, DeleteOperation, or ReplaceOperation
        """

        # determine the range of deleted indices, will be nothing for an insert operation, or will span >= 1
        # token for any other type
        if type(operation) is CompoundOperation:
            self.__beginIndex, self.__endIndex = operation.getIndexRange()
        else:
            self.__beginIndex = operation.getIndex()
            self.__endIndex = self.__beginIndex if type(operation) is InsertOperation else self.__beginIndex + 1

        # determine new tokens which will be added, will be nothing for delete operation
        if type(operation) is CompoundOperation:
            self.__newTokens = operation.getTokens()
        else:
            self.__newTokens = [] if type(operation) is DeleteOperation else [operation.getToken()]

        # set the type
        self.__type = None
        self.__setType()

    def FromMachineString(string: str) -> "CompoundOperation":
        """
        Returns a `CompoundOperation` which represents the given machine string such that the following equality holds:
        `operation == CompoundOperation.FromMachineString(operation.getMachineString())`. The `CompoundOperation` is
        derived from the given machine string regardless if it is of general form or typed form.
        """

        # first, attempt to parse the input machine string
        match = re.search(r"^<(?P<type>.+?)> (?P<beginIndex>\d+) (?P<endIndex>\d+)" +
                r" <sep>(?P<newTokens> (?:[^ ]+ )*)</(?P=type)>$", string)
        if not match:
            raise ValueError("CompoundOperation: invalid machine string: '{}'".format(string))

        # get compound operation attributes from match results
        _type = {"op": None, "ins": InsertOperation, "del": DeleteOperation, "rep": ReplaceOperation}[match.group("type")]
        beginIndex = int(match.group("beginIndex"))
        endIndex = int(match.group("endIndex"))
        newTokens = match.group("newTokens").strip().split(" ")
        if newTokens == [""]:
            newTokens = []

        # make sure endIndex is at least beginIndex
        if not endIndex >= beginIndex:
            raise ValueError("CompoundOperation: invalid machine string: '{}'".format(string))

        # build the compound operation, start with an "empty" operation with no deletions or insertions
        operation = CompoundOperation(InsertOperation(beginIndex, "<placeholder>"))
        operation.addLoose(DeleteOperation(beginIndex))
        operation.__beginIndex = beginIndex
        operation.__endIndex = endIndex
        operation.__newTokens = newTokens
        operation.__setType()

        # make sure that if the machine string was of typed form, that the resulting CompoundOperation has the same type
        if _type is not None and _type != operation.getType():
            raise ValueError("CompoundOperation: invalid machine string: '{}'".format(string))

        return operation

    def getMachineString(self, form: str = "general") -> str:
        """
        Returns a string formatted for use in training a machine learning model, i.e. a `HephaestusModel`. The structure is
        as follows:

        `<X> beginIndex endIndex <sep> tokens </X>`

        The value of `X` depends on the given `form` parameter. The ouputted machine string can be of *general form* or
        *typed form*:
        - `"general"`: `X` will always be `"op"`, regardless of the CompoundOperation's type. Thus, the type of the operation
          is *generalized*. This is the default behavior.
        - `"typed"`: `X` will be one of `"ins"`, `"del"`, or `"rep"`, depending on the type of the `CompoundOperation`.

        The range `beginIndex:endIndex` refers to the pythonic range of tokens which the `CompoundOperation` deletes.
        Thus, if `beginIndex` and `endIndex` are equal, then no tokens are deleted. `tokens` refers to the list of tokens
        which are added at `beginIndex` once the aformentioned range is deleted.

        Note: this method is different from the `__str__()` method, which returns a more human-readable string.
        """

        # determine tag value based on form
        tag = ""
        if form == "general":
            tag = "op"
        elif form == "typed":
            tag = {InsertOperation: "ins", DeleteOperation: "del", ReplaceOperation: "rep"}[self.__type]
        else:
            raise ValueError("CompoundOperation: invalid form: {}".format(repr(form)))

        return "<{}> {} {} <sep>{}</{}>".format(
            tag,
            self.__beginIndex,
            self.__endIndex,
            " " if len(self.__newTokens) == 0 else " " + " ".join(self.__newTokens) + " ",
            tag
        )

    def __eq__(self, other: "CompoundOperation") -> bool:
        # don't have to check __type attribute, because it is a function of the index range and tokens
        return (
            type(other) is CompoundOperation and
            self.getIndexRange() == other.getIndexRange() and
            self.__newTokens == other.__newTokens
        )

    def __str__(self) -> str:

        s =  "COMPOUND_{} ".format({
            InsertOperation: "INSERT",
            DeleteOperation: "DELETE",
            ReplaceOperation: "REPLACE"
        }[self.__type])

        if self.__type is InsertOperation:
            s += "{}".format(self.__beginIndex)
        else:
            s += "{}:{}".format(self.__beginIndex, self.__endIndex)

        if self.__type is not DeleteOperation:
            s += " -> {}".format(self.__newTokens)

        return s

    def __repr__(self) -> str:
        return str(self)

    def __len__(self) -> int:
        """
        Returns the Levenshtein distance from A to B, where A is some `AbstractMethod` and B is the `AbstractMethod`
        resulting from applying this compound operation to A.
        """
        return max(self.__endIndex - self.__beginIndex, len(self.__newTokens))

    def getIndexRange(self) -> Tuple[int, int]:
        """
        Returns the range of indices whose tokens are deleted as a tuple in the form [start, end).
        """
        return (self.__beginIndex, self.__endIndex)

    def getTokens(self) -> List[str]:
        """
        Returns the list of tokens to be added.
        """
        return self.__newTokens.copy()

    def getType(self) -> Type[EditOperation]:
        """
        Returns the type of the `CompoundOperation`, which can be one of three values:

        - `InsertOperation`: Applying this operation will only result in token insertions
        - `DeleteOperation`: Applying this operation will only result in token deletions
        - `ReplaceOperation`: Applying this operation will result in token deletions and insertions
        """
        return self.__type

    def addLoose(self, operation: EditOperation) -> bool:
        """
        Attempts to add the given `operation` such that it is loosely compatible with the overall `CompoundOperation`.
        This may change the type of the `CompoundOperation`. If the addition was successful, then returns True; else,
        returns False.
        """

        # turn the operation to be inserted into a compound operation, then get attibutes
        op = CompoundOperation(operation)
        opBeginIndex, opEndIndex = op.getIndexRange()
        opTokens = op.getTokens()

        # this is one past the index of the token last affected once this operation is applied
        endAffectedIndex = self.__beginIndex + len(self.__newTokens)

        # if the ranges of affected tokens do not touch, then quit
        if opEndIndex < self.__beginIndex or opBeginIndex > endAffectedIndex:
            return False

        # determine how this operation will change considering the added operation
        if opBeginIndex < self.__beginIndex:
            if opEndIndex <= endAffectedIndex:
                self.__newTokens = opTokens + self.__newTokens[opEndIndex - self.__beginIndex :]
                self.__beginIndex = opBeginIndex
            else:
                self.__newTokens = opTokens
                self.__beginIndex = opBeginIndex
                self.__endIndex += opEndIndex - endAffectedIndex
        else:
            if opEndIndex <= endAffectedIndex:
                self.__newTokens = (self.__newTokens[: opBeginIndex - self.__beginIndex] + opTokens +
                        self.__newTokens[opEndIndex - self.__beginIndex :])
            else:
                self.__newTokens = self.__newTokens[: opBeginIndex - self.__beginIndex] + opTokens
                self.__endIndex += opEndIndex - endAffectedIndex

        # set the type and return true
        self.__setType()
        return True

    def addStrict(self, operation: EditOperation) -> bool:
        """
        Attempts to add the given `operation` such that it is strictly compatible with the overall `CompoundOperation`.
        If the addition was successful, then returns True; else, returns False.
        """

        # stop if the operation is not of the same type
        if not (type(operation) is self.__type or
                type(operation) is CompoundOperation and operation.getType() is self.__type):
            return False

        # add the operation
        return self.addLoose(operation)

    def applyToTokens(self, tokens: List[str]) -> None:
        """
        Applies the operation to the given list of tokens. Raises an error if it affects token indices which
        are out of bounds.
        """

        if self.__endIndex > len(tokens):
            raise IndexError("CompoundOperation: cannot delete index range {}:{} (out of bounds)".format(
                    self.__beginIndex, self.__endIndex))

        tokens[self.__beginIndex:self.__endIndex] = self.__newTokens

    def __setType(self) -> None:
        """
        Sets the type based on the index range and the tokens to be inserted.
        """

        indexRange = self.__endIndex - self.__beginIndex

        # If nothing is removed and nothing is inserted, then it's a replace operation
        if indexRange == 0 and len(self.__newTokens) == 0:
            self.__type = ReplaceOperation

        # If nothing is removed, then it's an insert operation
        elif indexRange == 0:
            self.__type = InsertOperation

        # if nothing is added, then it's a delete operation
        elif len(self.__newTokens) == 0:
            self.__type = DeleteOperation

        # anything else means that there are removals and additions, so it's a replaceOperation
        else:
            self.__type = ReplaceOperation

# Cell
#hide
# define for a second time here so that CompoundOperation no longer has to be a forward declaration
EditOperation = Union[InsertOperation, DeleteOperation, ReplaceOperation, CompoundOperation]