/*
 * 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.service.cli;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.apache.hive.service.rpc.thrift.TStatus;
import org.apache.hive.service.rpc.thrift.TStatusCode;

/**
 * HiveSQLException.
 *
 */
public class HiveSQLException extends SQLException {

  /**
   *
   */
  private static final long serialVersionUID = -6095254671958748094L;

  /**
   *
   */
  public HiveSQLException() {
    super();
  }

  /**
   * @param reason
   */
  public HiveSQLException(String reason) {
    super(reason);
  }

  /**
   * @param cause
   */
  public HiveSQLException(Throwable cause) {
    super(cause);
  }

  /**
   * @param reason
   * @param sqlState
   */
  public HiveSQLException(String reason, String sqlState) {
    super(reason, sqlState);
  }

  /**
   * @param reason
   * @param cause
   */
  public HiveSQLException(String reason, Throwable cause) {
    super(reason, cause);
  }

  /**
   * @param reason
   * @param sqlState
   * @param vendorCode
   */
  public HiveSQLException(String reason, String sqlState, int vendorCode) {
    super(reason, sqlState, vendorCode);
  }

  /**
   * @param reason
   * @param sqlState
   * @param cause
   */
  public HiveSQLException(String reason, String sqlState, Throwable cause) {
    super(reason, sqlState, cause);
  }

  /**
   * @param reason
   * @param sqlState
   * @param vendorCode
   * @param cause
   */
  public HiveSQLException(String reason, String sqlState, int vendorCode, Throwable cause) {
    super(reason, sqlState, vendorCode, cause);
  }

  public HiveSQLException(TStatus status) {
    // TODO: set correct vendorCode field
    super(status.getErrorMessage(), status.getSqlState(), status.getErrorCode());
    if (status.getInfoMessages() != null) {
      initCause(toCause(status.getInfoMessages()));
    }
  }

  /**
   * Converts current object to a {@link TStatus} object
   * @return a {@link TStatus} object
   */
  public TStatus toTStatus() {
    // TODO: convert sqlState, etc.
    TStatus tStatus = new TStatus(TStatusCode.ERROR_STATUS);
    tStatus.setSqlState(getSQLState());
    tStatus.setErrorCode(getErrorCode());
    tStatus.setErrorMessage(getMessage());
    tStatus.setInfoMessages(toString(this));
    return tStatus;
  }

  /**
   * Converts the specified {@link Exception} object into a {@link TStatus} object
   * @param e a {@link Exception} object
   * @return a {@link TStatus} object
   */
  public static TStatus toTStatus(Exception e) {
    if (e instanceof HiveSQLException) {
      return ((HiveSQLException)e).toTStatus();
    }
    TStatus tStatus = new TStatus(TStatusCode.ERROR_STATUS);
    tStatus.setErrorMessage(e.getMessage());
    tStatus.setInfoMessages(toString(e));
    return tStatus;
  }

  /**
   * Converts a {@link Throwable} object into a flattened list of texts including its stack trace
   * and the stack traces of the nested causes.
   * @param ex  a {@link Throwable} object
   * @return    a flattened list of texts including the {@link Throwable} object's stack trace
   *            and the stack traces of the nested causes.
   */
  public static List<String> toString(Throwable ex) {
    return toString(ex, null);
  }

  private static List<String> toString(Throwable cause, StackTraceElement[] parent) {
    StackTraceElement[] trace = cause.getStackTrace();
    int m = trace.length - 1;
    if (parent != null) {
      int n = parent.length - 1;
      while (m >= 0 && n >= 0 && trace[m].equals(parent[n])) {
        m--;
        n--;
      }
    }
    List<String> detail = enroll(cause, trace, m);
    cause = cause.getCause();
    if (cause != null) {
      detail.addAll(toString(cause, trace));
    }
    return detail;
  }

  private static List<String> enroll(Throwable ex, StackTraceElement[] trace, int max) {
    List<String> details = new ArrayList<String>();
    StringBuilder builder = new StringBuilder();
    builder.append('*').append(ex.getClass().getName()).append(':');
    builder.append(ex.getMessage()).append(':');
    builder.append(trace.length).append(':').append(max);
    details.add(builder.toString());
    for (int i = 0; i <= max; i++) {
      builder.setLength(0);
      builder.append(trace[i].getClassName()).append(':');
      builder.append(trace[i].getMethodName()).append(':');
      String fileName = trace[i].getFileName();
      builder.append(fileName == null ? "" : fileName).append(':');
      builder.append(trace[i].getLineNumber());
      details.add(builder.toString());
    }
    return details;
  }

  /**
   * Converts a flattened list of texts including the stack trace and the stack
   * traces of the nested causes into a {@link Throwable} object.
   * @param details a flattened list of texts including the stack trace and the stack
   *                traces of the nested causes
   * @return        a {@link Throwable} object
   */
  public static Throwable toCause(List<String> details) {
    return toStackTrace(details, null, 0);
  }

  private static Throwable toStackTrace(List<String> details, StackTraceElement[] parent, int index) {
    String detail = details.get(index++);
    if (!detail.startsWith("*")) {
      return null;  // should not be happened. ignore remaining
    }
    int i1 = detail.indexOf(':');
    int i3 = detail.lastIndexOf(':');
    int i2 = detail.substring(0, i3).lastIndexOf(':');
    String exceptionClass = detail.substring(1, i1);
    String exceptionMessage = detail.substring(i1 + 1, i2);
    Throwable ex = newInstance(exceptionClass, exceptionMessage);

    Integer length = Integer.valueOf(detail.substring(i2 + 1, i3));
    Integer unique = Integer.valueOf(detail.substring(i3 + 1));

    int i = 0;
    StackTraceElement[] trace = new StackTraceElement[length];
    for (; i <= unique; i++) {
      detail = details.get(index++);
      int j1 = detail.indexOf(':');
      int j3 = detail.lastIndexOf(':');
      int j2 = detail.substring(0, j3).lastIndexOf(':');
      String className = detail.substring(0, j1);
      String methodName = detail.substring(j1 + 1, j2);
      String fileName = detail.substring(j2 + 1, j3);
      if (fileName.isEmpty()) {
        fileName = null;
      }
      int lineNumber = Integer.valueOf(detail.substring(j3 + 1));
      trace[i] = new StackTraceElement(className, methodName, fileName, lineNumber);
    }
    int common = trace.length - i;
    if (common > 0) {
      System.arraycopy(parent, parent.length - common, trace, trace.length - common, common);
    }
    if (details.size() > index) {
      ex.initCause(toStackTrace(details, trace, index));
    }
    ex.setStackTrace(trace);
    return ex;
  }

  private static Throwable newInstance(String className, String message) {
    try {
      return (Throwable)Class.forName(className).getConstructor(String.class).newInstance(message);
    } catch (Exception e) {
      return new RuntimeException(className + ":" + message);
    }
  }
}
