/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2013 by Pentaho : 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.checksum;

import java.security.MessageDigest;
import java.util.zip.Adler32;
import java.util.zip.CRC32;

import org.apache.commons.codec.binary.Hex;
import org.pentaho.di.core.Const;
import org.pentaho.di.core.exception.KettleException;
import org.pentaho.di.core.row.RowDataUtil;
import org.pentaho.di.i18n.BaseMessages;
import org.pentaho.di.trans.Trans;
import org.pentaho.di.trans.TransMeta;
import org.pentaho.di.trans.step.BaseStep;
import org.pentaho.di.trans.step.StepDataInterface;
import org.pentaho.di.trans.step.StepInterface;
import org.pentaho.di.trans.step.StepMeta;
import org.pentaho.di.trans.step.StepMetaInterface;

/**
 * Caculate a checksum for each row.
 *
 * @author Samatar Hassan
 * @since 30-06-2008
 */
public class CheckSum extends BaseStep implements StepInterface {

  private static Class<?> PKG = CheckSumMeta.class; // for i18n purposes, needed by Translator2!!

  private CheckSumMeta meta;

  private CheckSumData data;

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

  public boolean processRow( StepMetaInterface smi, StepDataInterface sdi ) throws KettleException {
    meta = (CheckSumMeta) smi;
    data = (CheckSumData) sdi;

    Object[] r = getRow(); // get row, set busy!
    if ( r == null ) {
      // no more input to be expected...
      setOutputDone();
      return false;
    }

    if ( first ) {
      first = false;

      data.outputRowMeta = getInputRowMeta().clone();
      data.nrInfields = data.outputRowMeta.size();
      meta.getFields( data.outputRowMeta, getStepname(), null, null, this, repository, metaStore );

      if ( meta.getFieldName() == null || meta.getFieldName().length > 0 ) {
        data.fieldnrs = new int[meta.getFieldName().length];

        for ( int i = 0; i < meta.getFieldName().length; i++ ) {
          data.fieldnrs[i] = getInputRowMeta().indexOfValue( meta.getFieldName()[i] );
          if ( data.fieldnrs[i] < 0 ) {
            logError( BaseMessages.getString( PKG, "CheckSum.Log.CanNotFindField", meta.getFieldName()[i] ) );
            throw new KettleException( BaseMessages.getString( PKG, "CheckSum.Log.CanNotFindField", meta
              .getFieldName()[i] ) );
          }
        }
      } else {
        data.fieldnrs = new int[r.length];
        for ( int i = 0; i < r.length; i++ ) {
          data.fieldnrs[i] = i;
        }
      }
      data.fieldnr = data.fieldnrs.length;

      try {
        if ( meta.getCheckSumType().equals( CheckSumMeta.TYPE_MD5 )
          || meta.getCheckSumType().equals( CheckSumMeta.TYPE_SHA1 ) ) {
          data.digest = MessageDigest.getInstance( meta.getCheckSumType() );
        }
      } catch ( Exception e ) {
        throw new KettleException( BaseMessages.getString( PKG, "CheckSum.Error.Digest" ), e );
      }

    } // end if first

    Object[] outputRowData = null;

    try {
      if ( meta.getCheckSumType().equals( CheckSumMeta.TYPE_ADLER32 )
        || meta.getCheckSumType().equals( CheckSumMeta.TYPE_CRC32 ) ) {
        // get checksum
        Long checksum = calculCheckSum( r );
        outputRowData = RowDataUtil.addValueData( r, data.nrInfields, checksum );
      } else {
        // get checksum

        byte[] o = createCheckSum( r );

        switch ( meta.getResultType() ) {
          case CheckSumMeta.result_TYPE_BINARY:
            outputRowData = RowDataUtil.addValueData( r, data.nrInfields, o );
            break;
          case CheckSumMeta.result_TYPE_HEXADECIMAL:
            String hex =
              meta.isCompatibilityMode() ? byteToHexEncode_compatible( o ) : new String( Hex.encodeHex( o ) );
            outputRowData = RowDataUtil.addValueData( r, data.nrInfields, hex );
            break;
          default:
            outputRowData = RowDataUtil.addValueData( r, data.nrInfields, getStringFromBytes( o ) );
            break;
        }
      }

      if ( checkFeedback( getLinesRead() ) ) {
        if ( log.isDetailed() ) {
          logDetailed( BaseMessages.getString( PKG, "CheckSum.Log.LineNumber", Long.toString( getLinesRead() ) ) );
        }
      }

      // add new values to the row.
      putRow( data.outputRowMeta, outputRowData ); // copy row to output
      // rowset(s);
    } catch ( Exception e ) {
      boolean sendToErrorRow = false;
      String errorMessage = null;

      if ( getStepMeta().isDoingErrorHandling() ) {
        sendToErrorRow = true;
        errorMessage = e.toString();
      } else {
        logError( BaseMessages.getString( PKG, "CheckSum.ErrorInStepRunning" ) + e.getMessage() );
        setErrors( 1 );
        stopAll();
        setOutputDone(); // signal end to receiver(s)
        return false;
      }
      if ( sendToErrorRow ) {
        // Simply add this row to the error row
        putError( getInputRowMeta(), r, 1, errorMessage, meta.getResultFieldName(), "CheckSum001" );
      }
    }
    return true;
  }

  private byte[] createCheckSum( Object[] r ) throws Exception {
    StringBuilder Buff = new StringBuilder();

    // Loop through fields
    for ( int i = 0; i < data.fieldnr; i++ ) {
      String fieldvalue = getInputRowMeta().getString( r, data.fieldnrs[i] );
      Buff.append( fieldvalue );
    }

    // Updates the digest using the specified array of bytes
    data.digest.update( Buff.toString().getBytes() );
    // Completes the hash computation by performing final operations such as padding
    byte[] hash = data.digest.digest();
    // After digest has been called, the MessageDigest object is reset to its initialized state

    return hash;
  }

  private static String getStringFromBytes( byte[] bytes ) {
    StringBuilder sb = new StringBuilder();
    for ( int i = 0; i < bytes.length; i++ ) {
      byte b = bytes[i];
      sb.append( 0x00FF & b );
      if ( i + 1 < bytes.length ) {
        sb.append( "-" );
      }
    }
    return sb.toString();
  }

  public String byteToHexEncode_compatible( byte[] in ) {
    if ( in == null ) {
      return null;
    }
    final char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    String hex = new String( in );

    char[] s = hex.toCharArray();
    StringBuilder hexString = new StringBuilder( 2 * s.length );

    for ( int i = 0; i < s.length; i++ ) {
      hexString.append( hexDigits[( s[i] & 0x00F0 ) >> 4] ); // hi nibble
      hexString.append( hexDigits[s[i] & 0x000F] ); // lo nibble
    }

    return hexString.toString();
  }

  private Long calculCheckSum( Object[] r ) throws Exception {
    Long retval;
    StringBuilder Buff = new StringBuilder();

    // Loop through fields
    for ( int i = 0; i < data.fieldnr; i++ ) {
      String fieldvalue = getInputRowMeta().getString( r, data.fieldnrs[i] );
      Buff.append( fieldvalue );
    }

    if ( meta.getCheckSumType().equals( CheckSumMeta.TYPE_CRC32 ) ) {
      CRC32 crc32 = new CRC32();
      crc32.update( Buff.toString().getBytes() );
      retval = new Long( crc32.getValue() );
    } else {
      Adler32 adler32 = new Adler32();
      adler32.update( Buff.toString().getBytes() );
      retval = new Long( adler32.getValue() );
    }

    return retval;
  }

  public boolean init( StepMetaInterface smi, StepDataInterface sdi ) {
    meta = (CheckSumMeta) smi;
    data = (CheckSumData) sdi;

    if ( super.init( smi, sdi ) ) {
      if ( Const.isEmpty( meta.getResultFieldName() ) ) {
        logError( BaseMessages.getString( PKG, "CheckSum.Error.ResultFieldMissing" ) );
        return false;
      }
      return true;
    }
    return false;
  }

}
