package com.jayway.jsonpath.internal;

import com.jayway.jsonpath.InvalidPathException;

public class CharacterIndex {

    private static final char OPEN_PARENTHESIS = '(';
    private static final char CLOSE_PARENTHESIS = ')';
    private static final char CLOSE_SQUARE_BRACKET = ']';
    private static final char SPACE = ' ';
    private static final char ESCAPE = '\\';
    private static final char SINGLE_QUOTE = '\'';
    private static final char DOUBLE_QUOTE = '"';
    private static final char MINUS = '-';
    private static final char PERIOD = '.';
    private static final char REGEX = '/';

    //workaround for issue: https://github.com/json-path/JsonPath/issues/590
    private static final char SCI_E = 'E';
    private static final char SCI_e = 'e';

    private final CharSequence charSequence;
    private int position;
    private int endPosition;

    public CharacterIndex(CharSequence charSequence) {
        this.charSequence = charSequence;
        this.position = 0;
        this.endPosition = charSequence.length() - 1;
    }

    public int length() {
        return endPosition + 1;
    }

    public char charAt(int idx) {
        return charSequence.charAt(idx);
    }

    public char currentChar() {
        return charSequence.charAt(position);
    }

    public boolean currentCharIs(char c) {
        return (charSequence.charAt(position) == c);
    }

    public boolean lastCharIs(char c) {
      return charSequence.charAt(endPosition) == c;
    }

    public boolean nextCharIs(char c) {
        return inBounds(position + 1) && (charSequence.charAt(position + 1) == c);
    }

    public int incrementPosition(int charCount) {
        return setPosition(position + charCount);
    }

    public int decrementEndPosition(int charCount) {
        return setEndPosition(endPosition - charCount);
    }

    public int setPosition(int newPosition) {
        //position = min(newPosition, charSequence.length() - 1);
        position = newPosition;
        return position;
    }

    private int setEndPosition(int newPosition) {
        endPosition = newPosition;
        return endPosition;
    }

    public int position(){
        return position;
    }

    public int indexOfClosingSquareBracket(int startPosition) {
        int readPosition = startPosition;
        while (inBounds(readPosition)) {
            if(charAt(readPosition) == CLOSE_SQUARE_BRACKET){
                return readPosition;
            }
            readPosition++;
        }
        return -1;
    }

    public int indexOfMatchingCloseChar(int startPosition, char openChar, char closeChar, boolean skipStrings, boolean skipRegex) {
        if(charAt(startPosition) != openChar){
            throw new InvalidPathException("Expected " + openChar + " but found " + charAt(startPosition));
        }

        int opened = 1;
        int readPosition = startPosition + 1;
        while (inBounds(readPosition)) {
            if (skipStrings) {
                char quoteChar = charAt(readPosition);
                if (quoteChar == SINGLE_QUOTE || quoteChar == DOUBLE_QUOTE){
                    readPosition = nextIndexOfUnescaped(readPosition, quoteChar);
                    if(readPosition == -1){
                        throw new InvalidPathException("Could not find matching close quote for " + quoteChar + " when parsing : " + charSequence);
                    }
                    readPosition++;
                }
            }
            if (skipRegex) {
                if (charAt(readPosition) == REGEX) {
                    readPosition = nextIndexOfUnescaped(readPosition, REGEX);
                    if(readPosition == -1){
                        throw new InvalidPathException("Could not find matching close for " + REGEX + " when parsing regex in : " + charSequence);
                    }
                    readPosition++;
                }
            }
            if (charAt(readPosition) == openChar) {
                opened++;
            }
            if (charAt(readPosition) == closeChar) {
                opened--;
                if (opened == 0) {
                    return readPosition;
                }
            }
            readPosition++;
        }
        return -1;
    }

    public int indexOfClosingBracket(int startPosition, boolean skipStrings, boolean skipRegex) {
        return indexOfMatchingCloseChar(startPosition, OPEN_PARENTHESIS, CLOSE_PARENTHESIS, skipStrings, skipRegex);
    }

    public int indexOfNextSignificantChar(char c) {
        return indexOfNextSignificantChar(position, c);
    }

    public int indexOfNextSignificantChar(int startPosition, char c) {
        int readPosition = startPosition + 1;
        while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) {
            readPosition++;
        }
        if (charAt(readPosition) == c) {
            return readPosition;
        } else {
            return -1;
        }
    }

    public int nextIndexOf(char c) {
        return nextIndexOf(position + 1, c);
    }

    public int nextIndexOf(int startPosition, char c) {
        int readPosition = startPosition;
        while (!isOutOfBounds(readPosition)) {
            if (charAt(readPosition) == c) {
                return readPosition;
            }
            readPosition++;
        }
        return -1;
    }

    public int nextIndexOfUnescaped(char c) {
        return nextIndexOfUnescaped(position, c);
    }

    public int nextIndexOfUnescaped(int startPosition, char c) {

        int readPosition = startPosition + 1;
        boolean inEscape = false;
        while (!isOutOfBounds(readPosition)) {
            if(inEscape){
                inEscape = false;
            } else if('\\' == charAt(readPosition)){
                inEscape = true;
            } else if (c == charAt(readPosition)){
                return readPosition;
            }
            readPosition ++;
        }
        return -1;
    }

    public char charAtOr(int postition, char defaultChar){
        if(!inBounds(postition)) return defaultChar;
        else return charAt(postition);
    }

    public boolean nextSignificantCharIs(int startPosition, char c) {
        int readPosition = startPosition + 1;
        while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) {
            readPosition++;
        }
        return !isOutOfBounds(readPosition) && charAt(readPosition) == c;
    }

    public boolean nextSignificantCharIs(char c) {
        return nextSignificantCharIs(position, c);
    }

    public char nextSignificantChar() {
        return nextSignificantChar(position);
    }

    public char nextSignificantChar(int startPosition) {
        int readPosition = startPosition + 1;
        while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) {
            readPosition++;
        }
        if (!isOutOfBounds(readPosition)) {
            return charAt(readPosition);
        } else {
            return ' ';
        }
    }

    public void readSignificantChar(char c) {
        if (skipBlanks().currentChar() != c) {
            throw new InvalidPathException(String.format("Expected character: %c", c));
        }
        incrementPosition(1);
    }

    public boolean hasSignificantSubSequence(CharSequence s) {
        skipBlanks();
        if (! inBounds(position + s.length() - 1)) {
            return false;
        }
        if (! subSequence(position, position + s.length()).equals(s)) {
            return false;
        }

        incrementPosition(s.length());
        return true;
    }

    public int indexOfPreviousSignificantChar(int startPosition){
        int readPosition = startPosition - 1;
        while (!isOutOfBounds(readPosition) && charAt(readPosition) == SPACE) {
            readPosition--;
        }
        if (!isOutOfBounds(readPosition)) {
            return readPosition;
        } else {
            return -1;
        }
    }

    public int indexOfPreviousSignificantChar(){
        return indexOfPreviousSignificantChar(position);
    }

    public char previousSignificantChar(int startPosition) {
        int previousSignificantCharIndex = indexOfPreviousSignificantChar(startPosition);
        if(previousSignificantCharIndex == -1) return ' ';
        else return charAt(previousSignificantCharIndex);
    }

    public char previousSignificantChar() {
        return previousSignificantChar(position);
    }

    public boolean currentIsTail() {
        return position >= endPosition;
    }

    public boolean hasMoreCharacters() {
        return inBounds(position + 1);
    }

    public boolean inBounds(int idx) {
        return (idx >= 0) && (idx <= endPosition);
    }
    public boolean inBounds() {
        return inBounds(position);
    }

    public boolean isOutOfBounds(int idx) {
        return !inBounds(idx);
    }

    public CharSequence subSequence(int start, int end) {
        return charSequence.subSequence(start, end);
    }

    public CharSequence charSequence() {
        return charSequence;
    }

    @Override
    public String toString() {
        return charSequence.toString();
    }

    public boolean isNumberCharacter(int readPosition) {
        char c = charAt(readPosition);
        //workaround for issue: https://github.com/json-path/JsonPath/issues/590
        return Character.isDigit(c) || c == MINUS  || c == PERIOD || c == SCI_E || c == SCI_e;
    }

    public CharacterIndex skipBlanks() {
        while (inBounds() && position < endPosition  && currentChar() == SPACE){
            incrementPosition(1);
        }
        return this;
    }

    private CharacterIndex skipBlanksAtEnd() {
        while (inBounds() && position < endPosition && lastCharIs(SPACE)){
            decrementEndPosition(1);
        }
        return this;
    }

    public CharacterIndex trim() {
        skipBlanks();
        skipBlanksAtEnd();
        return this;
    }
}
