/*******************************************************************************
 * Pentaho Big Data
 * <p>
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 * <p>
 * ******************************************************************************
 * <p>
 * Licensed 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.pentaho.hadoop.shim.common;

import org.pentaho.hadoop.shim.common.invocationhandler.DriverInvocationHandler;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.sql.DatabaseMetaData;
import java.sql.Date;
import java.sql.Driver;
import java.sql.ResultSet;
import java.sql.Statement;

/**
 * DriverProxyInvocationChain is a temporary solution for interacting with Hive drivers. At the time this class was
 * added, many methods from the JDBC API had not yet been implemented and would instead throw SQLExceptions. Also, some
 * methods such as HiveDatabaseMetaData.getTables() did not return any values.  For these reasons, a dynamic proxy chain
 * was put in place, in order to intercept methods that would otherwise not function properly, and instead inject
 * working and/or default functionality.
 * <p>
 * The "chain" part of this class is a result of not having access to all the necessary objects at driver creation time.
 * For this reason, we have to intercept methods that would return such objects, then create and return a proxy to those
 * objects. There are a number of objects and methods to which this applies, so the result is a "chain" of getting
 * access to objects via a proxy, then returning a proxy to those objects, which in turn may return proxied objects for
 * its methods, and so on.
 * <p>
 * The large amount of reflection used here is because not all Hadoop distributions support both Hive and Hive 2. Thus
 * before proxying or anything, we need to make sure we have the classes we need at runtime.
 */
public class DriverProxyInvocationChain {

  public static final String PENTAHO_CURRENT_DBNAME = "pentaho.current.dbname";
  /**
   * The initialized.
   */
  private static boolean initialized = false;

  public static final Date NULL_DATE = new Date( 0 ) {
    private static final long serialVersionUID = 1L;

    @Override
    public String toString() {
      return "NULL";
    }
  };

  /**
   * The hive1 db meta data class.
   */
  protected static Class<? extends DatabaseMetaData> hive1DbMetaDataClass = null;

  /**
   * The hive2 db meta data class.
   */
  protected static Class<? extends DatabaseMetaData> hive2DbMetaDataClass = null;

  /**
   * The hive1 result set class.
   */
  protected static Class<? extends ResultSet> hive1ResultSetClass = null;

  /**
   * The hive2 result set class.
   */
  protected static Class<? extends ResultSet> hive2ResultSetClass = null;

  /**
   * The hive1 client class.
   */
  protected static Class<?> hive1ClientClass = null;

  /**
   * The hive2 client class.
   */
  protected static Class<?> hive2ClientClass = null;

  /**
   * The hive1 statement class.
   */
  protected static Class<? extends Statement> hive1StatementClass = null;

  /**
   * The hive2 statement class.
   */
  protected static Class<? extends Statement> hive2StatementClass = null;

  public static Class<? extends DatabaseMetaData> getHive1DbMetaDataClass() {
    return hive1DbMetaDataClass;
  }

  public static void setHive1DbMetaDataClass(
    Class<? extends DatabaseMetaData> hive1DbMetaDataClass ) {
    DriverProxyInvocationChain.hive1DbMetaDataClass = hive1DbMetaDataClass;
  }

  public static Class<? extends DatabaseMetaData> getHive2DbMetaDataClass() {
    return hive2DbMetaDataClass;
  }

  public static void setHive2DbMetaDataClass(
    Class<? extends DatabaseMetaData> hive2DbMetaDataClass ) {
    DriverProxyInvocationChain.hive2DbMetaDataClass = hive2DbMetaDataClass;
  }

  public static Class<? extends ResultSet> getHive1ResultSetClass() {
    return hive1ResultSetClass;
  }

  public static void setHive1ResultSetClass( Class<? extends ResultSet> hive1ResultSetClass ) {
    DriverProxyInvocationChain.hive1ResultSetClass = hive1ResultSetClass;
  }

  public static Class<? extends ResultSet> getHive2ResultSetClass() {
    return hive2ResultSetClass;
  }

  public static void setHive2ResultSetClass( Class<? extends ResultSet> hive2ResultSetClass ) {
    DriverProxyInvocationChain.hive2ResultSetClass = hive2ResultSetClass;
  }

  public static Class<?> getHive1ClientClass() {
    return hive1ClientClass;
  }

  public static void setHive1ClientClass( Class<?> hive1ClientClass ) {
    DriverProxyInvocationChain.hive1ClientClass = hive1ClientClass;
  }

  public static Class<?> getHive2ClientClass() {
    return hive2ClientClass;
  }

  public static void setHive2ClientClass( Class<?> hive2ClientClass ) {
    DriverProxyInvocationChain.hive2ClientClass = hive2ClientClass;
  }

  public static Class<? extends Statement> getHive1StatementClass() {
    return hive1StatementClass;
  }

  public static void setHive1StatementClass( Class<? extends Statement> hive1StatementClass ) {
    DriverProxyInvocationChain.hive1StatementClass = hive1StatementClass;
  }

  public static Class<? extends Statement> getHive2StatementClass() {
    return hive2StatementClass;
  }

  public static void setHive2StatementClass( Class<? extends Statement> hive2StatementClass ) {
    DriverProxyInvocationChain.hive2StatementClass = hive2StatementClass;
  }

  protected static ClassLoader driverProxyClassLoader = null;

  /**
   * Gets the proxy.
   *
   * @param intf the intf
   * @param obj  the obj
   * @return the proxy
   */
  public static Driver getProxy( Class<? extends Driver> intf, final Driver obj ) {
    return getProxy( intf, obj, DriverInvocationHandler.class );
  }

  /**
   * Gets the proxy.
   *
   * @param intf the intf
   * @param obj  the obj
   * @return the proxy
   */
  public static Driver getProxy( Class<? extends Driver> intf, final Driver obj, Class driverInvocationHandlerClass ) {
    driverProxyClassLoader = obj.getClass().getClassLoader();
    if ( !initialized ) {
      init();
    }

    InvocationHandler invocationHandler = new DriverInvocationHandler( obj );

    try {
      Constructor driverHandler = driverInvocationHandlerClass.getConstructor( Driver.class );
      invocationHandler = (InvocationHandler) driverHandler.newInstance( Driver.class.cast( obj ) );
    } catch ( NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e ) {
      // ignored
    }
    return (Driver) Proxy.newProxyInstance( driverProxyClassLoader, new Class[] { intf }, invocationHandler );
  }

  /**
   * Initializes the Driver proxy chain
   */
  @SuppressWarnings( { "unchecked" } )
  protected static void init() {

    // Get all the Hive 1 and Hive 2 classes we'll need to call methods on later.

    ClassLoader cl = driverProxyClassLoader;

    try {
      hive1DbMetaDataClass = (Class<? extends DatabaseMetaData>) Class.forName(
        "org.apache.hadoop.hive.jdbc.HiveDatabaseMetaData", false, cl );
      hive1ResultSetClass =
        (Class<? extends ResultSet>) Class.forName( "org.apache.hadoop.hive.jdbc.HiveQueryResultSet", false, cl );
      hive1ClientClass = Class.forName( "org.apache.hadoop.hive.service.HiveInterface", false, cl );
      hive1StatementClass =
        (Class<? extends Statement>) Class.forName( "org.apache.hadoop.hive.jdbc.HiveStatement", false, cl );
    } catch ( ClassNotFoundException cnfe ) {
      //ignored
    }

    try {
      hive2DbMetaDataClass =
        (Class<? extends DatabaseMetaData>) Class.forName( "org.apache.hive.jdbc.HiveDatabaseMetaData", false, cl );
      hive2ResultSetClass =
        (Class<? extends ResultSet>) Class.forName( "org.apache.hive.jdbc.HiveQueryResultSet", false, cl );
      hive2ClientClass = Class.forName( "org.apache.hive.service.cli.thrift.TCLIService$Iface", false, cl );
      hive2StatementClass =
        (Class<? extends Statement>) Class.forName( "org.apache.hive.jdbc.HiveStatement", false, cl );
    } catch ( ClassNotFoundException cnfe ) {
      //ignored
    }

    initialized = true;
  }

  protected static boolean isInitialized() {
    return initialized;
  }

  public static void setInitialized( boolean initialized ) {
    DriverProxyInvocationChain.initialized = initialized;
  }
}
