package org.postgresql.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;

/**
 * Parses a SQL Query.
 * It handles Java Escape Processing.
 * If the query is a select to a single table, it will determine the table's name.
 *
 * @author Ulrich Meis
 */
public class QueryParser {

    /**
     * somewhere in between
     */
    public static final int SPACE = 0;

    /**
     * within an unquoted sequence of characters that doesn't contain ,?{}$'";()
     */
    public static final int UNQUOTED = 1;

    /**
     * within a literal
     */
    public static final int LIT_QUOTED = 2;

    /**
     * within a quoted identifier
     */
    public static final int ID_QUOTED = 3;


    /**
     * can be escaped by a preceding backslash, see docs 4.1.2.1
     */
    public static final char LIT_QUOTE = '\'';

    /**
     * can not be escaped by a preceding backslash, see docs 4.1.1
     */
    public static final char ID_QUOTE = '"';


    /**
     * considered as whitespace, see docs 4.1
     */
    public static final char WS1 = ' ';

    /**
     * considered as whitespace, see docs 4.1
     */
    public static final char WS2 = '\n';

    /**
     * considered as whitespace, see docs 4.1
     */
    public static final char WS3 = '\t';

    public static final char COMMA = ',';
    public static final char QUESTIONMARK = '?';
    public static final char CBRACE_O = '{';
    public static final char CBRACE_C = '}';
    public static final char BRACKET_O = '(';
    public static final char BRACKET_C = ')';
    public static final char DOLLAR = '$';
    public static final char SEMICOLON = ';';


    /**
     * if one of these items occurs after from, it marks the end of the from clause.
     * The from clause needs to be examined in order to check if we deal with a single table or not.
     */
    public static final HashSet endingfrom = new HashSet(10);

    static {
        endingfrom.add("WHERE");
        endingfrom.add("GROUP");
        endingfrom.add("HAVING");
        endingfrom.add("UNION");
        endingfrom.add("INTERSECT");
        endingfrom.add("EXCEPT");
        endingfrom.add("ORDER");
        endingfrom.add("LIMIT");
        endingfrom.add("OFFSET");
        endingfrom.add("JOIN");
    }


    /**
     * if dealing with a select to a single table, the table's name is stored here
     */
    private String tableName;

    /**
     * as passed to the constructor
     */
    private String query;

    /**
     * as computed by fractionize
     */
    private ArrayList output = new ArrayList();

    /**
     * Each subquery contains an array of fragments (fragments being delimited by ?)
     */
    private ArrayList subqueryL = new ArrayList();

    /**
     * indicates whether Java Escape Syntax should be handled
     */
    private boolean replaceProcessing;

    /**
     * holds the prefix of a function call for the running server version
     */
    private String functioncallsyntax;

    /**
     * Saves passed arguments and runs fractionize
     */
    public QueryParser(String query, boolean replaceProcessing, boolean greater7dot3) {
        if (greater7dot3)
            functioncallsyntax = "select * from ";
        else
            functioncallsyntax = "select ";
        this.query = query;
        this.replaceProcessing = replaceProcessing;
        fractionize();
    }

    /**
     * Parses the given string and computes its fractions.
     * The division into fractions eliminates further need of handling quotes.
     * A few properties of a fraction:
     * 1. A quoted literal or identifier makes up one and only one fraction
     * 2. A fraction either contains only unquoted whitespace or no unquoted whitespace at all
     * 3. The following characters have, if not appearing in quotes, a fraction of their own: ,?{};()
     * <p/>
     * The addFraction method handles further processing.
     */
    private void fractionize() {
        // current position
        int pos = 0;

        // current state
        int state = SPACE;

        // is filled up while parsing
        String fraction = "";

        // holds current and next character respectively
        char cur, next;

        // go ones through the query
        while (pos < query.length()) {

            cur = query.charAt(pos);
            next = (pos + 1) == query.length() ? ' ' : query.charAt(pos + 1);

            // decides new state upon current state and current character
            switch (state) {

                case SPACE:
                    switch (cur) {
                        case LIT_QUOTE:
                            if (fraction.length() > 0) addFraction(fraction);
                            fraction = "" + cur;
                            state = LIT_QUOTED;
                            break;
                        case ID_QUOTE:
                            if (fraction.length() > 0) addFraction(fraction);
                            fraction = "" + cur;
                            state = ID_QUOTED;
                            break;
                        case WS1:
                        case WS2:
                        case WS3:
                            fraction += cur;
                            break;
                        case COMMA:
                        case QUESTIONMARK:
                        case CBRACE_C:
                        case CBRACE_O:
                        case BRACKET_O:
                        case BRACKET_C:
                        case SEMICOLON:
                            if (fraction.length() > 0) addFraction(fraction);
                            addFraction("" + cur);
                            fraction = "";
                            break;
                        default:
                            if (fraction.length() > 0) addFraction(fraction);
                            fraction = "" + cur;
                            state = UNQUOTED;
                            break;
                        case DOLLAR:
                            if (fraction.length() > 0) addFraction(fraction);
                            int closing = query.indexOf(DOLLAR, pos + 1);
                            if (closing == -1) break; // no more dollars, throw an error ?
                            String dollarword = query.substring(pos, closing + 1);
                            int second = query.indexOf(dollarword, pos + 1);
                            if (second == -1) break; // no matching dollarword, throw an error ?
                            fraction = query.substring(pos, second + dollarword.length());
                            addFraction(fraction);
                            fraction = "";
                            pos = second + dollarword.length() - 1;
                            break;
                    }
                    ;

                    break;
                case UNQUOTED:
                    switch (cur) {
                        case LIT_QUOTE:
                            addFraction(fraction);
                            fraction = "" + cur;
                            state = LIT_QUOTED;
                            break;
                        case ID_QUOTE:
                            addFraction(fraction);
                            fraction = "" + cur;
                            state = ID_QUOTED;
                            break;
                        case WS1:
                        case WS2:
                        case WS3:
                            addFraction(fraction);
                            fraction = "" + cur;
                            state = SPACE;
                            break;
                        case COMMA:
                        case QUESTIONMARK:
                        case CBRACE_C:
                        case CBRACE_O:
                        case BRACKET_O:
                        case BRACKET_C:
                        case SEMICOLON:
                            addFraction(fraction);
                            addFraction("" + cur);
                            fraction = "";
                            state = SPACE;
                            break;
                        default:
                            fraction += cur;
                            break;
                    }
                    ;
                    break;
                case LIT_QUOTED:
                    if (cur == LIT_QUOTE) {
                        if (next == LIT_QUOTE) {
                            fraction += cur;
                            fraction += next;
                            pos++;
                            break;
                        } else if (query.charAt(pos - 1) != '\\') {
                            addFraction(fraction + cur);
                            fraction = "";
                            state = SPACE;
                            break;
                        }
                    }
                    fraction += cur;
                    break;
                case ID_QUOTED:
                    if (cur == ID_QUOTE) {
                        if (next == ID_QUOTE) {
                            fraction += cur;
                            fraction += next;
                            pos++;
                        } else {
                            addFraction(fraction + cur);
                            fraction = "";
                            state = SPACE;
                        }
                    } else
                        fraction += cur;
                    ;
                    break;
            }
            pos++;
        }

        // save last word if there is one
        if (state == UNQUOTED) addFraction(fraction);

        if (fragment.length() > 0) fragmentL.add(fragment);
        if (fragmentL.size() > 0) subqueryL.add(fragmentL);

        // delete tableName in case it turned out not to be a single table select query
        if ((querytype == NONSINGLE) || (subqueryL.size() > 1)) tableName = null;
    }


    /**
     * addFraction stores the current fragment list here
     */
    private ArrayList fragmentL = new ArrayList();

    /**
     * addFraction stores the current fragment here
     */
    private String fragment = "";

    /**
     * addFraction stores the first char in the current fraction here
     */
    private char first;

    /**
     * We don't know the querytype yet
     */
    public static final int UNDEFINED = 0;

    /**
     * We deal with a select statement
     */
    public static final int SELECT = 1;

    /**
     * Whatever it is, it is not a select to a single table
     */
    public static final int NONSINGLE = 2;

    /**
     * used by addFraction
     */
    private int querytype = UNDEFINED;


    // from states for addFraction

    public static final int BEFORE_FROM_CLAUSE = 0;
    public static final int BEFORE_TABLE_NAME = 1;
    public static final int IN_FROM_CLAUSE = 2;
    public static final int AFTER_FROM_CLAUSE = 3;

    /**
     * used by addFraction
     */
    private int fromstate = BEFORE_FROM_CLAUSE;

    /**
     * Not Java escaping
     */
    public static final int STANDARD = 0;

    /**
     * Just found the opening bracket
     */
    public static final int JE_START = 1;

    /**
     * we found a question mark and are waiting for a '='
     */
    public static final int JE_CALL_QM = 2;

    /**
     * now we expect to see the call word
     */
    public static final int JE_CALL = 3;

    /**
     * we are done with handling the escape, just need to erase the closing bracket
     */
    public static final int JE_DONE = 4;

    private int fractionstate = STANDARD;

    /** need to count these for the table checking */
    private int openbrackets;

    private void addFraction(String fraction) {
        first = fraction.charAt(0);
        output.add(fraction);
        switch (fractionstate) {
            case JE_START:
                if (isWhitespace(first)) return;
                if (first == QUESTIONMARK)
                    fractionstate = JE_CALL_QM;
                else {
                    if (fraction.equalsIgnoreCase("call")) fragment += functioncallsyntax;
                    fractionstate = JE_DONE; // throw an error if it's not a legal escape?
                }
                return;
            case JE_CALL_QM:
                if (isWhitespace(first)) return;
                fractionstate = JE_CALL; // throw error if it ain't a '=' after a '?' ?
                return;
            case JE_CALL:
                if (isWhitespace(first)) return;
                fractionstate = JE_DONE; //throw an error if it ain't a call?
                fragment += functioncallsyntax;
                return;
            case STANDARD:
                if (first == SEMICOLON) {
                    if (fragment.length() > 0) fragmentL.add(fragment);
                    if (fragmentL.size() > 0) subqueryL.add(fragmentL);
                    fragmentL = new ArrayList();
                    fragment = "";
                    return;
                }
                if (replaceProcessing && first == CBRACE_O) {
                    fractionstate = JE_START;
                    return;
                }
                // notably there is no break here
            case JE_DONE:
                if ((fractionstate == JE_DONE) && fraction.equals("}")) {
                    fractionstate = STANDARD;
                    return;
                }
                if (first == QUESTIONMARK) {
                    fragmentL.add(fragment);
                    fragment = "";
                    return;
                }
                fragment += fraction;
                if (isWhitespace(first)) break; // from now on we don't care about white space

                // detect table name

                if (querytype == UNDEFINED) {
                    if (fraction.equalsIgnoreCase("SELECT"))
                        querytype = SELECT;
                    else
                        querytype = NONSINGLE;
                    break;
                }

                // if query is nonsingle we're done here
                if (querytype == NONSINGLE) break;

                if (openbrackets==0) switch (fromstate) {
                    case BEFORE_FROM_CLAUSE:
                        if (fraction.equalsIgnoreCase("FROM")) fromstate = BEFORE_TABLE_NAME;
                        break;
                    case BEFORE_TABLE_NAME:
                        if (fraction.equalsIgnoreCase("ONLY")) break;
                        if (first == BRACKET_O) querytype = NONSINGLE; // from (select ...)
                        tableName = fraction;
                        fromstate = IN_FROM_CLAUSE;
                        break;
                    case IN_FROM_CLAUSE:
                        if (first == BRACKET_O) {
                            querytype = NONSINGLE; // a function
                            break;
                        }
                        if (first == COMMA) {
                            querytype = NONSINGLE; // second from clause
                            break;
                        }
                        if (endingfrom.contains(fraction.toUpperCase()))
                            fromstate = AFTER_FROM_CLAUSE;
                        else
                            break;
                    case AFTER_FROM_CLAUSE:
                        if (fraction.equalsIgnoreCase("JOIN") || fraction.equalsIgnoreCase("UNION")) querytype = NONSINGLE;
                }
                if (first == BRACKET_O) openbrackets++;
                else if (first == BRACKET_C) openbrackets--;
                break;
        }

    }

    private boolean isWhitespace(char c) {
        return (c == WS1) || (c == WS2) || (c == WS3);
    }

    public String getTableName() {
        return tableName;
    }

    public static void main(String[] args) {
        byte[] bytes = new byte[100];
        while (true) {
            System.out.print("Your Query Please: ");
            try {
                int count = System.in.read(bytes);
                if (count <= 2) System.exit(0);
                System.out.println("\n");
                String input = new String(bytes, 0, count);
                QueryParser qp = new QueryParser(input, true, true);

                for (int i = 0; i < qp.output.size(); i++) System.out.print(((String) qp.output.get(i)) + '|');
                System.out.println("\n");

                if (qp.getTableName() != null) System.out.println("Table: " + qp.getTableName() + "\n");

                for (int i = 0; i < qp.subqueryL.size(); i++) {
                    System.out.print("Subquery " + i + " : ");
                    for (int j = 0; j < ((ArrayList) qp.subqueryL.get(i)).size(); j++) {
                        System.out.print(((ArrayList) qp.subqueryL.get(i)).get(j) + ((j == ((ArrayList) qp.subqueryL.get(i)).size() - 1) ? "" : " X?X "));
                    }
                    System.out.println("\n---------\n");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
