/*=========================================================================

  Copyright (c) Kitware, Inc.
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/VolViewCopyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=========================================================================*/
#include "vtkVVLODDataItemVolumeHelper.h"

#include "vtkObjectFactory.h"
#include "vtkImageData.h"
#include "vtkVVDataItemVolume.h"
#include "vtkImageReslice.h"
#include "vtkUnsignedCharArray.h"
#include "vtkPointData.h"
#include "vtkZLibDataCompressor.h"
#include "vtkVVApplication.h"
#include "vtkKWWindow.h"
#include "vtkKWRemoteIOManager.h"
#include "vtkKWProgressCommand.h"
#include "vtkKWCacheManager.h"
#include "vtkMetaImageWriter.h"
#include "vtkMetaImageReader.h"

#include "vtksys/SystemTools.hxx"

vtkCxxRevisionMacro(vtkVVLODDataItemVolumeHelper, "$Revision: 1.23 $");
vtkStandardNewMacro(vtkVVLODDataItemVolumeHelper);

#ifdef KWCommonPro_USE_XML_RW
#include "XML/vtkXMLVVLODDataItemVolumeHelperReader.h"
#include "XML/vtkXMLVVLODDataItemVolumeHelperWriter.h"
#include "XML/vtkXMLObjectWriter.h"
#include "XML/vtkXMLObjectReader.h"
#endif
vtkKWGetXMLReaderWriterObjectsImplementationMacro(vtkVVLODDataItemVolumeHelper, vtkXMLVVLODDataItemVolumeHelperReader, vtkXMLVVLODDataItemVolumeHelperWriter);

//----------------------------------------------------------------------
vtkVVLODDataItemVolumeHelper::vtkVVLODDataItemVolumeHelper()
{
  this->HighResVolume            = NULL;
  this->DataItemVolume           = NULL;
  this->InterpolationMode        = VTK_RESLICE_CUBIC;
  this->LODMode                  = LODModeToFactor;
  this->LODShrinkFactor          = 2.0;
  this->CompressionRatio         = 30.0;
  this->LODLevel                 = LODLevelHigh;
  this->CurrentLODLevel          = LODLevelHigh;
  this->Resample                 = NULL;
  this->MinimumSize[0] = this->MinimumSize[1] = this->MinimumSize[2] = 2;
  this->MinimumSizeInBytes = 3 * 1000 * 1000; // 3MB
}

//----------------------------------------------------------------------
vtkVVLODDataItemVolumeHelper::~vtkVVLODDataItemVolumeHelper()
{
  this->SetDataItemVolume(NULL);
  if (this->Resample)
    {
    this->Resample->Delete();
    this->Resample = NULL;
    }
}

//----------------------------------------------------------------------
void vtkVVLODDataItemVolumeHelper::Update()
{
  if (!this->DataItemVolume)
    {
    vtkErrorMacro( << "Please set volume first using SetDataItemVolume.");
    }

  vtkImageData * input = this->DataItemVolume->GetImageData();
  if (!input)
    {
    vtkErrorMacro( << "No input in DataItemVolume");
    }

  if (this->GetMTime() < this->BuildTime)
    {
    return; // Nothing to do.
    }

  vtkVVApplication * vvapp = vtkVVApplication::SafeDownCast(
                        this->DataItemVolume->GetApplication() );

  if (this->LODLevel == LODLevelLow && this->CurrentLODLevel == LODLevelHigh)
    {
    // We are going from High resolution to Low resolution.

    vtkImageData *image = this->GetImageAtLevel( this->CurrentLODLevel );

    vtkAbstractArray *scalars = image->GetPointData()->GetScalars();

    unsigned long uncompressedSizeInBytes = 
      scalars->GetDataTypeSize( scalars->GetDataType() ) * 
      scalars->GetNumberOfTuples() * 
      scalars->GetNumberOfComponents();  
    
    if (this->MinimumSizeInBytes && 
        uncompressedSizeInBytes < (unsigned long)this->MinimumSizeInBytes)
      {
      this->BuildTime.Modified();
      return;
      }

    if (this->LODShrinkFactor <= 1.0)
      {
      // No shrinking specified.
      this->BuildTime.Modified();
      return;
      }
      
    // We need to downsample from High to Low resolution.

    int outputExtent[6];
    double outputSpacing[3], outputOrigin[3];

    // Get the parameters of the resampled image. The function below returns 1 if
    // no resampling is to be done.
    if (!this->GetOutputExtentOriginAndSpacing( outputExtent, outputSpacing, outputOrigin ))
      {
      this->BuildTime.Modified();
      return; // no resampling to do. 
      }

    if (!this->Resample)
      {
      this->Resample = vtkImageReslice::New();
      }

    if (vvapp)
      {    
      // Do progress reporting to the application if its present...
      vtkKWProgressCommand *prog = vtkKWProgressCommand::New();
      prog->SetWindow(vtkKWWindowBase::SafeDownCast(vvapp->GetNthWindow(0)));
      prog->SetStartMessage( "Downsampling using tricubic interpolation.." );
      this->Resample->AddObserver(vtkCommand::StartEvent, prog);
      this->Resample->AddObserver(vtkCommand::ProgressEvent, prog);
      this->Resample->AddObserver(vtkCommand::EndEvent, prog);
      prog->Delete();
      }

    this->Resample->SetInput( input );
    this->Resample->SetOutputExtent( outputExtent );
    this->Resample->SetOutputOrigin( outputOrigin );
    this->Resample->SetOutputSpacing( outputSpacing );
    this->Resample->SetInterpolationModeToCubic(); // use cubic interpolation for accuracy.
    this->Resample->Update();

    vtkImageData *i = vtkImageData::New();
    i->ShallowCopy( this->Resample->GetOutput() );
    this->DataItemVolume->SetImageData( i );
    i->Delete();

    this->HighResVolume = input;
    this->CurrentLODLevel = LODLevelLow;
    }    

  else if (this->LODLevel == LODLevelHigh && this->CurrentLODLevel == LODLevelLow)
    {

    // We need to go from Low resolution back to High Resolution.

    if (!this->HighResVolume)
      {
      // Nothing to Undo
      return;
      }

    this->DataItemVolume->SetImageData( this->HighResVolume );
    this->HighResVolume = NULL;
    this->CurrentLODLevel = LODLevelHigh;

    if (this->Resample)
      {
      this->Resample->Delete();
      this->Resample = NULL;
      }    
    }

  this->BuildTime.Modified();
  

  // If the compression mode is JPEG2000, we also need to show the effect
  // of the degradation of the compression on the dataset. For ZIP, which 
  // is lossless, this isn't necessary, since the subsampled image is how
  // the final image will look.
  // 
  if (this->LODLevel == LODLevelLow && this->CurrentLODLevel == LODLevelLow)
    {
    if (this->SupportsCompression( VTK_COMPRESSION_JPEG2000 ))
      {
      // Create a temporary directory within the cache directory and write 
      // the compressed data. Then decompress it to estimate the degradation 
      // due to compression.
       
      if (vvapp)
        {

        vtkstd::string cacheDirectory = vtksys::SystemTools::GetFilenamePath(
          vvapp->GetRemoteIOManager()->
                                GetCacheManager()->GetRemoteCacheDirectory());
        cacheDirectory += "/.vv__temp_";
        
        // Remove the directory if it exists.
        vtksys::SystemTools::RemoveADirectory( cacheDirectory.c_str() );

        // Create the directory afresh
        vtksys::SystemTools::MakeDirectory( cacheDirectory.c_str() );

        // Write the compressed file(s) to this directory now.
        vtkstd::string compressedFname = cacheDirectory += "/compressed.mha";
        vvapp->GetNthWindow(0)->SetStatusText( "Compressing..." );
        if ( !this->Write( compressedFname.c_str(),
                           VTK_COMPRESSION_JPEG2000 ))
          {
          vtkErrorMacro( "Error compressing." );
          }

        // Now decompress the compressed file 
        vvapp->GetNthWindow(0)->SetStatusText( "Decompressing..." );
        vtkSmartPointer< vtkMetaImageReader > reader =
          vtkSmartPointer< vtkMetaImageReader >::New();
        reader->SetFileName( compressedFname.c_str() );

        // Do progress reporting to the application if its present...
        vtkKWProgressCommand *cb = vtkKWProgressCommand::New();
        cb->SetWindow(vtkKWWindowBase::SafeDownCast(vvapp->GetNthWindow(0)));
        cb->SetStartMessage( "Decompressing to estimate degradation.." );
        reader->AddObserver(vtkCommand::StartEvent, cb);
        reader->AddObserver(vtkCommand::ProgressEvent, cb);
        reader->AddObserver(vtkCommand::EndEvent, cb);
        cb->Delete();
      
        reader->Update();

        vtkImageData * imageShallow = vtkImageData::New();
        imageShallow->ShallowCopy( reader->GetOutput() );
        this->DataItemVolume->SetImageData( imageShallow );
        imageShallow->Delete();

        this->DataItemVolume->ResetRenderWidgetsInput();

        // Remove the directory containing the compressed data.
        vtksys::SystemTools::RemoveADirectory( cacheDirectory.c_str() );      
        }
      }
    }

  this->BuildTime.Modified();
}

//----------------------------------------------------------------------
vtkImageData * vtkVVLODDataItemVolumeHelper::GetImageAtLevel( int lodLevel )
{
  if (lodLevel == LODLevelHigh)
    {
    if (this->CurrentLODLevel == LODLevelHigh)
      {
      return this->DataItemVolume->GetImageData();
      }
    else
      {
      return this->HighResVolume;
      }
    }
  else
    {
    if (this->CurrentLODLevel == LODLevelHigh)
      {
      vtkErrorMacro( << "Please call SetLODLevel(LODLevelHigh) followed by Update() "
                     << " before requesting for the image at the low res LOD.");
      return NULL;
      }
    else
      {
      return this->DataItemVolume->GetImageData();
      }
    }
  return NULL;
}

//----------------------------------------------------------------------
unsigned long vtkVVLODDataItemVolumeHelper
::Compress( unsigned char *ptr, unsigned long size )
{
  vtkZLibDataCompressor * compressor = vtkZLibDataCompressor::New();
  vtkUnsignedCharArray * outputArray = compressor->Compress( ptr, size );
  unsigned long compressedSize = outputArray->GetNumberOfTuples();
  compressor->Delete();
  outputArray->Delete();
  return compressedSize;
}

//----------------------------------------------------------------------
unsigned long vtkVVLODDataItemVolumeHelper::Compress()
{
  this->Update();

  // Compress the current LOD level

  vtkImageData *image = this->GetImageAtLevel( this->CurrentLODLevel );

  vtkAbstractArray *scalars = image->GetPointData()->GetScalars();

  int uncompressedSizeInBytes = 
    scalars->GetDataTypeSize( scalars->GetDataType() ) * 
    scalars->GetNumberOfTuples() * 
    scalars->GetNumberOfComponents();  

  return this->Compress( (unsigned char *)image->GetScalarPointer(),
                         uncompressedSizeInBytes );
}

//----------------------------------------------------------------------
int vtkVVLODDataItemVolumeHelper::Write( 
      const char * filename, // filename string to write result   
      int useStrategy)
{

  // Sanity check to ensure that the uncompressed size of the file in bytes
  // is larger than the ivar MinimumSizeInBytes.

  vtkImageData *imageHighRes = this->GetImageAtLevel( LODLevelHigh );
  vtkAbstractArray *scalars = imageHighRes->GetPointData()->GetScalars();
  unsigned long uncompressedSizeInBytes = 
      scalars->GetDataTypeSize( scalars->GetDataType() ) * 
      scalars->GetNumberOfTuples() * 
      scalars->GetNumberOfComponents();  
  if (uncompressedSizeInBytes < this->MinimumSizeInBytes)
    {
    // Too small in terms of size
    return 0;
    }    
  

  // status string for optional progress reporting.
  std::string statusString;

  // If useStrategy == -1, automatically pick the strategy.
  int strategy = (useStrategy == -1 ? VTK_COMPRESSION_JPEG2000 : useStrategy);

  // Subsample if necessary, depending on parameters.
  this->SetLODLevel( LODLevelLow );
  this->Update();

  // Get the image to be compressed. This is the subsampled volume.
  if (!this->Resample)
    {
    vtkErrorMacro( << "The Resample filter is NULL!" );
    }
  vtkImageData *image = this->Resample->GetOutput();

  vtkSmartPointer< vtkMetaImageWriter > writer = 
      vtkSmartPointer< vtkMetaImageWriter >::New();
  writer->SetInput( image );

  if (strategy == vtkVVLODDataItemVolumeHelper::VTK_COMPRESSION_JPEG2000 &&
      this->SupportsCompression( strategy ))
    {
    writer->SetCompression(true);
    writer->SetCompressionTypeToJPEG2000();
    writer->SetJPEG2000CompressionRatio( this->CompressionRatio );
    writer->SetJPEG2000UseIrreversibleKernel(0);
    statusString = "Compressing using JPEG2000 ..";
    }

  else // (strategy == vtkVVLODDataItemVolumeHelper::VTK_COMPRESSION_DEFLATE)
       // OR image does not support j2k strategy.
    {
    // Deflate stream is written out as a single file.
    writer->SetCompression(true);
    writer->SetCompressionTypeToDeflate();
    statusString = "Compressing using Deflate ..";
    }

  writer->SetFileName( filename );

  // Do progress reporting to the application if its present...
  if (vtkKWApplication * app = this->DataItemVolume->GetApplication())
    {
    vtkKWProgressCommand *cb = vtkKWProgressCommand::New();
    cb->SetWindow(vtkKWWindowBase::SafeDownCast(app->GetNthWindow(0)));
    cb->SetStartMessage( statusString.c_str() );
    writer->AddObserver(vtkCommand::StartEvent, cb);
    writer->AddObserver(vtkCommand::ProgressEvent, cb);
    writer->AddObserver(vtkCommand::EndEvent, cb);
    cb->Delete();
    }

  // Write
  writer->Write();

  return 1;
}

//----------------------------------------------------------------------
int vtkVVLODDataItemVolumeHelper::SupportsCompression( int strategy )
{
  if (strategy == vtkVVLODDataItemVolumeHelper::VTK_COMPRESSION_JPEG2000)
    {
    vtkImageData *image = this->GetImageAtLevel( this->CurrentLODLevel );

    // Check the scalar type of the data to see if it supports 
    // J2k compression. Only char, uchar, short, ushort, with any number
    // of components are supported.
    vtkAbstractArray *scalars = image->GetPointData()->GetScalars();
    int dataType = scalars->GetDataType();
    if (!(  dataType == VTK_CHAR || 
            dataType == VTK_SIGNED_CHAR ||
            dataType == VTK_UNSIGNED_CHAR ||
            dataType == VTK_SHORT || 
            dataType == VTK_UNSIGNED_SHORT ))
      {
      return 0;
      }

    // too small.
    int dimensions[3];
    image->GetDimensions( dimensions );
    if (dimensions[0] < 16 || dimensions[1] < 16)
      {
      return 0;
      }

    int numComp = scalars->GetNumberOfComponents();      
    if (numComp != 1 && numComp != 3)
      {
      // There is a frickin bug in the OpenJPEG codec that does not allow
      // us to encode anything other than 1 or 3 component data, although
      // the JPEG2000 standard itself allows us to encode any number of 
      // components.
      // See http://groups.google.com/group/openjpeg/msg/c280b159287d6399?
      return 0;
      }

    }
  else
    {
    // Anything is compressible by DEFLATE
    return 1;
    }

  return 1;
}

//----------------------------------------------------------------------
// Returns 1 if any shrinking is to be done. 0 if not.
int vtkVVLODDataItemVolumeHelper
::GetOutputExtentOriginAndSpacing( int outputExtent[6], double outputSpacing[3], double outputOrigin[3] )
{
  if (vtkImageData *input = this->GetImageAtLevel( LODLevelHigh ))
    {
    int inputExtent[6];
    double inputSpacing[3];

    input->GetExtent(inputExtent);
    input->GetSpacing(inputSpacing);
    input->GetOrigin(outputOrigin);

    if (this->LODMode == LODModeToFactor)
      {
      for (int i = 0; i < 3; i++)
        {
        int shrinkFactor = this->LODShrinkFactor;
        while (shrinkFactor)
          {
          outputExtent[2*i] = inputExtent[2*i]/shrinkFactor;

          if ((inputExtent[2*i+1] - inputExtent[2*i] + 1) < this->MinimumSize[i])
            {
            // Can't shrink any further along this dimension.
            this->LastUsedLODShrinkFactor[i] = 1.0;
            outputExtent[2*i+1] = inputExtent[2*i+1];
            outputSpacing[i] = inputSpacing[i];
            break;
            }
          else
            {
            outputExtent[2*i+1] = (int)(((double)(inputExtent[2*i+1]-
                inputExtent[2*i]+1))/this->LODShrinkFactor) + outputExtent[2*i] - 1;
            outputSpacing[i] = inputSpacing[i] * 
              (double)(inputExtent[2*i+1] - inputExtent[2*i] + 1) / 
                ((double)(outputExtent[2*i+1] - outputExtent[2*i] + 1));

            this->LastUsedLODShrinkFactor[i] = shrinkFactor;
          
            // Make sure that we aren't too small to shrink. For instance,
            // we don't want to shrink a 1 slice image or a 4 slice image to
            // one slice.
            if ((outputExtent[2*i+1]-outputExtent[2*i]+1) < this->MinimumSize[i])
              {
              shrinkFactor = shrinkFactor >> 1;
              }
            else
              {
              shrinkFactor = 0;
              }
            }
          }
        }

      // The origins of the start of the high and low res volumes has to match up. 
      // Note that the volumes may start at non-zero extents
      double inputOrigin[3];
      input->GetOrigin(inputOrigin);
      for (int i = 0; i < 3; i++)
        {
        inputOrigin[i] += (((double)inputExtent[2*i]) * inputSpacing[i]);
        outputOrigin[i] = inputOrigin[i] - (((double)(outputExtent[2*i])) * outputSpacing[i]);
        }

      if (outputExtent[1] == inputExtent[1] &&
          outputExtent[3] == inputExtent[3] &&
          outputExtent[5] == inputExtent[5])
        {
        // There is no need to resample. Input and output are of the same
        // extents.
        return 0;
        }
      }

    return 1;
    }

  return 0;
}


//----------------------------------------------------------------------
void vtkVVLODDataItemVolumeHelper::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os,indent);
}


