/**
 * 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.
 */

package org.apache.hive.hplsql;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Stack;

import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.Interval;

public class Select {

  Exec exec = null;
  Stack<Var> stack = null;
  Conf conf;
  
  boolean trace = false; 
  
  Select(Exec e) {
    exec = e;  
    stack = exec.getStack();
    conf = exec.getConf();
    trace = exec.getTrace();
  }
   
  /**
   * Executing or building SELECT statement
   */
  public Integer select(HplsqlParser.Select_stmtContext ctx) { 
    if (ctx.parent instanceof HplsqlParser.StmtContext) {
      exec.stmtConnList.clear();
      trace(ctx, "SELECT");
    }
    boolean oldBuildSql = exec.buildSql; 
    exec.buildSql = true;
    StringBuilder sql = new StringBuilder();
    if (ctx.cte_select_stmt() != null) {
      sql.append(evalPop(ctx.cte_select_stmt()).toString());
      sql.append("\n");
    }
    sql.append(evalPop(ctx.fullselect_stmt()).toString());
    exec.buildSql = oldBuildSql;
    if (!(ctx.parent instanceof HplsqlParser.StmtContext)) {           // No need to execute at this stage
      exec.stackPush(sql);
      return 0;  
    }    
    if (trace && ctx.parent instanceof HplsqlParser.StmtContext) {
      trace(ctx, sql.toString());
    }
    if (exec.getOffline()) {
      trace(ctx, "Not executed - offline mode set");
      return 0;
    }
    String conn = exec.getStatementConnection();
    Query query = exec.executeQuery(ctx, sql.toString(), conn);
    if (query.error()) { 
      exec.signal(query);
      return 1;
    }
    trace(ctx, "SELECT completed successfully");
    exec.setSqlSuccess();
    try {
      ResultSet rs = query.getResultSet();
      ResultSetMetaData rm = null;
      if (rs != null) {
        rm = rs.getMetaData();
      }
      int into_cnt = getIntoCount(ctx);
      if (into_cnt > 0) {
        trace(ctx, "SELECT INTO statement executed");
        if (rs.next()) {
          for (int i = 1; i <= into_cnt; i++) {
            String into_name = getIntoVariable(ctx, i - 1);
            Var var = exec.findVariable(into_name);
            if (var != null) {
              if (var.type != Var.Type.ROW) {
                var.setValue(rs, rm, i);
              }
              else {
                var.setValues(rs, rm);
              }
              if (trace) {
                trace(ctx, var, rs, rm, i);
              }
            } 
            else {
              trace(ctx, "Variable not found: " + into_name);
            }
          }
          exec.incRowCount();
          exec.setSqlSuccess();
        }
        else {
          exec.setSqlCode(100);
          exec.signal(Signal.Type.NOTFOUND);
        }
      }
      // Print all results for standalone SELECT statement
      else if (ctx.parent instanceof HplsqlParser.StmtContext) {
        int cols = rm.getColumnCount();
        if (trace) {
          trace(ctx, "Standalone SELECT executed: " + cols + " columns in the result set");
        }
        while (rs.next()) {
          for (int i = 1; i <= cols; i++) {
            if (i > 1) {
              System.out.print("\t");
            }
            System.out.print(rs.getString(i));
          }
          System.out.println("");
          exec.incRowCount();
        }
      }
      // Scalar subquery
      else {
        trace(ctx, "Scalar subquery executed, first row and first column fetched only");
        if(rs.next()) {
          exec.stackPush(new Var().setValue(rs, rm, 1));
          exec.setSqlSuccess();          
        }
        else {
          evalNull();
          exec.setSqlCode(100);
        }
      }
    }
    catch (SQLException e) {
      exec.signal(query);
      exec.closeQuery(query, conn);
      return 1;
    }
    exec.closeQuery(query, conn);
    return 0; 
  }  

  /**
   * Common table expression (WITH clause)
   */
  public Integer cte(HplsqlParser.Cte_select_stmtContext ctx) {
    int cnt = ctx.cte_select_stmt_item().size();
    StringBuilder sql = new StringBuilder();
    sql.append("WITH ");
    for (int i = 0; i < cnt; i++) {
      HplsqlParser.Cte_select_stmt_itemContext c = ctx.cte_select_stmt_item(i);      
      sql.append(c.ident().getText());
      if (c.cte_select_cols() != null) {
        sql.append(" " + exec.getFormattedText(c.cte_select_cols()));
      }
      sql.append(" AS (");
      sql.append(evalPop(ctx.cte_select_stmt_item(i).fullselect_stmt()).toString());
      sql.append(")");
      if (i + 1 != cnt) {
        sql.append(",\n");
      }
    }
    exec.stackPush(sql);
    return 0;
  }
  
  /**
   * Part of SELECT
   */
  public Integer fullselect(HplsqlParser.Fullselect_stmtContext ctx) { 
    int cnt = ctx.fullselect_stmt_item().size();
    StringBuilder sql = new StringBuilder();
    for (int i = 0; i < cnt; i++) {
      String part = evalPop(ctx.fullselect_stmt_item(i)).toString(); 
      sql.append(part);
      if (i + 1 != cnt) {
        sql.append("\n" + getText(ctx.fullselect_set_clause(i)) + "\n");
      }
    }
    exec.stackPush(sql);
    return 0; 
  }
  
  public Integer subselect(HplsqlParser.Subselect_stmtContext ctx) {
    StringBuilder sql = new StringBuilder();
    sql.append(ctx.start.getText());
    exec.append(sql, evalPop(ctx.select_list()).toString(), ctx.start, ctx.select_list().getStart());
    Token last = ctx.select_list().stop;
    if (ctx.into_clause() != null) {
      last = ctx.into_clause().stop;
    }
    if (ctx.from_clause() != null) {
      exec.append(sql, evalPop(ctx.from_clause()).toString(), last, ctx.from_clause().getStart());
      last = ctx.from_clause().stop;
    } 
    else if (conf.dualTable != null) {
      sql.append(" FROM " + conf.dualTable);
    }
    if (ctx.where_clause() != null) {
      exec.append(sql, evalPop(ctx.where_clause()).toString(), last, ctx.where_clause().getStart());
      last = ctx.where_clause().stop;
    }
    if (ctx.group_by_clause() != null) {
      exec.append(sql, getText(ctx.group_by_clause()), last, ctx.group_by_clause().getStart());
      last = ctx.group_by_clause().stop;
    }
    if (ctx.having_clause() != null) {
      exec.append(sql, getText(ctx.having_clause()), last, ctx.having_clause().getStart());
      last = ctx.having_clause().stop;
    }
    if (ctx.qualify_clause() != null) {
      exec.append(sql, getText(ctx.qualify_clause()), last, ctx.qualify_clause().getStart());
      last = ctx.qualify_clause().stop;
    }
    if (ctx.order_by_clause() != null) {
      exec.append(sql, getText(ctx.order_by_clause()), last, ctx.order_by_clause().getStart());
      last = ctx.order_by_clause().stop;
    }
    if (ctx.select_options() != null) {
      Var opt = evalPop(ctx.select_options());
      if (!opt.isNull()) {
        sql.append(" " + opt.toString());
      }
    }
    if (ctx.select_list().select_list_limit() != null) {
      sql.append(" LIMIT " + evalPop(ctx.select_list().select_list_limit().expr()));
    }
    exec.stackPush(sql);
    return 0; 
  }
  
  /**
   * SELECT list 
   */
  public Integer selectList(HplsqlParser.Select_listContext ctx) { 
    StringBuilder sql = new StringBuilder();
    if (ctx.select_list_set() != null) {
      sql.append(exec.getText(ctx.select_list_set())).append(" ");
    }
    int cnt = ctx.select_list_item().size();
    for (int i = 0; i < cnt; i++) {
      if (ctx.select_list_item(i).select_list_asterisk() == null) {
        sql.append(evalPop(ctx.select_list_item(i).expr()));
        if (ctx.select_list_item(i).select_list_alias() != null) {
          sql.append(" " + exec.getText(ctx.select_list_item(i).select_list_alias()));
        }
      }
      else {
        sql.append(exec.getText(ctx.select_list_item(i).select_list_asterisk()));
      }
      if (i + 1 < cnt) {
        sql.append(", ");
      }
    }
    exec.stackPush(sql);
    return 0;
  }

  /**
   * FROM clause
   */
  public Integer from(HplsqlParser.From_clauseContext ctx) { 
    StringBuilder sql = new StringBuilder();
    sql.append(ctx.T_FROM().getText()).append(" ");
    sql.append(evalPop(ctx.from_table_clause()));
    int cnt = ctx.from_join_clause().size();
    for (int i = 0; i < cnt; i++) {
      sql.append(evalPop(ctx.from_join_clause(i)));
    }
    exec.stackPush(sql);
    return 0;
  }
  
  /**
   * Single table name in FROM
   */
  public Integer fromTable(HplsqlParser.From_table_name_clauseContext ctx) {     
    StringBuilder sql = new StringBuilder();
    sql.append(evalPop(ctx.table_name()));
    if (ctx.from_alias_clause() != null) {
      sql.append(" ").append(exec.getText(ctx.from_alias_clause()));
    }
    exec.stackPush(sql);
    return 0; 
  }
  
  /**
   * Subselect in FROM
   */
  public Integer fromSubselect(HplsqlParser.From_subselect_clauseContext ctx) {     
    StringBuilder sql = new StringBuilder();
    sql.append("(");
    sql.append(evalPop(ctx.select_stmt()).toString());
    sql.append(")");
    if (ctx.from_alias_clause() != null) {
      sql.append(" ").append(exec.getText(ctx.from_alias_clause()));
    }
    exec.stackPush(sql);
    return 0; 
  }
 
  /**
   * JOIN clause in FROM
   */
  public Integer fromJoin(HplsqlParser.From_join_clauseContext ctx) {
    StringBuilder sql = new StringBuilder();
    if (ctx.T_COMMA() != null) {
      sql.append(", ");
      sql.append(evalPop(ctx.from_table_clause()));
    }
    else if (ctx.from_join_type_clause() != null) {
      sql.append(" ");
      sql.append(exec.getText(ctx.from_join_type_clause()));
      sql.append(" ");
      sql.append(evalPop(ctx.from_table_clause()));
      sql.append(" ");
      sql.append(exec.getText(ctx, ctx.T_ON().getSymbol(), ctx.bool_expr().getStop()));
    }
    exec.stackPush(sql);
    return 0; 
  }

  /**
   * FROM TABLE (VALUES ...) clause
   */
  public Integer fromTableValues(HplsqlParser.From_table_values_clauseContext ctx) {
    StringBuilder sql = new StringBuilder();
    int rows = ctx.from_table_values_row().size();
    sql.append("(");
    for (int i = 0; i < rows; i++) {
      int cols = ctx.from_table_values_row(i).expr().size();
      int cols_as = ctx.from_alias_clause().L_ID().size();
      sql.append("SELECT ");
      for (int j = 0; j < cols; j++) {
        sql.append(evalPop(ctx.from_table_values_row(i).expr(j)));
        if (j < cols_as) {
          sql.append(" AS ");
          sql.append(ctx.from_alias_clause().L_ID(j));  
        }
        if (j + 1 < cols) {
          sql.append(", ");
        }
      }
      if (conf.dualTable != null) {
        sql.append(" FROM " + conf.dualTable);
      }
      if (i + 1 < rows) {
        sql.append("\nUNION ALL\n");
      }
    }
    sql.append(") ");
    if (ctx.from_alias_clause() != null) {
      sql.append(ctx.from_alias_clause().ident().getText());
    }
    exec.stackPush(sql);
    return 0; 
  }
  
  /**
   * WHERE clause
   */
  public Integer where(HplsqlParser.Where_clauseContext ctx) { 
    boolean oldBuildSql = exec.buildSql; 
    exec.buildSql = true;
    StringBuilder sql = new StringBuilder();
    sql.append(ctx.T_WHERE().getText());
    sql.append(" " + evalPop(ctx.bool_expr()));
    exec.stackPush(sql);
    exec.buildSql = oldBuildSql;
    return 0;
  }
  
  /**
   * Get INTO clause
   */
  HplsqlParser.Into_clauseContext getIntoClause(HplsqlParser.Select_stmtContext ctx) {
    if (ctx.fullselect_stmt().fullselect_stmt_item(0).subselect_stmt() != null) {
      return ctx.fullselect_stmt().fullselect_stmt_item(0).subselect_stmt().into_clause();
    }
    return null;
  }
  
  /**
   * Get number of elements in INTO or var=col assignment clause
   */
  int getIntoCount(HplsqlParser.Select_stmtContext ctx) {
    HplsqlParser.Into_clauseContext into = getIntoClause(ctx);
    if (into != null) {
      return into.ident().size();
    }
    List<HplsqlParser.Select_list_itemContext> sl = ctx.fullselect_stmt().fullselect_stmt_item(0).subselect_stmt().select_list().select_list_item(); 
    if (sl.get(0).T_EQUAL() != null) {
      return sl.size();
    }
    return 0;
  }
  
  /**
   * Get variable name assigned in INTO or var=col clause by index 
   */
  String getIntoVariable(HplsqlParser.Select_stmtContext ctx, int idx) {
    HplsqlParser.Into_clauseContext into = getIntoClause(ctx);
    if (into != null) {
      return into.ident(idx).getText();
    }
    HplsqlParser.Select_list_itemContext sl = ctx.fullselect_stmt().fullselect_stmt_item(0).subselect_stmt().select_list().select_list_item(idx); 
    if (sl != null) {
      return sl.ident().getText();
    }
    return null;
  }
  
  /** 
   * SELECT statement options - LIMIT n, WITH UR i.e
   */
  public Integer option(HplsqlParser.Select_options_itemContext ctx) { 
    if (ctx.T_LIMIT() != null) {
      exec.stackPush("LIMIT " + evalPop(ctx.expr()));
    }
    return 0; 
  }
  
  /**
   * Evaluate the expression to NULL
   */
  void evalNull() {
    exec.stackPush(Var.Null); 
  }

  /**
   * Evaluate the expression and pop value from the stack
   */
  Var evalPop(ParserRuleContext ctx) {
    exec.visit(ctx);
    if (!exec.stack.isEmpty()) { 
      return exec.stackPop();
    }
    return Var.Empty;
  }

  /**
   * Get node text including spaces
   */
  String getText(ParserRuleContext ctx) {
    return ctx.start.getInputStream().getText(new Interval(ctx.start.getStartIndex(), ctx.stop.getStopIndex()));
  }
  
  /**
   * Execute rules
   */
  Integer visit(ParserRuleContext ctx) {
    return exec.visit(ctx);  
  } 
  
  /**
   * Execute children rules
   */
  Integer visitChildren(ParserRuleContext ctx) {
    return exec.visitChildren(ctx);  
  }  
  
  /**
   * Trace information
   */
  void trace(ParserRuleContext ctx, String message) {
    exec.trace(ctx, message);
  }
  
  void trace(ParserRuleContext ctx, Var var, ResultSet rs, ResultSetMetaData rm, int idx) throws SQLException {
    exec.trace(ctx, var, rs, rm, idx);
  }    
}