package com.palacesun.engine.spy;

import com.palacesun.engine.common.ConnectionInformation;
import com.palacesun.engine.common.P6LogQuery;
import com.palacesun.engine.event.JdbcEventListener;
import com.palacesun.engine.wrapper.ConnectionWrapper;
import java.io.PrintWriter;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Wrapper;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.CommonDataSource;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.PooledConnection;
import javax.sql.XAConnection;
import javax.sql.XADataSource;

public class P6DataSource implements DataSource, ConnectionPoolDataSource, XADataSource, Referenceable, Serializable {
   protected transient CommonDataSource realDataSource;
   protected String rdsName;
   protected transient JdbcEventListenerFactory jdbcEventListenerFactory;

   public P6DataSource() {
   }

   public P6DataSource(DataSource delegate) {
      this.realDataSource = delegate;
   }

   public String getRealDataSource() {
      return this.rdsName;
   }

   public void setRealDataSource(String jndiName) {
      this.rdsName = jndiName;
   }

   protected synchronized void bindDataSource() throws SQLException {
      if (null == this.realDataSource) {
         P6SpyLoadableOptions options = P6SpyOptions.getActiveInstance();
         if (this.rdsName == null) {
            this.rdsName = options.getRealDataSource();
         }

         if (this.rdsName == null) {
            throw new SQLException("P6DataSource: no value for Real Data Source Name, cannot perform jndi lookup");
         } else {
            Hashtable<String, String> env = null;
            String factory;
            if ((factory = options.getJNDIContextFactory()) != null) {
               env = new Hashtable();
               env.put("java.naming.factory.initial", factory);
               String url = options.getJNDIContextProviderURL();
               if (url != null) {
                  env.put("java.naming.provider.url", url);
               }

               String custom = options.getJNDIContextCustom();
               if (custom != null) {
                  env.putAll(this.parseDelimitedString(custom));
               }
            }

            try {
               InitialContext ctx;
               if (env != null) {
                  ctx = new InitialContext(env);
               } else {
                  ctx = new InitialContext();
               }

               this.realDataSource = (CommonDataSource)ctx.lookup(this.rdsName);
            } catch (NamingException e) {
               throw new SQLException("P6DataSource: naming exception during jndi lookup of Real Data Source Name of '" + this.rdsName + "'. " + e.getMessage(), e);
            }

            Map<String, String> props = this.parseDelimitedString(options.getRealDataSourceProperties());
            if (props != null) {
               this.setDataSourceProperties(props);
            }

            if (this.realDataSource == null) {
               throw new SQLException("P6DataSource: jndi lookup for Real Data Source Name of '" + this.rdsName + "' failed, cannot bind named data source.");
            }
         }
      }
   }

   private void setDataSourceProperties(Map<String, String> props) throws SQLException {
      Map<String, String> matchedProps = new HashMap();
      Class<?> klass = this.realDataSource.getClass();

      for(Method method : klass.getMethods()) {
         String methodName = method.getName();
         if (methodName.startsWith("set")) {
            String propertyName = methodName.substring(3).toLowerCase();

            for(Map.Entry<String, String> entry : props.entrySet()) {
               String key = (String)entry.getKey();
               if (key.toLowerCase().equals(propertyName)) {
                  try {
                     String value = (String)entry.getValue();
                     Class<?>[] types = method.getParameterTypes();
                     Class<?> paramType = types[0];
                     if (paramType.isAssignableFrom(String.class)) {
                        P6LogQuery.debug("calling " + methodName + " on DataSource " + this.rdsName + " with " + value);
                        method.invoke(this.realDataSource, value);
                        matchedProps.put(key, value);
                     } else if (paramType.isPrimitive() && Integer.TYPE.equals(paramType)) {
                        P6LogQuery.debug("calling " + methodName + " on DataSource " + this.rdsName + " with " + value);
                        method.invoke(this.realDataSource, Integer.valueOf(value));
                        matchedProps.put(key, value);
                     } else {
                        P6LogQuery.debug("method " + methodName + " on DataSource " + this.rdsName + " matches property " + propertyName + " but expects unsupported type " + paramType.getName());
                        matchedProps.put(key, value);
                     }
                  } catch (IllegalAccessException e) {
                     throw new SQLException("spy.properties file includes datasource property " + key + " for datasource " + this.rdsName + " but access is denied to method " + methodName, e);
                  } catch (InvocationTargetException e) {
                     throw new SQLException("spy.properties file includes datasource property " + key + " for datasource " + this.rdsName + " but call method " + methodName + " fails", e);
                  }
               }
            }
         }
      }

      for(String key : props.keySet()) {
         if (!matchedProps.containsKey(key)) {
            P6LogQuery.debug("spy.properties file includes datasource property " + key + " for datasource " + this.rdsName + " but class " + klass.getName() + " has no method" + " by that name");
         }
      }

   }

   private HashMap<String, String> parseDelimitedString(String delimitedString) {
      if (delimitedString == null) {
         return null;
      } else {
         HashMap<String, String> result = new HashMap();
         StringTokenizer st = new StringTokenizer(delimitedString, ",", false);

         while(st.hasMoreElements()) {
            String pair = st.nextToken();
            StringTokenizer pst = new StringTokenizer(pair, ";", false);
            if (pst.hasMoreElements()) {
               String name = pst.nextToken();
               if (pst.hasMoreElements()) {
                  String value = pst.nextToken();
                  result.put(name, value);
               }
            }
         }

         return result;
      }
   }

   public Reference getReference() throws NamingException {
      Reference reference = new Reference(this.getClass().getName(), P6DataSourceFactory.class.getName(), null);
      reference.add(new StringRefAddr("dataSourceName", this.getRealDataSource()));
      return reference;
   }

   public int getLoginTimeout() throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      return this.realDataSource.getLoginTimeout();
   }

    /**
     * Return the parent Logger of all the Loggers used by this data source. This
     * should be the Logger farthest from the root Logger that is
     * still an ancestor of all of the Loggers used by this data source. Configuring
     * this Logger will affect all of the log messages generated by the data source.
     * In the worst case, this may be the root Logger.
     *
     * @return the parent Logger for this data source
     * @throws SQLFeatureNotSupportedException if the data source does not use
     *                                         {@code java.util.logging}
     * @since 1.7
     */
    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    public void setLoginTimeout(int inVar) throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      this.realDataSource.setLoginTimeout(inVar);
   }

   public PrintWriter getLogWriter() throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      return this.realDataSource.getLogWriter();
   }

   public void setLogWriter(PrintWriter inVar) throws SQLException {
      this.realDataSource.setLogWriter(inVar);
   }

   public Connection getConnection() throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      long start = System.nanoTime();
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      JdbcEventListener jdbcEventListener = this.jdbcEventListenerFactory.createJdbcEventListener();
      ConnectionInformation connectionInformation = ConnectionInformation.fromDataSource(this.realDataSource);

      try {
         jdbcEventListener.onBeforeGetConnection(connectionInformation);
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
         throw new SQLException(e);
      }

      Connection conn;
      try {
         conn = ((DataSource)this.realDataSource).getConnection();
         connectionInformation.setConnection(conn);
         if (conn.getMetaData() != null) {
            connectionInformation.setUrl(conn.getMetaData().getURL());
         }

         connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
         jdbcEventListener.onAfterGetConnection(connectionInformation, (SQLException)null);
      } catch (SQLException e) {
         connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
         jdbcEventListener.onAfterGetConnection(connectionInformation, e);
         throw e;
      }

      try {
         return ConnectionWrapper.wrap(conn, jdbcEventListener, connectionInformation);
      } catch (Exception e) {
         throw new SQLException(e);
      }
   }

   public Connection getConnection(String username, String password) throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      long start = System.nanoTime();
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      JdbcEventListener jdbcEventListener = this.jdbcEventListenerFactory.createJdbcEventListener();
      ConnectionInformation connectionInformation = ConnectionInformation.fromDataSource(this.realDataSource);

      try {
         jdbcEventListener.onBeforeGetConnection(connectionInformation);
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
         throw new SQLException(e);
      }

      Connection conn;
      try {
         conn = ((DataSource)this.realDataSource).getConnection(username, password);
         connectionInformation.setConnection(conn);
         connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
         jdbcEventListener.onAfterGetConnection(connectionInformation, (SQLException)null);
      } catch (SQLException e) {
         connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
         jdbcEventListener.onAfterGetConnection(connectionInformation, e);
         throw e;
      }

      try {
         return ConnectionWrapper.wrap(conn, jdbcEventListener, connectionInformation);
      } catch (Exception e) {
         throw new SQLException(e);
      }
   }

   public boolean isWrapperFor(Class<?> iface) throws SQLException {
      return ((Wrapper)this.realDataSource).isWrapperFor(iface);
   }

   public <T> T unwrap(Class<T> iface) throws SQLException {
      return (T)((DataSource)this.realDataSource).unwrap(iface);
   }

   public PooledConnection getPooledConnection() throws SQLException {
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      return new P6XAConnection(((ConnectionPoolDataSource)this.castRealDS(ConnectionPoolDataSource.class)).getPooledConnection(), this.jdbcEventListenerFactory);
   }

   public PooledConnection getPooledConnection(String user, String password) throws SQLException {
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      return new P6XAConnection(((ConnectionPoolDataSource)this.castRealDS(ConnectionPoolDataSource.class)).getPooledConnection(user, password), this.jdbcEventListenerFactory);
   }

   public XAConnection getXAConnection() throws SQLException {
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      return new P6XAConnection(((XADataSource)this.castRealDS(XADataSource.class)).getXAConnection(), this.jdbcEventListenerFactory);
   }

   public XAConnection getXAConnection(String user, String password) throws SQLException {
      if (this.jdbcEventListenerFactory == null) {
         this.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
      }

      return new P6XAConnection(((XADataSource)this.castRealDS(XADataSource.class)).getXAConnection(user, password), this.jdbcEventListenerFactory);
   }

   <T> T castRealDS(Class<T> iface) throws SQLException {
      if (this.realDataSource == null) {
         this.bindDataSource();
      }

      if (iface.isInstance(this.realDataSource)) {
         return (T)this.realDataSource;
      } else if (this.isWrapperFor(iface)) {
         return (T)this.unwrap(iface);
      } else {
         throw new IllegalStateException("realdatasource type not supported: " + this.realDataSource);
      }
   }

   public void setJdbcEventListenerFactory(JdbcEventListenerFactory jdbcEventListenerFactory) {
      this.jdbcEventListenerFactory = jdbcEventListenerFactory;
   }
}
