/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2013, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package ch.qos.logback.classic.db;

import static ch.qos.logback.core.db.DBHelper.closeStatement;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import ch.qos.logback.classic.db.names.DBNameResolver;
import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
import ch.qos.logback.classic.spi.*;
import ch.qos.logback.core.CoreConstants;
import ch.qos.logback.core.db.DBAppenderBase;

/**
 * The DBAppender inserts logging events into three database tables in a format
 * independent of the Java programming language.
 *
 * For more information about this appender, please refer to the online manual
 * at http://logback.qos.ch/manual/appenders.html#DBAppender
 *
 * @author Ceki G&uuml;lc&uuml;
 * @author Ray DeCampo
 * @author S&eacute;bastien Pennec
 */
public class DBAppender extends DBAppenderBase<ILoggingEvent> {
  protected String insertPropertiesSQL;
  protected String insertExceptionSQL;
  protected String insertSQL;
  protected static final Method GET_GENERATED_KEYS_METHOD;

  private DBNameResolver dbNameResolver;

  static final int TIMESTMP_INDEX = 1;
  static final int  FORMATTED_MESSAGE_INDEX  = 2;
  static final int  LOGGER_NAME_INDEX = 3;
  static final int  LEVEL_STRING_INDEX = 4;
  static final int  THREAD_NAME_INDEX = 5;
  static final int  REFERENCE_FLAG_INDEX = 6;
  static final int  ARG0_INDEX = 7;
  static final int  ARG1_INDEX = 8;
  static final int  ARG2_INDEX = 9;
  static final int  ARG3_INDEX = 10;
  static final int  CALLER_FILENAME_INDEX = 11;
  static final int  CALLER_CLASS_INDEX = 12;
  static final int  CALLER_METHOD_INDEX = 13;
  static final int  CALLER_LINE_INDEX = 14;
  static final int  EVENT_ID_INDEX  = 15;

  static final StackTraceElement EMPTY_CALLER_DATA = CallerData.naInstance();

  static {
    // PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
    Method getGeneratedKeysMethod;
    try {
      // the
      getGeneratedKeysMethod = PreparedStatement.class.getMethod(
          "getGeneratedKeys", (Class[]) null);
    } catch (Exception ex) {
      getGeneratedKeysMethod = null;
    }
    GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
  }

  public void setDbNameResolver(DBNameResolver dbNameResolver) {
    this.dbNameResolver = dbNameResolver;
  }

  @Override
  public void start() {
    if (dbNameResolver == null)
      dbNameResolver = new DefaultDBNameResolver();
    insertExceptionSQL = SQLBuilder.buildInsertExceptionSQL(dbNameResolver);
    insertPropertiesSQL = SQLBuilder.buildInsertPropertiesSQL(dbNameResolver);
    insertSQL = SQLBuilder.buildInsertSQL(dbNameResolver);
    super.start();
  }

  @Override
  protected void subAppend(ILoggingEvent event, Connection connection,
      PreparedStatement insertStatement) throws Throwable {

    bindLoggingEventWithInsertStatement(insertStatement, event);
    bindLoggingEventArgumentsWithPreparedStatement(insertStatement, event.getArgumentArray());

    // This is expensive... should we do it every time?
    bindCallerDataWithPreparedStatement(insertStatement, event.getCallerData());

    int updateCount = insertStatement.executeUpdate();
    if (updateCount != 1) {
      addWarn("Failed to insert loggingEvent");
    }
  }

  protected void secondarySubAppend(ILoggingEvent event, Connection connection,
      long eventId) throws Throwable {
    Map<String, String> mergedMap = mergePropertyMaps(event);
    insertProperties(mergedMap, connection, eventId);

    if (event.getThrowableProxy() != null) {
      insertThrowable(event.getThrowableProxy(), connection, eventId);
    }
  }

  void bindLoggingEventWithInsertStatement(PreparedStatement stmt,
      ILoggingEvent event) throws SQLException {
    stmt.setLong(TIMESTMP_INDEX, event.getTimeStamp());
    stmt.setString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
    stmt.setString(LOGGER_NAME_INDEX, event.getLoggerName());
    stmt.setString(LEVEL_STRING_INDEX, event.getLevel().toString());
    stmt.setString(THREAD_NAME_INDEX, event.getThreadName());
    stmt.setShort(REFERENCE_FLAG_INDEX, DBHelper.computeReferenceMask(event));
  }

  void bindLoggingEventArgumentsWithPreparedStatement(PreparedStatement stmt,
      Object[] argArray) throws SQLException {

    int arrayLen = argArray != null ? argArray.length : 0;

    for(int i = 0; i < arrayLen && i < 4; i++) {
      stmt.setString(ARG0_INDEX+i, asStringTruncatedTo254(argArray[i]));
    }
    if(arrayLen < 4) {
      for(int i = arrayLen; i < 4; i++) {
        stmt.setString(ARG0_INDEX+i, null);
      }
    }
  }

  String asStringTruncatedTo254(Object o) {
     String s = null;
     if(o != null) {
         s= o.toString();
     }

    if(s == null) {
      return null;
    }
    if(s.length() <= 254) {
      return s;
    } else {
      return s.substring(0, 254);
    }
  }

  void bindCallerDataWithPreparedStatement(PreparedStatement stmt,
      StackTraceElement[] callerDataArray) throws SQLException {

    StackTraceElement caller = extractFirstCaller(callerDataArray);

    stmt.setString(CALLER_FILENAME_INDEX, caller.getFileName());
    stmt.setString(CALLER_CLASS_INDEX, caller.getClassName());
    stmt.setString(CALLER_METHOD_INDEX, caller.getMethodName());
    stmt.setString(CALLER_LINE_INDEX, Integer.toString(caller.getLineNumber()));
  }

  private StackTraceElement extractFirstCaller(StackTraceElement[] callerDataArray) {
    StackTraceElement caller = EMPTY_CALLER_DATA;
    if(hasAtLeastOneNonNullElement(callerDataArray))
      caller = callerDataArray[0];
    return caller;
  }

  private boolean hasAtLeastOneNonNullElement(StackTraceElement[] callerDataArray) {
    return callerDataArray != null && callerDataArray.length > 0 && callerDataArray[0] != null;
  }

  Map<String, String> mergePropertyMaps(ILoggingEvent event) {
    Map<String, String> mergedMap = new HashMap<String, String>();
    // we add the context properties first, then the event properties, since
    // we consider that event-specific properties should have priority over
    // context-wide properties.
    Map<String, String> loggerContextMap = event.getLoggerContextVO()
        .getPropertyMap();
    Map<String, String> mdcMap = event.getMDCPropertyMap();
    if (loggerContextMap != null) {
      mergedMap.putAll(loggerContextMap);
    }
    if (mdcMap != null) {
      mergedMap.putAll(mdcMap);
    }

    return mergedMap;
  }

  @Override
  protected Method getGeneratedKeysMethod() {
    return GET_GENERATED_KEYS_METHOD;
  }

  @Override
  protected String getInsertSQL() {
    return insertSQL;
  }

  protected void insertProperties(Map<String, String> mergedMap,
      Connection connection, long eventId) throws SQLException {
    Set<String> propertiesKeys = mergedMap.keySet();
    if (propertiesKeys.size() > 0) {
      PreparedStatement insertPropertiesStatement = null;
      try {
        insertPropertiesStatement = connection
          .prepareStatement(insertPropertiesSQL);

        for (String key : propertiesKeys) {
          String value = mergedMap.get(key);

          insertPropertiesStatement.setLong(1, eventId);
          insertPropertiesStatement.setString(2, key);
          insertPropertiesStatement.setString(3, value);

          if (cnxSupportsBatchUpdates) {
            insertPropertiesStatement.addBatch();
          } else {
            insertPropertiesStatement.execute();
          }
        }

        if (cnxSupportsBatchUpdates) {
          insertPropertiesStatement.executeBatch();
        }
      } finally {
        closeStatement(insertPropertiesStatement);
      }
    }
  }

  /**
   * Add an exception statement either as a batch or execute immediately if
   * batch updates are not supported.
   */
  void updateExceptionStatement(PreparedStatement exceptionStatement,
      String txt, short i, long eventId) throws SQLException {
    exceptionStatement.setLong(1, eventId);
    exceptionStatement.setShort(2, i);
    exceptionStatement.setString(3, txt);
    if (cnxSupportsBatchUpdates) {
      exceptionStatement.addBatch();
    } else {
      exceptionStatement.execute();
    }
  }

  short buildExceptionStatement(IThrowableProxy tp, short baseIndex,
      PreparedStatement insertExceptionStatement, long eventId)
      throws SQLException {

    StringBuilder buf = new StringBuilder();
    ThrowableProxyUtil.subjoinFirstLine(buf, tp);
    updateExceptionStatement(insertExceptionStatement, buf.toString(),
        baseIndex++, eventId);

    int commonFrames = tp.getCommonFrames();
    StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
    for (int i = 0; i < stepArray.length - commonFrames; i++) {
      StringBuilder sb = new StringBuilder();
      sb.append(CoreConstants.TAB);
      ThrowableProxyUtil.subjoinSTEP(sb, stepArray[i]);
      updateExceptionStatement(insertExceptionStatement, sb.toString(),
          baseIndex++, eventId);
    }

    if (commonFrames > 0) {
      StringBuilder sb = new StringBuilder();
      sb.append(CoreConstants.TAB).append("... ").append(commonFrames).append(
          " common frames omitted");
      updateExceptionStatement(insertExceptionStatement, sb.toString(),
          baseIndex++, eventId);
    }

    return baseIndex;
  }

  protected void insertThrowable(IThrowableProxy tp, Connection connection,
      long eventId) throws SQLException {

    PreparedStatement exceptionStatement = null;
    try {
      exceptionStatement = connection.prepareStatement(insertExceptionSQL);

      short baseIndex = 0;
      while (tp != null) {
        baseIndex = buildExceptionStatement(tp, baseIndex, exceptionStatement,
            eventId);
        tp = tp.getCause();
      }

      if (cnxSupportsBatchUpdates) {
        exceptionStatement.executeBatch();
      }
    } finally {
      closeStatement(exceptionStatement);
    }

  }
}
