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

  Program: GDCM (Grassroots DICOM). A DICOM library

  Copyright (c) 2006-2011 Mathieu Malaterre
  All rights reserved.
  See Copyright.txt or http://gdcm.sourceforge.net/Copyright.html 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 "gdcmEmptyMaskGenerator.h"

#include "gdcmImage.h"
#include "gdcmImageWriter.h"
#include "gdcmFileDerivation.h"
#include "gdcmUIDGenerator.h"
#include "gdcmImageRegionReader.h"
#include "gdcmDirectory.h"
#include "gdcmScanner.h"
#include "gdcmFilename.h"
#include "gdcmFileStreamer.h"
#include "gdcmAnonymizer.h"
#include "gdcmAttribute.h"
#include "gdcmTagKeywords.h"

namespace gdcm {

struct EmptyMaskGenerator::impl
{
  static const Tag TSOPClassUID;
  static const Tag TSOPInstanceUID;
  static const Tag TSeriesInstanceUID;
  static const Tag TFrameOfReferenceUID;

  SOPClassUIDMode mode;
  std::string inputdir;
  std::string outputdir;
  UIDGenerator uid;
  std::map< std::string, std::string > seriesuidhash;
  std::map< std::string, std::string > framerefuidhash;
  Scanner s;
  bool collectuids(Tag const & tag, std::map< std::string, std::string > & hash);
  bool setup(const char * dirname, const char * outdir);
  bool setmask( File & file );
  bool derive( const char * filename, File & file ) const;
  bool anonymizeattributes( const char * filename, File & file );
  bool populateattributes( const char * filename, File const & orifile, File & file );
  bool setts( File & file );
  bool run(const char * filename, const char * outfile);
};

const Tag EmptyMaskGenerator::impl::TSOPClassUID = Tag(0x0008,0x0016);
const Tag EmptyMaskGenerator::impl::TSOPInstanceUID = Tag(0x0008,0x0018);
const Tag EmptyMaskGenerator::impl::TSeriesInstanceUID = Tag(0x0020,0x000e);
const Tag EmptyMaskGenerator::impl::TFrameOfReferenceUID = Tag(0x0020,0x0052);

bool EmptyMaskGenerator::impl::collectuids( Tag const & tag, std::map< std::string, std::string > & hash)
{
  Scanner::ValuesType vt = s.GetValues(tag);
  for( Scanner::ValuesType::const_iterator it = vt.begin();
    it != vt.end(); ++it )
    {
    const char * newuid = uid.Generate();
    hash.insert( std::make_pair( *it, newuid ) );
    }
  return true;
}

bool EmptyMaskGenerator::impl::setmask( File & file )
{
  DataSet& ds = file.GetDataSet();
  namespace kwd = Keywords;
  kwd::ImageType imtype;
  kwd::ImageType copy;
  imtype.SetFromDataSet( ds );
  unsigned int nvalues = imtype.GetNumberOfValues();
  unsigned int newvalues = std::max( nvalues, 4u );
  copy.SetNumberOfValues( newvalues );
  // copy original ones:
  for( unsigned int i = 0u; i < nvalues; ++i )
    {
    copy.SetValue(i, imtype.GetValue(i) );
    }
  // Make up non empty values:
  static const CSComp values[] = {"DERIVED","SECONDARY","OTHER"};
  for( unsigned int i = nvalues; i < 3u;  ++i )
    {
    copy.SetValue(i, values[i] );
    }
  // The fourth value is required to be set to 'MASK' for SD
  copy.SetValue( 3u, "MASK" );
  ds.Replace( copy.GetAsDataElement() );
  return true;
}

bool EmptyMaskGenerator::impl::setup(const char * dirname, const char * outdir)
{
  if( !System::FileIsDirectory( dirname ) )
    return false;
  if( !System::MakeDirectory( outdir ) )
    return false;
  Directory d;
  // recursive search by default
  const unsigned int nfiles = d.Load( dirname, true );
  if( nfiles == 0 )
    {
    gdcmDebugMacro( "No files found in: " << dirname );
    return false;
    }
  Directory::FilenamesType const & filenames = d.GetFilenames();

  s.AddTag( TSOPClassUID );
  s.AddTag( TSOPInstanceUID );
  s.AddTag( TSeriesInstanceUID );
  s.AddTag( TFrameOfReferenceUID );
  // reduce verbosity when looping over a set of files:
  Trace::WarningOff();
  if( !s.Scan( filenames ) )
    {
    gdcmDebugMacro( "Scanner failure for directory: " << dirname );
    return false;
    }
  if( !collectuids( TSeriesInstanceUID, seriesuidhash ) ) return false;
  // Frame of Reference are relative to Series UID
  // http://dicom.nema.org/medical/Dicom/2015a/output/chtml/part03/sect_C.7.4.html
  if( !collectuids( TFrameOfReferenceUID, framerefuidhash ) ) return false;

  return true;
}

bool EmptyMaskGenerator::impl::derive( const char * filename, File & file ) const
{
  FileDerivation fd;
  const char * referencedsopclassuid = s.GetValue (filename, TSOPClassUID);
  const char * referencedsopinstanceuid = s.GetValue (filename, TSOPInstanceUID);
  if( !fd.AddReference( referencedsopclassuid, referencedsopinstanceuid ) )
    {
    gdcmDebugMacro( "Impossible to AddRef: " << filename );
    // This is not considered an error to not reference, eg. UID padded with 0
    }

  // CID 7202 Source Image Purposes of Reference
  // DCM 121321 Mask image for image processing operation
  fd.SetPurposeOfReferenceCodeSequenceCodeValue( 121321 );
  // CID 7203 Image Derivation
  // DCM 113047 Pixel by pixel mask
  fd.SetDerivationCodeSequenceCodeValue( 113047 );
  fd.SetDerivationDescription( "Empty Mask Derivation" );
  // always append derivation history to any existing one:
  fd.SetAppendDerivationHistory( true );
  fd.SetFile( file );
  if( !fd.Derive() )
    {
    gdcmDebugMacro( "Sorry could not derive using input info" );
    return false;
    }
  return true;
}

bool EmptyMaskGenerator::impl::anonymizeattributes( const char * filename, File & file )
{
  Anonymizer ano;
  ano.SetFile( file );
  ano.RemoveGroupLength();
  ano.RemovePrivateTags();
  namespace kwd = Keywords;
  ano.Remove( kwd::WindowCenter::GetTag() );
  ano.Remove( kwd::WindowWidth::GetTag() );
  if( !ano.Replace (TSOPInstanceUID, uid.Generate()) ) return false;
  const char * oldseriesuid = s.GetValue (filename, TSeriesInstanceUID);
  const char * oldframerefuid = s.GetValue (filename, TFrameOfReferenceUID);
  if( oldseriesuid )
    {
    std::map< std::string, std::string >::const_iterator it1 = seriesuidhash.find( oldseriesuid );
    if( !ano.Replace (TSeriesInstanceUID, it1->second.c_str() ) ) return false;
    }
  if( oldframerefuid )
    {
    std::map< std::string, std::string >::const_iterator it2 = framerefuidhash.find( oldframerefuid );
    if( !ano.Replace (TFrameOfReferenceUID, it2->second.c_str() ) ) return false;
    }
  return true;
}

bool EmptyMaskGenerator::impl::populateattributes( const char * filename, File const & orifile, File & file )
{
  namespace kwd = Keywords;
  DataSet & ds = file.GetDataSet();

  // ContentDate
  char date[22];
  const size_t datelen = 8;
  System::GetCurrentDateTime(date);
  kwd::ContentDate contentdate;
  // Do not copy the whole cstring:
  contentdate.SetValue( DAComp( date, datelen ) );
  ds.Insert( contentdate.GetAsDataElement() );
  // ContentTime
  const size_t timelen = 6 + 1 + 6; // time + milliseconds
  kwd::ContentTime contenttime;
  // Do not copy the whole cstring:
  contenttime.SetValue( TMComp(date+datelen, timelen) );
  ds.Insert( contenttime.GetAsDataElement() );

  const DataSet & orids = orifile.GetDataSet();
  kwd::SeriesInstanceUID seriesinstanceuid;
  seriesinstanceuid.SetValue( uid.Generate() ); // In case original instance is missing Series Instance UID
  kwd::SeriesNumber seriesnumber = { 1 };
  const char * oldseriesuid = s.GetValue (filename, TSeriesInstanceUID);
  if( oldseriesuid )
    {
    std::map< std::string, std::string >::iterator it =
      seriesuidhash.find( oldseriesuid );
    seriesinstanceuid.SetValue( it->second.c_str() );
    std::map< std::string, std::string >::difference_type diff =
      std::distance(seriesuidhash.begin(),it);
    seriesnumber.SetValue( 1 + (int)diff ); // Start at one
    }
  ds.Insert( seriesinstanceuid.GetAsDataElement() );
  ds.Insert( seriesnumber.GetAsDataElement() );
  kwd::FrameOfReferenceUID frameref;
  frameref.SetValue( uid.Generate() );
  const char * oldframerefuid = s.GetValue (filename, TFrameOfReferenceUID);
  if( oldframerefuid )
    {
    std::map< std::string, std::string >::const_iterator it = framerefuidhash.find( oldframerefuid );
    frameref.SetValue( it->second.c_str() );
    }
  ds.Insert( frameref.GetAsDataElement() );
  kwd::InstanceNumber instancenum = { 1 };
  if( orids.FindDataElement( instancenum.GetTag() ) )
    {
    instancenum.SetFromDataSet( orids );
    }
  else
    {
    static unsigned int counter = 0; // unsigned will wrap properly
    instancenum.SetValue( counter++ );
    }
  ds.Insert( instancenum.GetAsDataElement() );
  kwd::StudyInstanceUID studyinstanceuid;
  studyinstanceuid.SetFromDataSet( orids );
  ds.Insert( studyinstanceuid.GetAsDataElement() );
  kwd::StudyID studyid = { "ST1" };
  studyid.SetFromDataSet( orids );
  ds.Insert( studyid.GetAsDataElement() );
  kwd::PatientID patientid;
  patientid.SetFromDataSet( orids );
  ds.Insert( patientid.GetAsDataElement() );
  kwd::PositionReferenceIndicator pri;
  ds.Insert( pri.GetAsDataElement() );
  kwd::BodyPartExamined bodypartex;
  bodypartex.SetFromDataSet( orids );
  ds.Insert( bodypartex.GetAsDataElement() );
  // Sync with Body Part Examined:
  kwd::Laterality lat;
  if( orids.FindDataElement( lat.GetTag() ) )
    {
    lat.SetFromDataSet( orids );
    ds.Insert( lat.GetAsDataElement() );
    }
  kwd::PatientOrientation pator;
  pator.SetFromDataSet( orids );
  ds.Insert( pator.GetAsDataElement() );
  kwd::BurnedInAnnotation bia = { "NO" };
  ds.Insert( bia.GetAsDataElement() );
  kwd::ConversionType convtype = { "SYN" };
  ds.Insert( convtype.GetAsDataElement() );
  kwd::PresentationLUTShape plutshape = { "IDENTITY" }; // MONOCHROME2
  ds.Insert( plutshape.GetAsDataElement() );
  kwd::SOPClassUID sopclassuid;
  // gdcm will pick the Word in case Byte class is not compatible:
  MediaStorage ms = MediaStorage::MultiframeGrayscaleByteSecondaryCaptureImageStorage;
  sopclassuid.SetValue( ms.GetString() );
  ds.Insert( sopclassuid.GetAsDataElement() );
  return true;
}

bool EmptyMaskGenerator::impl::setts( File & file )
{
  FileMetaInformation & fmi = file.GetHeader();
  const TransferSyntax & orits = fmi.GetDataSetTransferSyntax();
  TransferSyntax::TSType newts = TransferSyntax::ImplicitVRLittleEndian;
  if( orits.IsExplicit() )
    {
    newts = TransferSyntax::ExplicitVRLittleEndian;
    }
  fmi.Clear();
  fmi.SetDataSetTransferSyntax( newts );
  return true;
}

bool EmptyMaskGenerator::impl::run(const char * filename, const char * outfile)
{
  if( !s.IsKey( filename ) )
    {
    gdcmErrorMacro( "Not DICOM file: " << filename );
    return false;
    }

  ImageRegionReader irr;
  irr.SetFileName( filename );
  if( !irr.ReadInformation() )
    {
    gdcmErrorMacro( "Impossible to ReadInformation (not an image?): " << filename );
    return false;
    }
  size_t buflen = irr.ComputeBufferLength();
  Image & img = irr.GetImage();
  if( img.GetPhotometricInterpretation() != PhotometricInterpretation::MONOCHROME1
   && img.GetPhotometricInterpretation() != PhotometricInterpretation::MONOCHROME2 )
    {
    gdcmErrorMacro( "Cannot process PhotometricInterpretation from: " << filename );
    return false;
    }

  if( mode == UseOriginalSOPClassUID )
    {
    File & file = irr.GetFile();
    // derive operation needs to operate on original attributes (before anonymization):
    if( !derive( filename, file ) ) return false;
    // copy original attributes:
    if( !anonymizeattributes( filename, file ) ) return false;
    if( !setmask( file ) ) return false;
    if( !setts( file ) ) return false;

    Writer w;
    w.SetFile( file );
    w.SetFileName( outfile );
    if( !w.Write() )
      {
      return false;
      }
    }
  else if ( mode == UseGrayscaleSecondaryImageStorage )
    {
    ImageWriter w;
    File & file = w.GetFile();
    if( !derive( filename, file ) ) return false;
    // create attributes:
    if( !populateattributes( filename, irr.GetFile(), file ) ) return false;
    if( !setmask( file ) ) return false;
    if( !setts( file ) ) return false;

    PixelFormat & pf = img.GetPixelFormat();
    pf.SetPixelRepresentation(0); // always overwrite to unsigned
    img.SetSlope(1);
    img.SetIntercept(0);
    w.SetImage( img );
    w.SetFileName( outfile );
    // sentinel, SC is never acceptable:
    if( w.ComputeTargetMediaStorage() == MediaStorage::SecondaryCaptureImageStorage )
      {
      gdcmErrorMacro( "Failure to compute MediaStorage: " << filename );
      return false;
      }
    if( !w.Write() )
      {
      return false;
      }
    }
  else
    {
    // possibly dead code, but make sure to report an error for invalid state:
    return false;
    }

  // now create the empty pixel data element, a chunk at a time:
  FileStreamer fs;
  fs.SetTemplateFileName(outfile);
  fs.SetOutputFileName(outfile);
  Tag pixeldata (0x7fe0, 0x0010);
  fs.CheckDataElement( pixeldata ); // double check generated output
  if( !fs.StartDataElement( pixeldata ) )
    {
    gdcmErrorMacro( "StartDataElement" );
    return false;
    }
    {
    const unsigned int chunk = 4096u;
    char bytes[chunk] = {};
    const unsigned int nchunks = (unsigned int)( buflen / chunk);
    const unsigned int remain = buflen % chunk;
    for( unsigned int i = 0; i < nchunks; ++i )
      {
      // Read the source file into a byte array.
      fs.AppendToDataElement( pixeldata, bytes, chunk );
      }
    fs.AppendToDataElement( pixeldata, bytes, remain );
    }
  if( !fs.StopDataElement( pixeldata ) )
    {
    // Most likely an issue with Pixel Data Length computation:
    gdcmErrorMacro( "StopDataElement" );
    return false;
    }

  return true;
}

EmptyMaskGenerator::EmptyMaskGenerator():pimpl(new impl)
{
}

EmptyMaskGenerator::~EmptyMaskGenerator()
{
  delete pimpl;
}

void EmptyMaskGenerator::SetSOPClassUIDMode( SOPClassUIDMode mode )
{
  pimpl->mode = mode;
}

void EmptyMaskGenerator::SetInputDirectory(const char * dirname)
{
  if( dirname )
    pimpl->inputdir = dirname;
}

void EmptyMaskGenerator::SetOutputDirectory(const char * dirname)
{
  if( dirname )
    pimpl->outputdir = dirname;
}


bool EmptyMaskGenerator::Execute()
{
  const char * dirname = pimpl->inputdir.c_str();
  const char * outdir = pimpl->outputdir.c_str();
  if( !pimpl->setup(dirname, outdir) )
    {
    return false;
    }
  bool success = true;
  Directory::FilenamesType const & filenames = pimpl->s.GetFilenames();
  for( Directory::FilenamesType::const_iterator it =  filenames.begin(); it != filenames.end(); ++it )
    {
    const char * filename = it->c_str();
    Filename fn( filename );
    std::string outfile = outdir;
    outfile += '/';
    outfile += fn.GetName();
    if( !pimpl->run( filename, outfile.c_str() ) )
      {
      gdcmErrorMacro( "Failure to EmptyMask" );
      // Since we may have failed in the middle of writing of the file, remove it:
      if( System::FileExists(outfile.c_str()) && !System::RemoveFile(outfile.c_str()) )
        {
        gdcmErrorMacro( "Failure to RemoveFile: " << outfile );
        // may want to call exit() here, since we failed to remove a file we created in this process:
        return false;
        }
      // declare failure to process a file, but continue main loop:
      success = false;
      }
    }
  return success;
}

} // end namespace gdcm
