package com.github.aaronshan.functions.utils.json;

import com.google.common.collect.AbstractIterator;

import static java.lang.Character.isLetterOrDigit;
import static java.lang.String.format;

/**
 * @author ruifeng.shan
 * date: 2016-07-25
 * time: 15:06
 */
public class JsonPathTokenizer extends AbstractIterator<String> {
    private static final char QUOTE = '\"';
    private static final char DOT = '.';
    private static final char OPEN_BRACKET = '[';
    private static final char CLOSE_BRACKET = ']';
    private static final char UNICODE_CARET = '\u2038';

    private final String path;
    private int index;

    public JsonPathTokenizer(String path) {
        if (path == null) {
            throw new NullPointerException("path is null");
        }
        this.path = path;

        if (path.isEmpty()) {
            throw invalidJsonPath();
        }

        // skip the start token
        match('$');
    }

    private static boolean isUnquotedPathCharacter(char c) {
        return c == ':' || isUnquotedSubscriptCharacter(c);
    }

    private static boolean isUnquotedSubscriptCharacter(char c) {
        return c == '_' || isLetterOrDigit(c);
    }

    @Override
    protected String computeNext() {
        if (!hasNextCharacter()) {
            return endOfData();
        }

        if (tryMatch(DOT)) {
            return matchPathSegment();
        }

        if (tryMatch(OPEN_BRACKET)) {
            String token = tryMatch(QUOTE) ? matchQuotedSubscript() : matchUnquotedSubscript();

            match(CLOSE_BRACKET);
            return token;
        }

        throw invalidJsonPath();
    }

    private String matchPathSegment() {
        // seek until we see a special character or whitespace
        int start = index;
        while (hasNextCharacter() && isUnquotedPathCharacter(peekCharacter())) {
            nextCharacter();
        }
        int end = index;

        String token = path.substring(start, end);

        // an empty unquoted token is not allowed
        if (token.isEmpty()) {
            throw invalidJsonPath();
        }

        return token;
    }

    private String matchUnquotedSubscript() {
        // seek until we see a special character or whitespace
        int start = index;
        while (hasNextCharacter() && isUnquotedSubscriptCharacter(peekCharacter())) {
            nextCharacter();
        }
        int end = index;

        String token = path.substring(start, end);

        // an empty unquoted token is not allowed
        if (token.isEmpty()) {
            throw invalidJsonPath();
        }

        return token;
    }

    private String matchQuotedSubscript() {
        // quote has already been matched

        // seek until we see the close quote
        int start = index;
        while (hasNextCharacter() && peekCharacter() != QUOTE) {
            nextCharacter();
        }
        int end = index;

        String token = path.substring(start, end);

        match(QUOTE);
        return token;
    }

    private boolean hasNextCharacter() {
        return index < path.length();
    }

    private void match(char expected) {
        if (!tryMatch(expected)) {
            throw invalidJsonPath();
        }
    }

    private boolean tryMatch(char expected) {
        if (peekCharacter() != expected) {
            return false;
        }
        index++;
        return true;
    }

    private void nextCharacter() {
        index++;
    }

    private char peekCharacter() {
        return path.charAt(index);
    }

    private RuntimeException invalidJsonPath() {
        return new RuntimeException(format("Invalid JSON path: '%s'", path));
    }

    @Override
    public String toString() {
        return path.substring(0, index) + UNICODE_CARET + path.substring(index);
    }
}