package org.jabref.logic.util.strings;

import org.jabref.logic.formatter.casechanger.CapitalizeFormatter;
import org.jabref.logic.formatter.casechanger.LowerCaseFormatter;
import org.jabref.logic.formatter.casechanger.UpperCaseFormatter;
import org.jabref.model.util.ResultingStringState;

public class StringManipulator {
    private enum LetterCase {
        UPPER,
        LOWER,
        CAPITALIZED
    }

    enum Direction {
        NEXT(1),
        PREVIOUS(-1);

        public final int OFFSET;

        Direction(int offset) {
            this.OFFSET = offset;
        }
    }

    /**
     * Change word casing in a string from the given position to the next word boundary.
     *
     * @param text          The text to manipulate.
     * @param caretPosition The index to start from.
     * @param targetCase    The case mode the string should be changed to.
     *
     * @return              The resulting text and caret position.
     */
    private static ResultingStringState setWordCase(String text, int caretPosition, LetterCase targetCase) {
        int nextWordBoundary = getNextWordBoundary(caretPosition, text, Direction.NEXT);

        // Preserve whitespaces
        int wordStartPosition = caretPosition;
        while (wordStartPosition < nextWordBoundary && Character.isWhitespace(text.charAt(wordStartPosition))) {
            wordStartPosition++;
        }

        String result = switch (targetCase) {
            case UPPER -> (new UpperCaseFormatter()).format(text.substring(wordStartPosition, nextWordBoundary));
            case LOWER -> (new LowerCaseFormatter()).format(text.substring(wordStartPosition, nextWordBoundary));
            case CAPITALIZED -> (new CapitalizeFormatter()).format(text.substring(wordStartPosition, nextWordBoundary));
        };

        return new ResultingStringState(
                nextWordBoundary,
                text.substring(0, wordStartPosition) + result + text.substring(nextWordBoundary));
    }

    /**
     * Delete all characters in a string from the given position to the next word boundary.
     *
     * @param caretPosition The index to start from.
     * @param text          The text to manipulate.
     * @param direction     The direction to search.
     *
     * @return              The resulting text and caret position.
     */
    static ResultingStringState deleteUntilWordBoundary(int caretPosition, String text, Direction direction) {
        // Define cutout range
        int nextWordBoundary = getNextWordBoundary(caretPosition, text, direction);

        // Construct new string without cutout
        return switch (direction) {
            case NEXT -> new ResultingStringState(
                    caretPosition,
                    text.substring(0, caretPosition) + text.substring(nextWordBoundary));
            case PREVIOUS -> new ResultingStringState(
                    nextWordBoundary,
                    text.substring(0, nextWordBoundary) + text.substring(caretPosition));
        };
    }

    /**
     * Utility method to find the next whitespace position in string after text
     *
     * @param caretPosition The current caret position
     * @param text          The string to search in
     * @param direction     The direction to move through string
     *
     * @return              The position of the next whitespace after a word
     */
    static int getNextWordBoundary(int caretPosition, String text, Direction direction) {
        int i = caretPosition;

        if (direction == Direction.PREVIOUS) {
            // Swallow whitespaces
            while (i > 0 && Character.isWhitespace((text.charAt(i + direction.OFFSET)))) {
                i += direction.OFFSET;
            }

            // Read next word
            while (i > 0 && !Character.isWhitespace(text.charAt(i + direction.OFFSET))) {
                i += direction.OFFSET;
            }
        } else if (direction == Direction.NEXT) {
            // Swallow whitespaces
            while (i < text.length() && Character.isWhitespace(text.charAt(i))) {
                i += direction.OFFSET;
            }

            // Read next word
            while (i < text.length() && !Character.isWhitespace((text.charAt(i)))) {
                i += direction.OFFSET;
            }
        }

        return i;
    }

    /**
     * Capitalize the word on the right side of the cursor.
     *
     * @param caretPosition The position of the cursor
     * @param text          The string to manipulate
     *
     * @return String       The resulting text and caret position.
     */
    public static ResultingStringState capitalize(int caretPosition, String text) {
        return setWordCase(text, caretPosition, LetterCase.CAPITALIZED);
    }

    /**
     * Make all characters in the word uppercase.
     *
     * @param caretPosition The position of the cursor
     * @param text          The string to manipulate
     *
     * @return String       The resulting text and caret position.
     */
    public static ResultingStringState uppercase(int caretPosition, String text) {
        return setWordCase(text, caretPosition, LetterCase.UPPER);
    }

    /**
     * Make all characters in the word lowercase.
     *
     * @param caretPosition The position of the cursor
     * @param text          The string to manipulate
     *
     * @return String       The resulting text and caret position.
     */
    public static ResultingStringState lowercase(int caretPosition, String text) {
        return setWordCase(text, caretPosition, LetterCase.LOWER);
    }

    /**
     * Remove the next word on the right side of the cursor.
     *
     * @param caretPosition The position of the cursor
     * @param text          The string to manipulate
     *
     * @return String       The resulting text and caret position.
     */
    public static ResultingStringState killWord(int caretPosition, String text) {
        return deleteUntilWordBoundary(caretPosition, text, Direction.NEXT);
    }

    /**
     * Remove the previous word on the left side of the cursor.
     *
     * @param caretPosition The position of the cursor
     * @param text          The string to manipulate
     *
     * @return String       The resulting text and caret position.
     */
    public static ResultingStringState backwardKillWord(int caretPosition, String text) {
        return deleteUntilWordBoundary(caretPosition, text, Direction.PREVIOUS);
    }
}
