package edu.caltech.nanodb.sqlparse;


import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;

import edu.caltech.nanodb.commands.ShowSystemStatsCommand;
import edu.caltech.nanodb.expressions.ArithmeticOperator;
import edu.caltech.nanodb.expressions.BooleanOperator;
import edu.caltech.nanodb.expressions.ColumnName;
import edu.caltech.nanodb.expressions.ColumnValue;
import edu.caltech.nanodb.expressions.CompareOperator;
import edu.caltech.nanodb.expressions.DateTimeUtils;
import edu.caltech.nanodb.expressions.ExistsOperator;
import edu.caltech.nanodb.expressions.InSubqueryOperator;
import edu.caltech.nanodb.expressions.InValuesOperator;
import edu.caltech.nanodb.expressions.IsNullOperator;
import edu.caltech.nanodb.expressions.LiteralValue;
import edu.caltech.nanodb.expressions.NegateOperator;
import edu.caltech.nanodb.expressions.ScalarSubquery;
import edu.caltech.nanodb.expressions.StringMatchOperator;
import edu.caltech.nanodb.functions.FunctionDirectory;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;

import edu.caltech.nanodb.commands.AnalyzeCommand;
import edu.caltech.nanodb.commands.BeginTransactionCommand;
import edu.caltech.nanodb.commands.Command;
import edu.caltech.nanodb.commands.CommandProperties;
import edu.caltech.nanodb.commands.CommitTransactionCommand;
import edu.caltech.nanodb.commands.ConstraintDecl;
import edu.caltech.nanodb.commands.CrashCommand;
import edu.caltech.nanodb.commands.CreateIndexCommand;
import edu.caltech.nanodb.commands.CreateTableCommand;
import edu.caltech.nanodb.commands.DeleteCommand;
import edu.caltech.nanodb.commands.DropIndexCommand;
import edu.caltech.nanodb.commands.DropTableCommand;
import edu.caltech.nanodb.commands.DumpIndexCommand;
import edu.caltech.nanodb.commands.DumpTableCommand;
import edu.caltech.nanodb.commands.ExitCommand;
import edu.caltech.nanodb.commands.ExplainCommand;
import edu.caltech.nanodb.commands.FlushCommand;
import edu.caltech.nanodb.commands.InsertCommand;
import edu.caltech.nanodb.commands.OptimizeCommand;
import edu.caltech.nanodb.commands.RollbackTransactionCommand;
import edu.caltech.nanodb.commands.SelectCommand;
import edu.caltech.nanodb.commands.SetPropertyCommand;
import edu.caltech.nanodb.commands.ShowTableStatsCommand;
import edu.caltech.nanodb.commands.ShowTablesCommand;
import edu.caltech.nanodb.commands.ShowPropertiesCommand;
import edu.caltech.nanodb.commands.TableColumnDecl;
import edu.caltech.nanodb.commands.UpdateCommand;
import edu.caltech.nanodb.commands.VerifyCommand;
import edu.caltech.nanodb.expressions.Expression;
import edu.caltech.nanodb.expressions.FunctionCall;
import edu.caltech.nanodb.expressions.OrderByExpression;
import edu.caltech.nanodb.queryast.FromClause;
import edu.caltech.nanodb.queryast.SelectClause;
import edu.caltech.nanodb.queryast.SelectValue;
import edu.caltech.nanodb.relations.ColumnInfo;
import edu.caltech.nanodb.relations.ColumnType;
import edu.caltech.nanodb.relations.ForeignKeyValueChangeOption;
import edu.caltech.nanodb.relations.JoinType;
import edu.caltech.nanodb.relations.SQLDataType;
import edu.caltech.nanodb.relations.TableConstraintType;


/**
 * This class translates NanoSQL parse trees generated by the ANTLR4 grammar
 * into the corresponding hierarchy of {@link Command} and {@link Expression}
 * objects used by the server for command execution.
 */
public class NanoSQLTranslator extends NanoSQLBaseVisitor<Object> {

    /** A function directory for resolving function calls. */
    private FunctionDirectory functionDirectory;


    public NanoSQLTranslator(FunctionDirectory functionDirectory) {
        this.functionDirectory = functionDirectory;
    }


    public NanoSQLTranslator() {
        this(null);
    }


    /*======================================================================*/
    /* ALL COMMAND RULES                                                    */
    /*======================================================================*/


    //=== COMMANDS ===========================================================

    @Override
    public Object visitCommands(NanoSQLParser.CommandsContext ctx) {
        ArrayList<Command> commands = new ArrayList<>();

        for (NanoSQLParser.CommandContext cc : ctx.command())
            commands.add((Command) visit(cc));

        return commands;
    }

    @Override
    public Object visitCommand(NanoSQLParser.CommandContext ctx) {
        return visit(ctx.commandNoSemicolon());
    }

    //=== TRANSACTIONS =======================================================

    @Override
    public Object visitBeginTxnStmt(NanoSQLParser.BeginTxnStmtContext ctx) {
        return new BeginTransactionCommand();
    }

    @Override
    public Object visitCommitTxnStmt(NanoSQLParser.CommitTxnStmtContext ctx) {
        return new CommitTransactionCommand();
    }

    @Override
    public Object visitRollbackTxnStmt(NanoSQLParser.RollbackTxnStmtContext ctx) {
        return new RollbackTransactionCommand();
    }

    //=== GENERAL UTILITY COMMANDS ===========================================

    @Override
    public Object visitFlushStmt(NanoSQLParser.FlushStmtContext ctx) {
        return new FlushCommand();
    }

    @Override
    public Object visitExitStmt(NanoSQLParser.ExitStmtContext ctx) {
        return new ExitCommand();
    }

    @Override
    public Object visitCrashStmt(NanoSQLParser.CrashStmtContext ctx) {
        int secs = 0;
        if (ctx.INT_LITERAL() != null)
            secs = Integer.parseInt(ctx.INT_LITERAL().getText());

        return new CrashCommand(secs);
    }

    @Override
    public Object visitShowPropsStmt(NanoSQLParser.ShowPropsStmtContext ctx) {
        ShowPropertiesCommand cmd = new ShowPropertiesCommand();
        if (ctx.pattern != null)
            cmd.setFilter(ctx.pattern.getText().toLowerCase());

        return cmd;
    }

    @Override
    public Object visitSetPropStmt(NanoSQLParser.SetPropStmtContext ctx) {
        String propName = ctx.name.getText();

        // Remove the quotes from around the property name
        propName = propName.substring(1, propName.length() - 1);

        return new SetPropertyCommand(propName,
            (Expression) visit(ctx.expression()));
    }

    @Override
    public Object visitShowSystemStatsStmt(NanoSQLParser.ShowSystemStatsStmtContext ctx) {
        String systemName = ctx.name.getText();

        // Remove the quotes from around the subsystem name
        systemName = systemName.substring(1, systemName.length() - 1);

        return new ShowSystemStatsCommand(systemName);
    }

    //=== TABLE/INDEX UTILITY COMMANDS =======================================

    @Override
    public Object visitShowTablesStmt(NanoSQLParser.ShowTablesStmtContext ctx) {
        return new ShowTablesCommand();
    }

    @Override
    public Object visitAnalyzeStmt(NanoSQLParser.AnalyzeStmtContext ctx) {
        AnalyzeCommand cmd = new AnalyzeCommand();
        for (TerminalNode n : ctx.IDENT())
            cmd.addTable(n.getText().toLowerCase());

        return cmd;
    }

    @Override
    public Object visitOptimizeStmt(NanoSQLParser.OptimizeStmtContext ctx) {
        OptimizeCommand cmd = new OptimizeCommand();
        for (TerminalNode n : ctx.IDENT())
            cmd.addTable(n.getText().toLowerCase());

        return cmd;
    }

    @Override
    public Object visitVerifyStmt(NanoSQLParser.VerifyStmtContext ctx) {
        VerifyCommand cmd = new VerifyCommand();
        for (TerminalNode n : ctx.IDENT())
            cmd.addTable(n.getText().toLowerCase());

        return cmd;
    }

    @Override
    public Object visitDumpTableStmt(NanoSQLParser.DumpTableStmtContext ctx) {
        String filename = null;
        if (ctx.fileName != null) {
            filename = ctx.fileName.getText();

            // Remove the quotes from around the filename
            filename = filename.substring(1, filename.length() - 1);
        }

        String format = null;
        if (ctx.format != null) {
            format = ctx.format.getText().toLowerCase();

            // Remove the quotes from around the format
            format = format.substring(1, format.length() - 1);
        }

        return new DumpTableCommand(ctx.tableName.getText().toLowerCase(),
            filename, format);
    }

    @Override
    public Object visitDumpIndexStmt(NanoSQLParser.DumpIndexStmtContext ctx) {
        String filename = null;
        if (ctx.fileName != null) {
            filename = ctx.fileName.getText();

            // Remove the quotes from around the filename
            filename = filename.substring(1, filename.length() - 1);
        }

        String format = null;
        if (ctx.format != null) {
            format = ctx.format.getText().toLowerCase();

            // Remove the quotes from around the format
            format = format.substring(1, format.length() - 1);
        }

        return new DumpIndexCommand(ctx.indexName.getText().toLowerCase(),
            ctx.tableName.getText().toLowerCase(), filename, format);
    }

    @Override
    public Object visitShowTableStatsStmt(NanoSQLParser.ShowTableStatsStmtContext ctx) {
        return new ShowTableStatsCommand(ctx.tableName.getText().toLowerCase());
    }

    //=== DDL OPERATIONS =====================================================

    @Override
    public Object visitCmdProperties(NanoSQLParser.CmdPropertiesContext ctx) {
        CommandProperties cmdProps = new CommandProperties();
        for (int i = 0; i < ctx.IDENT().size(); i++) {
            String name = ctx.IDENT(i).getText().toLowerCase();
            Object value = visit(ctx.literalValue(i));
            cmdProps.set(name, value);
        }
        return cmdProps;
    }

    @Override
    public Object visitCreateTableStmt(NanoSQLParser.CreateTableStmtContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();
        boolean temporary = (ctx.TEMPORARY() != null);
        boolean ifNotExists = (ctx.EXISTS() != null);

        CreateTableCommand cmd = new CreateTableCommand(tableName);
        cmd.setTemporary(temporary);
        cmd.setIfNotExists(ifNotExists);

        for (NanoSQLParser.TableColDeclContext tcdCtx : ctx.tableColDecl()) {
            TableColumnDecl colDecl = (TableColumnDecl) visit(tcdCtx);
            cmd.addColumn(colDecl);
        }

        for (NanoSQLParser.TableConstraintContext tcCtx : ctx.tableConstraint()) {
            ConstraintDecl constraint = (ConstraintDecl) visit(tcCtx);
            cmd.addConstraint(constraint);
        }

        if (ctx.cmdProperties() != null)
            cmd.setProperties((CommandProperties) visit(ctx.cmdProperties()));

        return cmd;
    }

    @Override
    public Object visitTableColDecl(NanoSQLParser.TableColDeclContext ctx) {
        String columnName = ctx.columnName.getText().toLowerCase();
        ColumnType columnType = (ColumnType) visit(ctx.columnType());
        ColumnInfo colInfo = new ColumnInfo(columnName, columnType);

        TableColumnDecl colDecl = new TableColumnDecl(colInfo);
        for (NanoSQLParser.ColumnConstraintContext ccc : ctx.columnConstraint()) {
            ConstraintDecl constraint = (ConstraintDecl) visit(ccc);
            constraint.addColumn(columnName);
            colDecl.addConstraint(constraint);
        }

        return colDecl;
    }

    @Override
    public Object visitColTypeInt(NanoSQLParser.ColTypeIntContext ctx) {
        return ColumnType.INTEGER;
    }

    @Override
    public Object visitColTypeBigInt(NanoSQLParser.ColTypeBigIntContext ctx) {
        return ColumnType.BIGINT;
    }

    @Override
    public Object visitColTypeDecimal(NanoSQLParser.ColTypeDecimalContext ctx) {
        ColumnType colType = new ColumnType(SQLDataType.NUMERIC);

        if (ctx.precision != null)
            colType.setPrecision(Integer.parseInt(ctx.precision.getText()));

        if (ctx.scale != null)
            colType.setScale(Integer.parseInt(ctx.scale.getText()));

        return colType;
    }

    @Override
    public Object visitColTypeFloat(NanoSQLParser.ColTypeFloatContext ctx) {
        return ColumnType.FLOAT;
    }

    @Override
    public Object visitColTypeDouble(NanoSQLParser.ColTypeDoubleContext ctx) {
        return ColumnType.DOUBLE;
    }

    @Override
    public Object visitColTypeChar(NanoSQLParser.ColTypeCharContext ctx) {
        ColumnType colType = new ColumnType(SQLDataType.CHAR);
        colType.setLength(Integer.parseInt(ctx.length.getText()));
        return colType;
    }

    @Override
    public Object visitColTypeVarChar(NanoSQLParser.ColTypeVarCharContext ctx) {
        ColumnType colType = new ColumnType(SQLDataType.VARCHAR);
        colType.setLength(Integer.parseInt(ctx.length.getText()));
        return colType;
    }

    @Override
    public Object visitColTypeDate(NanoSQLParser.ColTypeDateContext ctx) {
        return ColumnType.DATE;
    }

    @Override
    public Object visitColTypeTime(NanoSQLParser.ColTypeTimeContext ctx) {
        return ColumnType.TIME;
    }

    @Override
    public Object visitColTypeDateTime(NanoSQLParser.ColTypeDateTimeContext ctx) {
        return ColumnType.DATETIME;
    }

    @Override
    public Object visitColTypeTimestamp(NanoSQLParser.ColTypeTimestampContext ctx) {
        return ColumnType.TIMESTAMP;
    }

    @Override
    public Object visitColConstraintNotNull(NanoSQLParser.ColConstraintNotNullContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        return new ConstraintDecl(TableConstraintType.NOT_NULL, name, true);
    }

    @Override
    public Object visitColConstraintUnique(NanoSQLParser.ColConstraintUniqueContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        return new ConstraintDecl(TableConstraintType.UNIQUE, name, true);
    }

    @Override
    public Object visitColConstraintPrimaryKey(NanoSQLParser.ColConstraintPrimaryKeyContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        return new ConstraintDecl(TableConstraintType.PRIMARY_KEY, name, true);
    }

    @Override
    public Object visitColConstraintForeignKey(NanoSQLParser.ColConstraintForeignKeyContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        ConstraintDecl decl = new ConstraintDecl(TableConstraintType.FOREIGN_KEY, name, true);
        decl.setRefTable(ctx.refTableName.getText().toLowerCase());
        if (ctx.refColumnName != null)
            decl.addRefColumn(ctx.refColumnName.getText().toLowerCase());

        if (ctx.DELETE() != null)
            decl.setOnDeleteOption((ForeignKeyValueChangeOption) visit(ctx.delOpt));

        if (ctx.UPDATE() != null)
            decl.setOnUpdateOption((ForeignKeyValueChangeOption) visit(ctx.updOpt));

        return decl;
    }

    @Override
    public Object visitTblConstraintUnique(NanoSQLParser.TblConstraintUniqueContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        ConstraintDecl decl = new ConstraintDecl(TableConstraintType.UNIQUE, name, false);
        for (Token colName : ctx.columnName)
            decl.addColumn(colName.getText().toLowerCase());

        return decl;
    }

    @Override
    public Object visitTblConstraintPrimaryKey(NanoSQLParser.TblConstraintPrimaryKeyContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        ConstraintDecl decl = new ConstraintDecl(TableConstraintType.PRIMARY_KEY, name, false);
        for (Token colName : ctx.columnName)
            decl.addColumn(colName.getText().toLowerCase());

        return decl;
    }

    @Override
    public Object visitTblConstraintForeignKey(NanoSQLParser.TblConstraintForeignKeyContext ctx) {
        String name = null;
        if (ctx.constraintName != null)
            name = ctx.constraintName.getText().toLowerCase();

        ConstraintDecl decl = new ConstraintDecl(TableConstraintType.FOREIGN_KEY, name, false);
        for (Token colName : ctx.columnName)
            decl.addColumn(colName.getText().toLowerCase());

        decl.setRefTable(ctx.refTableName.getText().toLowerCase());
        for (Token refColName : ctx.refColumnName)
            decl.addRefColumn(refColName.getText().toLowerCase());

        if (ctx.DELETE() != null)
            decl.setOnDeleteOption((ForeignKeyValueChangeOption) visit(ctx.delOpt));

        if (ctx.UPDATE() != null)
            decl.setOnUpdateOption((ForeignKeyValueChangeOption) visit(ctx.updOpt));

        return decl;
    }

    @Override
    public Object visitCascadeOptRestrict(NanoSQLParser.CascadeOptRestrictContext ctx) {
        return ForeignKeyValueChangeOption.RESTRICT;
    }

    @Override
    public Object visitCascadeOptCascade(NanoSQLParser.CascadeOptCascadeContext ctx) {
        return ForeignKeyValueChangeOption.CASCADE;
    }

    @Override
    public Object visitCascadeOptSetNull(NanoSQLParser.CascadeOptSetNullContext ctx) {
        return ForeignKeyValueChangeOption.SET_NULL;
    }

    @Override
    public Object visitDropTableStmt(NanoSQLParser.DropTableStmtContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();
        boolean ifExists = (ctx.EXISTS() != null);
        return new DropTableCommand(tableName, ifExists);
    }

    @Override
    public Object visitCreateIndexStmt(NanoSQLParser.CreateIndexStmtContext ctx) {
        String indexName = ctx.indexName.getText().toLowerCase();
        String tableName = ctx.tableName.getText().toLowerCase();
        boolean unique = (ctx.UNIQUE() != null);
        boolean ifNotExists = (ctx.EXISTS() != null);

        CreateIndexCommand cmd = new CreateIndexCommand(indexName, tableName, unique);
        cmd.setIfNotExists(ifNotExists);

        for (Token n : ctx.columnName)
            cmd.addColumn(n.getText().toLowerCase());

        if (ctx.cmdProperties() != null)
            cmd.setProperties((CommandProperties) visit(ctx.cmdProperties()));

        return cmd;
    }

    @Override
    public Object visitDropIndexStmt(NanoSQLParser.DropIndexStmtContext ctx) {
        String indexName = ctx.indexName.getText().toLowerCase();
        String tableName = ctx.tableName.getText().toLowerCase();
        boolean ifExists = (ctx.EXISTS() != null);
        return new DropIndexCommand(indexName, tableName, ifExists);
    }

    //=== DML OPERATIONS =====================================================

    @Override
    public Object visitSelectStmt(NanoSQLParser.SelectStmtContext ctx) {
        SelectClause selectClause = new SelectClause();

        selectClause.setDistinct(ctx.DISTINCT() != null);

        for (NanoSQLParser.SelectValueContext svc : ctx.selectValue())
            selectClause.addSelectValue((SelectValue) visit(svc));

        if (ctx.FROM() != null)
            selectClause.setFromClause((FromClause) visit(ctx.fromExpr()));

        if (ctx.WHERE() != null)
            selectClause.setWhereExpr((Expression) visit(ctx.wherePred));

        if (ctx.GROUP() != null) {
            for (NanoSQLParser.ExpressionContext gec : ctx.groupExpr)
                selectClause.addGroupByExpr((Expression) visit(gec));

            if (ctx.HAVING() != null)
                selectClause.setHavingExpr((Expression) visit(ctx.havingPred));
        }

        if (ctx.ORDER() != null) {
            for (NanoSQLParser.OrderByExprContext oc : ctx.orderByExpr())
                selectClause.addOrderByExpr((OrderByExpression) visit(oc));
        }

        if (ctx.LIMIT() != null)
            selectClause.setLimit(Integer.parseInt(ctx.limit.getText()));

        if (ctx.OFFSET() != null)
            selectClause.setOffset(Integer.parseInt(ctx.offset.getText()));

        return new SelectCommand(selectClause);
    }

    @Override
    public Object visitSelectValue(NanoSQLParser.SelectValueContext ctx) {
        Expression e = (Expression) visit(ctx.expression());
        String alias = null;
        if (ctx.alias != null)
            alias = ctx.alias.getText().toLowerCase();

        return new SelectValue(e, alias);
    }

    @Override
    public Object visitJoinTypeInner(NanoSQLParser.JoinTypeInnerContext ctx) {
        return JoinType.INNER;
    }

    @Override
    public Object visitJoinTypeLeftOuter(NanoSQLParser.JoinTypeLeftOuterContext ctx) {
        return JoinType.LEFT_OUTER;
    }

    @Override
    public Object visitJoinTypeRightOuter(NanoSQLParser.JoinTypeRightOuterContext ctx) {
        return JoinType.RIGHT_OUTER;
    }

    @Override
    public Object visitJoinTypeFullOuter(NanoSQLParser.JoinTypeFullOuterContext ctx) {
        return JoinType.FULL_OUTER;
    }

    @Override
    public Object visitFromCrossJoin(NanoSQLParser.FromCrossJoinContext ctx) {
        FromClause lhs = (FromClause) visit(ctx.fromExpr(0));
        FromClause rhs = (FromClause) visit(ctx.fromExpr(1));
        return new FromClause(lhs, rhs, JoinType.CROSS);
    }

    @Override
    public Object visitFromNaturalJoin(NanoSQLParser.FromNaturalJoinContext ctx) {
        FromClause lhs = (FromClause) visit(ctx.fromExpr(0));
        FromClause rhs = (FromClause) visit(ctx.fromExpr(1));

        JoinType type = JoinType.INNER;
        if (ctx.joinType() != null)
            type = (JoinType) visit(ctx.joinType());

        FromClause fc = new FromClause(lhs, rhs, type);
        fc.setConditionType(FromClause.JoinConditionType.NATURAL_JOIN);
        return fc;
    }

    @Override
    public Object visitFromJoinOn(NanoSQLParser.FromJoinOnContext ctx) {
        FromClause lhs = (FromClause) visit(ctx.fromExpr(0));
        FromClause rhs = (FromClause) visit(ctx.fromExpr(1));

        JoinType type = JoinType.INNER;
        if (ctx.joinType() != null)
            type = (JoinType) visit(ctx.joinType());

        FromClause fc = new FromClause(lhs, rhs, type);
        fc.setConditionType(FromClause.JoinConditionType.JOIN_ON_EXPR);
        fc.setOnExpression((Expression) visit(ctx.expression()));
        return fc;
    }

    @Override
    public Object visitFromJoinUsing(NanoSQLParser.FromJoinUsingContext ctx) {
        FromClause lhs = (FromClause) visit(ctx.fromExpr(0));
        FromClause rhs = (FromClause) visit(ctx.fromExpr(1));

        JoinType type = JoinType.INNER;
        if (ctx.joinType() != null)
            type = (JoinType) visit(ctx.joinType());

        FromClause fc = new FromClause(lhs, rhs, type);
        fc.setConditionType(FromClause.JoinConditionType.JOIN_USING);

        for (Token columnName : ctx.columnName)
            fc.addUsingName(columnName.getText().toLowerCase());

        return fc;
    }

    @Override
    public Object visitFromImplicitCrossJoin(NanoSQLParser.FromImplicitCrossJoinContext ctx) {
        FromClause lhs = (FromClause) visit(ctx.fromExpr(0));
        FromClause rhs = (FromClause) visit(ctx.fromExpr(1));
        return new FromClause(lhs, rhs, JoinType.CROSS);
    }

    @Override
    public Object visitFromTable(NanoSQLParser.FromTableContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();

        String alias = null;
        if (ctx.alias != null)
            alias = ctx.alias.getText().toLowerCase();

        return new FromClause(tableName, alias);
    }

    @Override
    public Object visitFromTableFunction(NanoSQLParser.FromTableFunctionContext ctx) {
        FunctionCall functionCall = (FunctionCall) visit(ctx.functionCall());
        String alias = null;
        if (ctx.alias != null)
            alias = ctx.alias.getText().toLowerCase();

        return new FromClause(functionCall, alias);
    }

    @Override
    public Object visitFromNestedSelect(NanoSQLParser.FromNestedSelectContext ctx) {
        SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());
        return new FromClause(selectCmd.getSelectClause(), ctx.alias.getText().toLowerCase());
    }

    @Override
    public Object visitFromParens(NanoSQLParser.FromParensContext ctx) {
        return visit(ctx.fromExpr());
    }

    @Override
    public Object visitOrderByExpr(NanoSQLParser.OrderByExprContext ctx) {
        boolean ascending = true;
        if (ctx.DESC() != null || ctx.DESCENDING() != null)
            ascending = false;

        return new OrderByExpression(
            (Expression) visit(ctx.expression()), ascending);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitInsertStmt(NanoSQLParser.InsertStmtContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();

        ArrayList<String> colNames = new ArrayList<>();
        for (Token cn : ctx.columnName)
            colNames.add(cn.getText().toLowerCase());

        InsertCommand cmd;
        if (ctx.expressionList() != null) {
            ArrayList<Expression> exprs =
                (ArrayList<Expression>) visit(ctx.expressionList());
            cmd = new InsertCommand(tableName, colNames, exprs);
        }
        else {
            SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());
            SelectClause selectClause = selectCmd.getSelectClause();
            cmd = new InsertCommand(tableName, colNames, selectClause);
        }

        return cmd;
    }

    @Override
    public Object visitUpdateStmt(NanoSQLParser.UpdateStmtContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();
        UpdateCommand cmd = new UpdateCommand(tableName);

        for (int i = 0; i < ctx.columnName.size(); i++) {
            String columnName = ctx.columnName.get(i).getText().toLowerCase();
            Expression expr = (Expression) visit(ctx.expression(i));
            cmd.addValue(columnName, expr);
        }

        if (ctx.WHERE() != null)
            cmd.setWhereExpr((Expression) visit(ctx.predicate));

        return cmd;
    }

    @Override
    public Object visitDeleteStmt(NanoSQLParser.DeleteStmtContext ctx) {
        String tableName = ctx.tableName.getText().toLowerCase();
        Expression expr = null;
        if (ctx.WHERE() != null)
            expr = (Expression) visit(ctx.expression());

        return new DeleteCommand(tableName, expr);
    }

    @Override
    public Object visitExplainSelect(NanoSQLParser.ExplainSelectContext ctx) {
        SelectCommand cmdToExplain = (SelectCommand) visit(ctx.selectStmt());
        return new ExplainCommand(cmdToExplain);
    }

    @Override
    public Object visitExplainInsert(NanoSQLParser.ExplainInsertContext ctx) {
        InsertCommand cmdToExplain = (InsertCommand) visit(ctx.insertStmt());
        return new ExplainCommand(cmdToExplain);
    }

    @Override
    public Object visitExplainUpdate(NanoSQLParser.ExplainUpdateContext ctx) {
        UpdateCommand cmdToExplain = (UpdateCommand) visit(ctx.updateStmt());
        return new ExplainCommand(cmdToExplain);
    }

    @Override
    public Object visitExplainDelete(NanoSQLParser.ExplainDeleteContext ctx) {
        DeleteCommand cmdToExplain = (DeleteCommand) visit(ctx.deleteStmt());
        return new ExplainCommand(cmdToExplain);
    }


    /*======================================================================*/
    /* ALL EXPRESSION RULES                                                 */
    /*======================================================================*/


    //=== LITERALS ===========================================================

    @Override
    public Object visitLiteralNull(NanoSQLParser.LiteralNullContext ctx) {
        return null;
    }

    @Override
    public Object visitLiteralTrue(NanoSQLParser.LiteralTrueContext ctx) {
        return Boolean.TRUE;
    }

    @Override
    public Object visitLiteralFalse(NanoSQLParser.LiteralFalseContext ctx) {
        return Boolean.FALSE;
    }

    @Override
    public Object visitLiteralInteger(NanoSQLParser.LiteralIntegerContext ctx) {
        String s = ctx.INT_LITERAL().getText();
        BigInteger bigInt = new BigInteger(s);
        try {
            return bigInt.intValueExact();
        }
        catch (ArithmeticException e) {
            // Too big to fit in an integer.
        }

        try {
            return bigInt.longValueExact();
        }
        catch (ArithmeticException e) {
            // Too big to fit in a long.
        }

        return bigInt;
    }

    @Override
    public Object visitLiteralDecimal(NanoSQLParser.LiteralDecimalContext ctx) {
        return new BigDecimal(ctx.DECIMAL_LITERAL().getText());
    }

    @Override
    public Object visitLiteralString(NanoSQLParser.LiteralStringContext ctx) {
        // Make sure to lop off the quotes at the start and end of the string
        String s = ctx.STRING_LITERAL().getText();
        return s.substring(1, s.length() - 1);
    }

    @Override
    public Object visitLiteralInterval(NanoSQLParser.LiteralIntervalContext ctx) {
        // Get the text.  Chop off the quotes at the start/end of the string
        String s = ctx.STRING_LITERAL().getText();
        s = s.substring(1, s.length() - 1);

        // Parse the string into an interval value.
        return DateTimeUtils.parseInterval(s);
    }

    //=== COLUMN REFERENCES ==================================================

    @Override
    public Object visitColRefTable(NanoSQLParser.ColRefTableContext ctx) {
        return new ColumnName(ctx.tableName.getText().toLowerCase(),
            ctx.columnName.getText().toLowerCase());
    }

    @Override
    public Object visitColRefNoTable(NanoSQLParser.ColRefNoTableContext ctx) {
        return new ColumnName(ctx.columnName.getText().toLowerCase());
    }

    @Override
    public Object visitColRefWildcardTable(NanoSQLParser.ColRefWildcardTableContext ctx) {
        return new ColumnName(ctx.tableName.getText().toLowerCase(), null);
    }

    @Override
    public Object visitColRefWildcardNoTable(NanoSQLParser.ColRefWildcardNoTableContext ctx) {
        return new ColumnName(null, null);
    }

    //=== FUNCTION CALLS =====================================================

    @Override
    public Object visitFunctionCall(NanoSQLParser.FunctionCallContext ctx) {
        String functionName = ctx.functionName.getText().toUpperCase();
        boolean distinct = (ctx.DISTINCT() != null);

        ArrayList<Expression> args = new ArrayList<>();
        for (NanoSQLParser.ExpressionContext ec : ctx.expression())
            args.add((Expression) visit(ec));

        // We handle "DISTINCT" and wildcard arguments in a special way, by
        // modifying the function name to indicate these characteristics.
        if (args.size() == 1) {
            Expression e = args.get(0);
            if (e instanceof ColumnValue) {
                ColumnName colName = ((ColumnValue) e).getColumnName();
                if (colName.isColumnWildcard())
                    functionName += "#STAR";
                else if (distinct)
                    functionName += "#DISTINCT";
            }
        }
        // TODO:  Report error if someone uses DISTINCT with multiple args.

        FunctionCall fnCall = new FunctionCall(functionName, distinct, args);
        if (functionDirectory != null)
            fnCall.resolve(functionDirectory);

        return fnCall;
    }

    //=== EXPRESSION LISTS ===================================================

    @Override
    public Object visitExpressionList(NanoSQLParser.ExpressionListContext ctx) {
        ArrayList<Expression> exprList = new ArrayList<>();
        for (NanoSQLParser.ExpressionContext ec : ctx.expression())
            exprList.add((Expression) visit(ec));

        return exprList;
    }

    //=== EXPRESSIONS ========================================================

    @Override
    public Object visitExprLiteral(NanoSQLParser.ExprLiteralContext ctx) {
        return new LiteralValue(visit(ctx.literalValue()));
    }

    @Override
    public Object visitExprColumnRef(NanoSQLParser.ExprColumnRefContext ctx) {
        return new ColumnValue((ColumnName) visit(ctx.columnRef()));
    }

    @Override
    public Object visitExprFunctionCall(NanoSQLParser.ExprFunctionCallContext ctx) {
        // Function calls are already represented as expressions, so we can
        // just call the functionCall() translation and return its result.
        return visit(ctx.functionCall());
    }

    @Override
    public Object visitExprUnarySign(NanoSQLParser.ExprUnarySignContext ctx) {
        Expression expr = (Expression) visit(ctx.expression());

        if ("-".equals(ctx.op.getText()))
            expr = new NegateOperator(expr);

        return expr;
    }

    @Override
    public Object visitExprMul(NanoSQLParser.ExprMulContext ctx) {
        ArithmeticOperator.Type type =
            ArithmeticOperator.Type.find(ctx.op.getText());

        Expression lhs = (Expression) visit(ctx.expression(0));
        Expression rhs = (Expression) visit(ctx.expression(1));

        return new ArithmeticOperator(type, lhs, rhs);
    }

    @Override
    public Object visitExprAdd(NanoSQLParser.ExprAddContext ctx) {
        String op = ctx.op.getText();
        ArithmeticOperator.Type type = ArithmeticOperator.Type.find(op);

        Expression lhs = (Expression) visit(ctx.expression(0));
        Expression rhs = (Expression) visit(ctx.expression(1));

        return new ArithmeticOperator(type, lhs, rhs);
    }

    @Override
    public Object visitExprCompare(NanoSQLParser.ExprCompareContext ctx) {
        String op = ctx.op.getText();
        CompareOperator.Type type = CompareOperator.Type.find(op);

        Expression lhs = (Expression) visit(ctx.expression(0));
        Expression rhs = (Expression) visit(ctx.expression(1));

        return new CompareOperator(type, lhs, rhs);
    }

    @Override
    public Object visitExprIsNull(NanoSQLParser.ExprIsNullContext ctx) {
        boolean invert = (ctx.NOT() != null);
        return new IsNullOperator((Expression) visit(ctx.expression()), invert);
    }

    @Override
    public Object visitExprBetween(NanoSQLParser.ExprBetweenContext ctx) {
        boolean invert = (ctx.NOT() != null);

        CompareOperator.Type lowOp = CompareOperator.Type.GREATER_OR_EQUAL;
        CompareOperator.Type highOp = CompareOperator.Type.LESS_OR_EQUAL;
        if (invert) {
            lowOp = CompareOperator.Type.LESS_THAN;
            highOp = CompareOperator.Type.GREATER_THAN;
        }

        Expression expr = (Expression) visit(ctx.expression(0));
        Expression lowExpr = (Expression) visit(ctx.expression(1));
        Expression highExpr = (Expression) visit(ctx.expression(2));

        BooleanOperator andExpr = new BooleanOperator(BooleanOperator.Type.AND_EXPR);
        andExpr.addTerm(new CompareOperator(lowOp, expr, lowExpr));

        // Clone the expression before using it again, so that if it is
        // mutated in subsequent steps, we won't get weird side effects.
        expr = expr.duplicate();
        andExpr.addTerm(new CompareOperator(highOp, expr, highExpr));

        return andExpr;
    }

    @Override
    public Object visitExprLike(NanoSQLParser.ExprLikeContext ctx) {
        Expression lhs = (Expression) visit(ctx.expression(0));
        Expression rhs = (Expression) visit(ctx.expression(1));
        return new StringMatchOperator(StringMatchOperator.Type.LIKE, lhs, rhs);
    }

    @Override
    public Object visitExprSimilarTo(NanoSQLParser.ExprSimilarToContext ctx) {
        Expression lhs = (Expression) visit(ctx.expression(0));
        Expression rhs = (Expression) visit(ctx.expression(1));
        return new StringMatchOperator(StringMatchOperator.Type.REGEX, lhs, rhs);
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitExprOneColInValues(NanoSQLParser.ExprOneColInValuesContext ctx) {
        Expression expr = (Expression) visit(ctx.expression());
        ArrayList<Expression> values =
            (ArrayList<Expression>) visit(ctx.expressionList());

        InValuesOperator op = new InValuesOperator(expr, values);
        op.setInvert(ctx.NOT() != null);

        return op;
    }

    @Override
    public Object visitExprOneColInSubquery(NanoSQLParser.ExprOneColInSubqueryContext ctx) {
        Expression expr = (Expression) visit(ctx.expression());
        SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());

        InSubqueryOperator op =
            new InSubqueryOperator(expr, selectCmd.getSelectClause());

        op.setInvert(ctx.NOT() != null);

        return op;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Object visitExprMultiColInSubquery(NanoSQLParser.ExprMultiColInSubqueryContext ctx) {
        ArrayList<Expression> exprList =
            (ArrayList<Expression>) visit(ctx.expressionList());
        SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());

        InSubqueryOperator op =
            new InSubqueryOperator(exprList, selectCmd.getSelectClause());

        op.setInvert(ctx.NOT() != null);

        return op;
    }

    @Override
    public Object visitExprExists(NanoSQLParser.ExprExistsContext ctx) {
        SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());
        return new ExistsOperator(selectCmd.getSelectClause());
    }

    @Override
    public Object visitExprNot(NanoSQLParser.ExprNotContext ctx) {
        BooleanOperator op = new BooleanOperator(BooleanOperator.Type.NOT_EXPR);
        op.addTerm((Expression) visit(ctx.expression()));
        return op;
    }

    @Override
    public Object visitExprAnd(NanoSQLParser.ExprAndContext ctx) {
        BooleanOperator op = new BooleanOperator(BooleanOperator.Type.AND_EXPR);
        op.addTerm((Expression) visit(ctx.expression(0)));
        op.addTerm((Expression) visit(ctx.expression(1)));
        return op;
    }

    @Override
    public Object visitExprOr(NanoSQLParser.ExprOrContext ctx) {
        BooleanOperator op = new BooleanOperator(BooleanOperator.Type.OR_EXPR);
        op.addTerm((Expression) visit(ctx.expression(0)));
        op.addTerm((Expression) visit(ctx.expression(1)));
        return op;
    }

    @Override
    public Object visitExprScalarSubquery(NanoSQLParser.ExprScalarSubqueryContext ctx) {
        SelectCommand selectCmd = (SelectCommand) visit(ctx.selectStmt());
        return new ScalarSubquery(selectCmd.getSelectClause());
    }

    @Override
    public Object visitExprParen(NanoSQLParser.ExprParenContext ctx) {
        return visit(ctx.expression());
    }
}