/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * 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
 *
 *    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.pentaho.di.trans.steps.luciddbstreamingloader;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.SocketException;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.pentaho.di.core.exception.KettleDatabaseException;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.row.ValueMetaInterface;
import org.pentaho.di.core.xml.XMLHandler;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.*;

/**
 * Perform main transformation.<br>
 * The logic is below:<br>
 * 1. Execute remote_rows udx thru jdbc (initialize database and generate sql_statment on remote_rows).<br>
 * 2. When incoming row is the 1st row, cache the index of every field and create header format object.<br>
 * 3. Send out data one row/per time thru socket based on TCP/IP.<br>
 * 4. Once all rows are sent out, close socket connection and return.<br>
 *
 * @author Ray Zhang
 * @since Jan-05-2010
 */
public class LucidDBStreamingLoader extends BaseDatabaseStep implements StepInterface {

  private static Class<?> PKG = LucidDBStreamingLoaderMeta.class;

  private LucidDBStreamingLoaderMeta meta;

  private LucidDBStreamingLoaderData data;

  public LucidDBStreamingLoader( StepMeta stepMeta, StepDataInterface stepDataInterface, int copyNr,
    TransMeta transMeta, Trans trans ) {
    super( stepMeta, stepDataInterface, copyNr, transMeta, trans );
  }

  // When user want to stop it, it will execute logic here.
  public void stopRunning( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException {
    meta = (LucidDBStreamingLoaderMeta) smi;
    data = (LucidDBStreamingLoaderData) sdi;

    if ( data.objOut != null ) {

      try {
        data.objOut.flush();
        data.objOut.close();

      } catch ( IOException e ) {
        // Already closed or other issue... log silent error
        logError( "Error while closing Remote LucidDB connection - likely already closed by earlier exception" );
      } finally {
        data.objOut = null;
      }
      if ( data.client != null ) {

        try {
          data.client.close();
        } catch ( IOException e ) {
          // Already closed or other issue... log silent error
          logError( "Error while closing Remote client connection - likely already closed by earlier exception" );
        } finally {
          data.client = null;
        }
      }
    }
    try {
      if ( data.sqlRunner != null ) {
        data.sqlRunner.join();
      }
    } catch ( InterruptedException e ) {
      // Issue converging thread
      logError( "Error while trying to rejoin/end SQLRunner thread from LucidDB" );
    }
  }

  public boolean processRow( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException {

    meta = (LucidDBStreamingLoaderMeta) smi;
    data = (LucidDBStreamingLoaderData) sdi;

    try {

      Object[] r = getRow(); // Get row from input rowset & set row
      // busy!

      if ( r == null ) { // no more input to be expected...

        if ( data.objOut != null ) {

          data.objOut.flush();
          data.objOut.close();
          if ( data.client != null ) {

            data.client.close();
          }
        }

        return false;
      }

      if ( first ) {

        first = false;

        // For anything other than Custom operations, check to see if the table exists
        if ( meta.getOperation() != LucidDBStreamingLoaderMeta.OPERATION_CUSTOM ) {
          if ( log.isDebug() ) {
            logDebug( "Connected to LucidDB" );
          }
          String qualifiedTableName =
            meta.getDatabaseMeta().getQuotedSchemaTableCombination(
              environmentSubstitute( meta.getSchemaName() ), environmentSubstitute( meta.getTableName() ) );

          if ( !data.db.checkTableExists( qualifiedTableName ) ) {

            throw new KettleException( "Error: Table " + qualifiedTableName + " doesn't existing in LucidDB" );

          }
        }

        String sql = meta.getDMLStatement( this, getInputRowMeta() );
        PreparedStatement ps = data.db.prepareSQL( sql );

        if ( log.isDebug() ) {
          logDebug( "Executing sql statements..." );
        }

        data.sqlRunner = new SqlRunner( data, ps );
        data.sqlRunner.start();

        if ( log.isDebug() ) {
          logDebug( "Remote rows is up now..." );
        }

        if ( log.isDebug() ) {
          logDebug( "Sleeping for 1 second" );
        }
        Thread.sleep( 1000 );

        if ( log.isDebug() ) {
          logDebug( "Initialize local socket connection..." );
        }
        if ( log.isDebug() ) {
          logDebug( "Parameters for socket: Host: " + meta.getHost() + " Port: " + meta.getPort() );
        }
        int try_cnt = 0;
        // Add a check whether remote rows is up.
        // If it is not up, it will sleep 5 second and then try to
        // connect.
        // Totally, we will try 5 times.
        while ( true ) {

          try {

            data.client = new Socket( meta.getHost(), Integer.valueOf( meta.getPort() ) );
            data.objOut = new ObjectOutputStream( data.client.getOutputStream() );

            if ( log.isDebug() ) {
              logDebug( "Local socket connection is ready" );
            }

            break;

          } catch ( SocketException se ) {

            if ( try_cnt < 5 ) {

              logBasic( "Local socket connection is not ready, so try to connect in 5 second" );
              Thread.sleep( 5000 );
              data.client = null;
              try_cnt++;
            } else {

              throw new KettleException( "Fatal Error: Remote_rows UDX can't be connected! Please check..." );
            }

          } catch ( Exception ex ) {

            throw ex;
          }

        }

        // Get combined set of incoming fields, reducing duplicates

        ArrayList<String> combined = new ArrayList<String>();
        // Add all keys
        for ( int i = 0; i < meta.getFieldStreamForKeys().length; i++ ) {
          combined.add( meta.getFieldStreamForKeys()[i] );
        }
        // Add all fields that are NOT already in keys
        for ( int i = 0; i < meta.getFieldStreamForFields().length; i++ ) {
          if ( !meta.isInKeys( meta.getFieldStreamForFields()[i] ) ) {
            combined.add( meta.getFieldStreamForFields()[i] );
          }
        }

        // Get length and create two arrays (data.keynrs and data.format)
        data.keynrs = new int[combined.size()];
        data.format = new String[combined.size()];

        // Iterate over combined set
        for ( int i = 0; i < combined.size(); i++ ) {
          data.keynrs[i] = getInputRowMeta().indexOfValue( combined.get( i ) );

          ValueMetaInterface v = getInputRowMeta().getValueMeta( data.keynrs[i] );

          data.format[i] = meta.getDatabaseMeta().getFieldDefinition( v, null, null, false );

          // data.format[i] = meta.getSQLDataType(getInputRowMeta().getValueMeta(data.keynrs[i]));
        }

        if ( isDetailed() ) {
          logDetailed( Arrays.toString( data.format ) );
        }

        // Create head format object.

        List<Object> header = new ArrayList<Object>();
        header.add( "1" ); // version
        List<String> format = new ArrayList<String>();
        for ( int i = 0; i < data.format.length; i++ ) {
          format.add( data.format[i] );
        }
        header.add( format );

        data.objOut.writeObject( header );
      }

      // End if ( first )

      // If there's been errors in the DML thread (exception with headers, etc)
      if ( data.sqlRunner.ex != null ) {

        throw new KettleException( data.sqlRunner.ex );

      }

      List<Object> entity = new ArrayList<Object>();

      for ( int i = 0; i < data.keynrs.length; i++ ) {

        int index = data.keynrs[i];
        ValueMetaInterface valueMeta = getInputRowMeta().getValueMeta( index );
        Object valueData = r[index];

        // Support NULL values.
        if ( r[i] == null ) {
          entity.add( null );

        } else {

          switch ( valueMeta.getType() ) {
            case ValueMetaInterface.TYPE_NUMBER:
              if ( log.isRowLevel() ) {
                logRowlevel( valueMeta.getNumber( valueData )
                  + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() );
              }
              entity.add( valueMeta.getNumber( valueData ) );

              break;
            case ValueMetaInterface.TYPE_STRING:
              if ( log.isRowLevel() ) {
                logRowlevel( valueMeta.getString( valueData )
                  + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() );
              }
              entity.add( valueMeta.getString( valueData ) );

              break;
            case ValueMetaInterface.TYPE_DATE:

              Date date = valueMeta.getDate( valueData );

              if ( log.isRowLevel() ) {
                logRowlevel( XMLHandler.date2string( date ) + ":" + valueMeta.getLength() );
              }
              java.sql.Date sqlDate = new java.sql.Date( date.getTime() );
              entity.add( sqlDate );

              break;
            case ValueMetaInterface.TYPE_BOOLEAN:
              if ( log.isRowLevel() ) {
                logRowlevel( Boolean.toString( valueMeta.getBoolean( valueData ) ) + ":" + valueMeta.getLength() );
              }
              entity.add( valueMeta.getBoolean( valueData ) );

              break;
            case ValueMetaInterface.TYPE_INTEGER:
              if ( log.isRowLevel() ) {
                logRowlevel( valueMeta.getInteger( valueData )
                  + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() );
              }
              entity.add( valueMeta.getInteger( valueData ) );

              break;
            case ValueMetaInterface.TYPE_BIGNUMBER:
              if ( log.isRowLevel() ) {
                logRowlevel( valueMeta.getBigNumber( valueData )
                  + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() );
              }
              entity.add( valueMeta.getBigNumber( valueData ) );

              break;
            case ValueMetaInterface.TYPE_BINARY:
              if ( log.isRowLevel() ) {
                logRowlevel( valueMeta.getBinary( valueData )
                  + ":" + valueMeta.getLength() + ":" + valueMeta.getTypeDesc() );
              }
              entity.add( valueMeta.getBinary( valueData ) );
              break;
            default:
              // Unknown datatype - it's worth a try?!? ;)
              entity.add( r[i] );
              break;

          }
        }

      }

      data.objOut.writeObject( entity );

      incrementLinesOutput();

      // NG: Are these both necessary?
      data.objOut.reset();
      data.objOut.flush();

      return true;
    } catch ( Exception e ) {
      logError( BaseMessages.getString( PKG, "LucidDBStreamingLoader.Log.ErrorInStep" ), e );
      setErrors( 1 );
      stopAll();
      setOutputDone(); // signal end to receiver(s)
      return false;
    }
  }

  public boolean init( StepMetaInterface smi, StepDataInterface sdi ) {
    meta = (LucidDBStreamingLoaderMeta) smi;
    data = (LucidDBStreamingLoaderData) sdi;
    if ( super.init( smi, sdi ) ) {
      try {
        data.db.setAutoCommit( true );
        return true;
      } catch ( KettleDatabaseException e ) {
        logError( e.getMessage() );
      }
    }
    return false;
  }

  @Override
  protected Class<?> getPKG() {
    return PKG;
  }

  public void dispose( StepMetaInterface smi, StepDataInterface sdi ) {
    meta = (LucidDBStreamingLoaderMeta) smi;
    data = (LucidDBStreamingLoaderData) sdi;

    // Close the output streams if still needed.
    //
    try {
      // Stop the SQL execution thread
      if ( data.sqlRunner != null ) {
        data.sqlRunner.join();
        data.sqlRunner = null;
      }
    } catch ( Exception e ) {
      setErrors( 1L );
      logError( "Unexpected error encountered while closing the client connection", e );
    }

    super.dispose( smi, sdi );
  }

  static class SqlRunner extends Thread {
    private LucidDBStreamingLoaderData data;

    private PreparedStatement ps;

    private SQLException ex;

    List<String> warnings;

    SqlRunner( LucidDBStreamingLoaderData data, PreparedStatement ps ) {
      this.data = data;
      this.ps = ps;
      warnings = new ArrayList<String>();
      ex = null;
    }

    public void run() {
      try {
        // TODO cross-check result against actual
        // number
        // of rows sent.
        ps.executeUpdate();

        // Pump out any warnings and save them.
        SQLWarning warning = ps.getWarnings();
        while ( warning != null ) {
          warnings.add( warning.getMessage() );
          warning = warning.getNextWarning();
        }
      } catch ( SQLException ex ) {
        this.ex = ex;
      } finally {
        try {
          data.db.closePreparedStatement( ps );

        } catch ( KettleException ke ) {
          // not much we can do with this
        } finally {
          ps = null;
        }
      }
    }

    void checkExcn() throws SQLException {
      // This is called from the main thread context to rethrow any
      // saved
      // excn.
      if ( ex != null ) {
        throw ex;
      }
    }
  }
}
