/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
// DWB14: parser loops if // comment at start of program
// DWB15: allow program to have trailing ';'
package org.apache.karaf.shell.support.parsing;

import java.util.ArrayList;
import java.util.List;

public class GogoParser {

    int current = 0;
    String text;
    boolean escaped;
    static final String SPECIAL = "<;|{[\"'$`(=";

    List<List<List<String>>> program;
    List<List<String>> statements;
    List<String> statement;
    int cursor;
    int start = -1;
    int c0;
    int c1;
    int c2;
    int c3;
    
    public GogoParser(String text, int cursor) {
    	this.text = text;
        this.cursor = cursor;
	}

	public void ws() {
        // derek: BUGFIX: loop if comment  at beginning of input
        //while (!eof() && isWhitespace(peek())) {
        while (!eof() && (!escaped && isWhitespace(peek()) || current == 0)) {
            if (current != 0 || !escaped && isWhitespace(peek())) {
                current++;
            }
            if (peek() == '/' && current < text.length() - 2
                && text.charAt(current + 1) == '/') {
                comment();
            }
            if (current == 0) {
                break;
            }
        }
    }

    private boolean isWhitespace(char ch) {
        return ch != '\n' && Character.isWhitespace(ch);
    }

    private void comment() {
        while (!eof() && peek() != '\n' && peek() != '\r') {
            next();
        }
    }

    public boolean eof() {
        return current >= text.length();
    }

    public char peek() {
        return peek(false);
    }

    char peek(boolean increment) {
        escaped = false;
        if (eof()) {
            return 0;
        }

        int last = current;
        char c = text.charAt(current++);

        if (c == '\\') {
            escaped = true;
            if (eof()) {
                throw new RuntimeException("Eof found after \\");
            }

            c = text.charAt(current++);

            switch (c) {
                case 't':
                    c = '\t';
                    break;
                case '\r':
                case '\n':
                    c = ' ';
                    break;
                case 'b':
                    c = '\b';
                    break;
                case 'f':
                    c = '\f';
                    break;
                case 'n':
                    c = '\n';
                    break;
                case 'r':
                    c = '\r';
                    break;
                case 'u':
                    c = unicode();
                    current += 4;
                    break;
                default:
                    // We just take the next character literally
                    // but have the escaped flag set, important for {},[] etc
            }
        }
        if (cursor > last && cursor <= current) {
            c0 = program != null ? program.size() : 0;
            c1 = statements != null ? statements.size() : 0;
            c2 = statement != null ? statement.size() : 0;
            c3 = (start >= 0) ? current - start : 0;
        }
        if (!increment) {
            current = last;
        }
        return c;
    }

    public List<List<List<String>>> program() {
        program = new ArrayList<>();
        ws();
        if (!eof()) {
            program.add(pipeline());
            while (peek() == ';' || peek() == '\n') {
                current++;
                List<List<String>> pipeline = pipeline();
                program.add(pipeline);
            }
        }
        if (!eof()) {
            throw new RuntimeException("Program has trailing text: " + context(current));
        }

        List<List<List<String>>> p = program;
        program = null;
        return p;
    }

    CharSequence context(int around) {
        return text.subSequence(Math.max(0, current - 20), Math.min(text.length(),
            current + 4));
    }

    public List<List<String>> pipeline() {
        statements = new ArrayList<>();
        statements.add(statement());
        while (peek() == '|') {
            current++;
            ws();
            if (!eof()) {
                statements.add(statement());
            }
            else {
                statements.add(new ArrayList<>());
                break;
            }
        }
        List<List<String>> s = statements;
        statements = null;
        return s;
    }

    public List<String> statement() {
        statement = new ArrayList<>();
        statement.add(value());
        while (!eof()) {
            ws();
            if (peek() == '|' || peek() == ';' || peek() == '\n') {
                break;
            }

            if (!eof()) {
                statement.add(messy());
            }
        }
        List<String> s = statement;
        statement = null;
        return s;
    }

    public String messy()
    {
        start = current;
        char c = peek();
        if (c > 0 && SPECIAL.indexOf(c) < 0) {
            current++;
            try {
                while (!eof()) {
                    c = peek();
                    if (!escaped && (c == ';' || c == '|' || c == '\n' || isWhitespace(c))) {
                        break;
                    }
                    next();
                }
                return text.substring(start, current);
            } finally {
                start = -1;
            }
        }
        else {
            return value();
        }
    }

    public int position() {
        return current;
    }

    public int cursorArgumentIndex() {
        return c2;
    }

    public int argumentPosition() {
        return c3;
    }

    public String value() {
        ws();

        start = current;
        try {
            char c = next();
            if (!escaped) {
                switch (c) {
                    case '{':
                        return text.substring(start, find('}', '{'));
                    case '(':
                        return text.substring(start, find(')', '('));
                    case '[':
                        return text.substring(start, find(']', '['));
                    case '<':
                        return text.substring(start, find('>', '<'));
                    case '=':
                        return text.substring(start, current);
                    case '"':
                    case '\'':
                        quote(c);
                        break;
                }
            }

            // Some identifier or number
            while (!eof()) {
                c = peek();
                if (!escaped) {
                    if (isWhitespace(c) || c == ';' || c == '|' || c == '=') {
                        break;
                    }
                    else if (c == '{') {
                        next();
                        find('}', '{');
                    }
                    else if (c == '(') {
                        next();
                        find(')', '(');
                    }
                    else if (c == '<') {
                        next();
                        find('>', '<');
                    }
                    else if (c == '[') {
                        next();
                        find(']', '[');
                    }
                    else if (c == '\'' || c == '"') {
                        next();
                        quote(c);
                        next();
                    }
                    else {
                        next();
                    }
                }
                else {
                    next();
                }
            }
            return text.substring(start, current);
        } finally {
            start = -1;
        }
    }

    public boolean escaped() {
        return escaped;
    }

    public char next() {
        return peek(true);
    }

    char unicode() {
        if (current + 4 > text.length()) {
            throw new IllegalArgumentException("Unicode \\u escape at eof at pos ..."
                + context(current) + "...");
        }

        String s = text.subSequence(current, current + 4).toString();
        int n = Integer.parseInt(s, 16);
        return (char) n;
    }

    int find(char target, char deeper) {
        int start = current;
        int level = 1;

        while (level != 0) {
            if (eof()) {
                throw new RuntimeException("Eof found in the middle of a compound for '"
                    + target + deeper + "', begins at " + context(start));
            }

            char c = next();
            if (!escaped) {
                if (c == target) {
                    level--;
                } else {
                    if (c == deeper) {
                        level++;
                    } else {
                        if (c == '"') {
                            quote('"');
                        } else {
                            if (c == '\'') {
                                quote('\'');
                            }
                            else {
                                if (c == '`') {
                                    quote('`');
                                }
                            }
                        }
                    }
                }
            }
        }
        return current;
    }

    int quote(char which) {
        while (!eof() && (peek() != which || escaped)) {
            next();
        }

        return current++;
    }

    CharSequence findVar() {
        int start = current;
        char c = peek();

        if (c == '{') {
            next();
            int end = find('}', '{');
            return text.subSequence(start, end);
        }
        if (c == '(') {
            next();
            int end = find(')', '(');
            return text.subSequence(start, end);
        }

        if (Character.isJavaIdentifierPart(c)) {
            while (c == '$') {
                c = next();
            }
            while (!eof() && (Character.isJavaIdentifierPart(c) || c == '.') && c != '$') {
                next();
                c = peek();
            }
            return text.subSequence(start, current);
        }
        throw new IllegalArgumentException(
            "Reference to variable does not match syntax of a variable: "
                + context(start));
    }

    public String toString() {
        return "..." + context(current) + "...";
    }

    public String unescape() {
        StringBuilder sb = new StringBuilder();
        while (!eof()) {
            sb.append(next());
        }
        return sb.toString();
    }

}
