/*
 * 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.math.BigDecimal;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;

import org.antlr.v4.runtime.ParserRuleContext;

public class Cmp implements Runnable {
  
  Exec exec;
  Timer timer = new Timer();  
  boolean trace = false;
  boolean info = false;
  
  Query query;
  String conn;
  HplsqlParser.Cmp_stmtContext ctx;
  
  int tests = 0;
  int failedTests = 0;
  int failedTestsHighDiff = 0;
  int failedTestsHighDiff10 = 0;
  
  Cmp(Exec e) {
    exec = e;  
    trace = exec.getTrace();
    info = exec.getInfo();
  }
  
  Cmp(Exec e, HplsqlParser.Cmp_stmtContext c, Query q, String cn) {
    exec = e;  
    trace = exec.getTrace();
    info = exec.getInfo();
    ctx = c;
    query = q;
    conn = cn;
  }
  
  /**
   * Run CMP command
   */
  Integer run(HplsqlParser.Cmp_stmtContext ctx) {
    trace(ctx, "CMP");
    this.ctx = ctx;
    timer.start();
    StringBuilder conn1 = new StringBuilder();
    StringBuilder conn2 = new StringBuilder();
    Query query1 = new Query();
    Query query2 = new Query();
    Boolean equal = null;
    try {
      String sql1 = getSql(ctx, conn1, 0);
      String sql2 = getSql(ctx, conn2, 1);
      if (trace) {
        trace(ctx, "Query 1: " + sql1);
        trace(ctx, "Query 2: " + sql2);
      }
      query1.setSql(sql1);
      query2.setSql(sql2);
      Cmp cmp1 = new Cmp(exec, ctx, query1, conn1.toString());
      Cmp cmp2 = new Cmp(exec, ctx, query2, conn2.toString());
      Thread t1 = new Thread(cmp1);
      Thread t2 = new Thread(cmp2);
      t1.start();
      t2.start();
      t1.join();
      t2.join();
      equal = compare(query1, query2);      
    }
    catch(Exception e) {
      exec.signal(e);
      return -1;
    }
    finally {
      long elapsed = timer.stop();
      if (info) {
        String message = "CMP ";
        if (equal != null) {
          if (equal) {
            message += "Equal, " + tests + " tests";
          }
          else {
            message += "Not Equal, " + failedTests + " of " + tests + " tests failed";
            message += ", " + failedTestsHighDiff + " tests with more than 0.01% difference";
            message += ", " + failedTestsHighDiff10 + " tests with more than 10% difference";
          }
        }
        else {
          message += "Failed";
        }
        info(ctx, message + ", " + timer.format());
      }
      exec.closeQuery(query1, conn1.toString());
      exec.closeQuery(query2, conn2.toString());
    }
    return 0;
  }
  
  /**
   * Get data for comparison from the source
   */
  public void run() {
    exec.executeQuery(ctx, query, conn);
  }
  
  /**
   * Compare the results
   */
  Boolean compare(Query query1, Query query2) {
    if (query1.error()) { 
      exec.signal(query1);
      return null;
    }
    else if (query2.error()) { 
      exec.signal(query2);
      return null;
    }
    ResultSet rs1 = query1.getResultSet();
    ResultSet rs2 = query2.getResultSet();
    if (rs1 == null || rs2 == null) {
      exec.setSqlCode(-1);
      return null;
    }
    boolean equal = true;
    tests = 0;
    failedTests = 0;
    try {
      ResultSetMetaData rm1 = rs1.getMetaData();
      ResultSetMetaData rm2 = rs2.getMetaData();
      int cnt1 = rm1.getColumnCount();
      int cnt2 = rm2.getColumnCount();
      tests = cnt1;
      while (rs1.next() && rs2.next()) {
        for (int i = 1; i <= tests; i++) {
          Var v1 = new Var(Var.Type.DERIVED_TYPE);
          Var v2 = new Var(Var.Type.DERIVED_TYPE);
          v1.setValue(rs1, rm1, i);
          if (i <= cnt2) {
            v2.setValue(rs2, rm2, i);
          }
          boolean e = true;
          if (!(v1.isNull() && v2.isNull()) && !v1.equals(v2)) {
            equal = false;
            e = false;
            failedTests++;
          }
          if (trace || info) {
            String m = rm1.getColumnName(i) + "\t" + v1.toString() + "\t" + v2.toString();
            if (!e) {
              m += "\tNot equal";
              BigDecimal diff = v1.percentDiff(v2);
              if (diff != null) {
                if (diff.compareTo(BigDecimal.ZERO) != 0) {
                  m += ", " + diff + "% difference";
                  failedTestsHighDiff++;
                  if (diff.compareTo(BigDecimal.TEN) > 0) {
                    failedTestsHighDiff10++;
                  }
                }
                else {
                  m += ", less then 0.01% difference";
                }
              }
              else {
                failedTestsHighDiff++;
                failedTestsHighDiff10++;
              }
            }
            if (trace) {
              trace(null, m);
            }
            else {
              info(null, m);
            }
          }
        }
        if (equal) {
          exec.setSqlSuccess();
        }
        else {
          exec.setSqlCode(1);
        }
      }
    }
    catch(Exception e) {
      exec.signal(e);
      return null;
    }
    return new Boolean(equal);
  }
  
  /**
   * Define the SQL query to access data
   */
  String getSql(HplsqlParser.Cmp_stmtContext ctx, StringBuilder conn, int idx) throws Exception {
    StringBuilder sql = new StringBuilder();
    String table = null;
    String query = null;
    if (ctx.cmp_source(idx).table_name() != null) {
      table = evalPop(ctx.cmp_source(idx).table_name()).toString();
    }
    else {
      query = evalPop(ctx.cmp_source(idx).select_stmt()).toString();
    }
    if (ctx.cmp_source(idx).T_AT() != null) {
      conn.append(ctx.cmp_source(idx).ident().getText());
    }
    else if (table != null) {
      conn.append(exec.getObjectConnection(ctx.cmp_source(idx).table_name().getText()));
    }  
    else {
      conn.append(exec.getStatementConnection());
    }
    sql.append("SELECT ");
    sql.append(getSelectList(ctx, conn.toString(), table, query));
    sql.append(" FROM ");
    if (table != null) {
      sql.append(table);
      if (ctx.cmp_source(idx).where_clause() != null) {
        sql.append(" " + evalPop(ctx.cmp_source(idx).where_clause()).toString());
      }
    }
    else {
      sql.append("(");
      sql.append(query);
      sql.append(") t");
    }
    return sql.toString();
  }
  
  /**
   * Define SELECT listto access data
   */
  String getSelectList(HplsqlParser.Cmp_stmtContext ctx, String conn, String table, String query) throws Exception {
    StringBuilder sql = new StringBuilder();
    sql.append("COUNT(1) AS row_count");
    if (ctx.T_SUM() != null && table != null) {
      Row row = exec.meta.getRowDataType(ctx, conn, table);
      if (row != null) {
        ArrayList<Column> cols = row.getColumns();
        int cnt = row.size();
        sql.append(",\n");
        for (int i = 0; i < cnt; i++) {
          Column col = cols.get(i);
          String name = col.getName();
          Var.Type type = Var.defineType(col.getType());
          sql.append("COUNT(" + name + ") AS " + name + "_COUNT_NOT_NULL");
          if (type == Var.Type.STRING) {
            sql.append(",\n");
            sql.append("SUM(LENGTH(" + name + ")) AS " + name + "_SUM_LENGTH,\n");
            sql.append("MIN(LENGTH(" + name + ")) AS " + name + "_MIN_LENGTH,\n");
            sql.append("MAX(LENGTH(" + name + ")) AS " + name + "_MAX_LENGTH");
          }
          else if (type == Var.Type.BIGINT || type == Var.Type.DECIMAL || type == Var.Type.DOUBLE) {
            sql.append(",\n");
            sql.append("SUM(" + name + ") AS " + name + "_SUM,\n");
            sql.append("MIN(" + name + ") AS " + name + "_MIN,\n");
            sql.append("MAX(" + name + ") AS " + name + "_MAX");
          }
          else if (type == Var.Type.DATE || type == Var.Type.TIMESTAMP) {
            sql.append(",\n");
            sql.append("SUM(YEAR(" + name + ")) AS " + name + "_SUM_YEAR,\n");
            sql.append("SUM(MONTH(" + name + ")) AS " + name + "_SUM_MONTH,\n");
            sql.append("SUM(DAY(" + name + ")) AS " + name + "_SUM_DAY,\n");
            sql.append("MIN(" + name + ") AS " + name + "_MIN,\n");
            sql.append("MAX(" + name + ") AS " + name + "_MAX");
          }
          if (i + 1 < cnt) {
            sql.append(",\n");
          }
        }
      }
    }
    return sql.toString();
  }
  
  /**
   * 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;
  }

  /**
   * Trace and information
   */
  public void trace(ParserRuleContext ctx, String message) {
    exec.trace(ctx, message);
  }
  
  public void info(ParserRuleContext ctx, String message) {
    exec.info(ctx, message);
  }
}
