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

  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 "gdcmStrictScanner.h"
#include "gdcmReader.h"
#include "gdcmGlobal.h"
#include "gdcmDicts.h"
#include "gdcmDict.h"
#include "gdcmDictEntry.h"
#include "gdcmStringFilter.h"
#include "gdcmProgressEvent.h"
#include "gdcmFileNameEvent.h"

#include <algorithm> // std::find

namespace gdcm
{


StrictScanner::~StrictScanner()
= default;

void StrictScanner::ClearTags()
{
  Tags.clear();
}

void StrictScanner::ClearSkipTags()
{
  SkipTags.clear();
}

void StrictScanner::AddSkipTag( Tag const & t )
{
  SkipTags.insert( t );
  gdcm_assert(0); // This is NOT implemented for now
}

// Warning: API is passing a public tag (no way to specify private tag)
void StrictScanner::AddPrivateTag( PrivateTag const & t )
{
  static const Global &g = GlobalInstance;
  static const Dicts &dicts = g.GetDicts();
  const DictEntry &entry = dicts.GetDictEntry( t );
  //std::cout << "Debug: " << entry << std::endl;
  // Is this tag an ASCII on ?
  if( entry.GetVR() & VR::VRASCII )
    {
    PrivateTags.insert( t );
    }
  else if( entry.GetVR() == VR::INVALID )
    {
    gdcmWarningMacro( "Only tag with known VR are allowed. Tag " << t << " will be discarded" );
    }
  else
    {
    gdcm_assert( entry.GetVR() & VR::VRBINARY );
    //gdcmWarningMacro( "Only ASCII VR are supported for now. Tag " << t << " will be discarded" );
    PrivateTags.insert( t );
    }
}

void StrictScanner::AddTag( Tag const & t )
{
  static const Global &g = GlobalInstance;
  static const Dicts &dicts = g.GetDicts();
  const DictEntry &entry = dicts.GetDictEntry( t );
  // Is this tag an ASCII on ?
  if( entry.GetVR() & VR::VRASCII )
    {
    Tags.insert( t );
    }
  else if( entry.GetVR() == VR::INVALID )
    {
    gdcmWarningMacro( "Only tag with known VR are allowed. Tag " << t << " will be discarded" );
    }
  else
    {
    gdcm_assert( entry.GetVR() & VR::VRBINARY );
    //gdcmWarningMacro( "Only ASCII VR are supported for now. Tag " << t << " will be discarded" );
    Tags.insert( t );
    }
}

// see gdcmReader.strict.cxx
bool StrictReadUpToTag( const char * filename, Tag const & last, std::set<Tag> const & skiptags );

bool StrictScanner::Scan( Directory::FilenamesType const & filenames )
{
  this->InvokeEvent( StartEvent() );

  // Is there at least one tag ?
  if( !Tags.empty() || !PrivateTags.empty() )
    {
    //if( filenames.empty() ) return true;

    // Prepare hash table:
    Mappings.clear();
    Mappings[""]; // Create a fake table for dummy file

    // Make our own copy:
    Filenames = filenames;

    // Find the tag with the highest value (get the one from the end of the std::set)
    Tag last;
    if( !Tags.empty() )
      {
      TagsType::const_reverse_iterator it1 = Tags.rbegin();
      const Tag & publiclast = *it1;
      last = publiclast;
      }
    if( !PrivateTags.empty() )
      {
      PrivateTagsType::const_reverse_iterator pit1 = PrivateTags.rbegin();
      Tag privatelast = *pit1;
      if( last < privatelast ) last = privatelast;
      }

    StringFilter sf;
    Directory::FilenamesType::const_iterator it = Filenames.begin();
    const double progresstick = 1. / (double)Filenames.size();
    Progress = 0;
    for(; it != Filenames.end(); ++it)
      {
      Reader reader;
      const char *filename = it->c_str();
      gdcm_assert( filename );
      reader.SetFileName( filename );
      // Pass #1, just check if the file is valid (up to the tag)
      const bool strict = StrictReadUpToTag( filename, last, SkipTags );
      if( strict )
      {
      // Pass #2, syntax is ok, retrieve data now:
      bool read = false;
      try
        {
        // Start reading all tags, including the 'last' one:
        read = reader.ReadUpToTag(last, SkipTags);
        }
      catch(std::exception & ex)
        {
        (void)ex;
        gdcmWarningMacro( "Failed to read:" << filename << " with ex:" << ex.what() );
        }
      catch(...)
        {
        gdcmWarningMacro( "Failed to read:" << filename  << " with unknown error" );
        }
      if( read )
        {
        // Keep the mapping:
        sf.SetFile( reader.GetFile() );
        StrictScanner::ProcessPublicTag(sf, filename);
        //StrictScanner::ProcessPrivateTag(sf, filename);
        }
        }
      // Update progress
      Progress += progresstick;
      ProgressEvent pe;
      pe.SetProgress( Progress );
      this->InvokeEvent( pe );
      // For outside application tell which file is being processed:
      FileNameEvent fe( filename );
      this->InvokeEvent( fe );
      }
    }

  this->InvokeEvent( EndEvent() );
  return true;
}

void StrictScanner::Print( std::ostream & os ) const
{
  os << "Values:\n";
  for(ValuesType::const_iterator it = Values.begin() ; it != Values.end();
    ++it)
    {
    os << *it << "\n";
    }
  os << "Mapping:\n";
  Directory::FilenamesType::const_iterator file = Filenames.begin();
  for(; file != Filenames.end(); ++file)
    {
    const char *filename = file->c_str();
    gdcm_assert( filename && *filename );
    bool b = IsKey(filename);
    const char *comment = !b ? "could not be read" : "could be read";
    os << "Filename: " << filename << " (" << comment << ")\n";
    //const FilenameToValue &mapping = Mappings[*tag];
    if( Mappings.find(filename) != Mappings.end() )
      {
      const TagToValue &mapping = GetMapping(filename);
      TagToValue::const_iterator it = mapping.begin();
      for( ; it != mapping.end(); ++it)
        {
        const Tag & tag = it->first;
        const char *value = it->second;
        os << tag << " -> [" << value << "]\n";
        }
      }
    }
}

static bool IsVRUI(Tag const &tag)
{
  static const Global &g = Global::GetInstance();
  static const Dicts &dicts = g.GetDicts();
  const DictEntry &dictentry = dicts.GetDictEntry(tag);
  if( dictentry.GetVR() == VR::UI ) return true;
  //if( tag == Tag(0x0020,0x000d)   // Study Instance UID : UI
  // || tag == Tag(0x0020,0x0052)   //
  // || tag == Tag(0x0020,0x000e) ) // Series Instance UID : UI
  //  {
  //  return true;
  //  }
  return false;
}

void StrictScanner::PrintTable( std::ostream & os ) const
{
  Directory::FilenamesType::const_iterator file = Filenames.begin();
  for(; file != Filenames.end(); ++file)
    {
    const char *filename = file->c_str();
    gdcm_assert( filename && *filename );
    os << '"' << filename << '"' << "\t";
    TagsType::const_iterator tag = Tags.begin();
    const TagToValue &mapping = GetMapping(filename);
    for( ; tag != Tags.end(); ++tag )
      {
      const Tag &t = *tag;
      bool isui = IsVRUI(t);
      const char *value = "";
      if( mapping.find(t) != mapping.end() ) {
        const char * v = mapping.find(t)->second;
        if(v) value = v;
      }
      os << '"' << (isui ? String<>::Trim( value ) : value) << '"';
      os << "\t";
      }
    os << "\n";
    }
}

StrictScanner::TagToValue const & StrictScanner::GetMapping(const char *filename) const
{
//  gdcm_assert( Mappings.find(filename) != Mappings.end() );
  gdcm_assert( filename && *filename );
  if( Mappings.find(filename) != Mappings.end() )
    return Mappings.find(filename)->second;
  return Mappings.find("")->second; // dummy file could not be found
}

bool StrictScanner::IsKey( const char * filename ) const
{
/*
  // std::find on contiguous array will operate in 0(n) which is way too slow, assume user is not too dumb...
  Directory::FilenamesType::const_iterator it = std::find(Filenames.begin(), Filenames.end(), filename);
  if( it == Filenames.end() )
    {
    gdcmErrorMacro( "The file: " << filename << " was not scanned" );
    return false;
    }
*/
  // Look for the file in Mappings table:
  gdcm_assert( filename && *filename );
  MappingType::const_iterator it2 = Mappings.find(filename);
  return it2 != Mappings.end();
}


Directory::FilenamesType StrictScanner::GetKeys() const
{
  Directory::FilenamesType keys;

  Directory::FilenamesType::const_iterator file = Filenames.begin();
  for(; file != Filenames.end(); ++file)
    {
    const char *filename = file->c_str();
    if( IsKey( filename ) )
      {
      keys.push_back( filename );
      }
    }
  gdcm_assert( keys.size() <= Filenames.size() );
  return keys;
}


const char* StrictScanner::GetValue(const char *filename, Tag const &t) const
{
  // \precondition
  gdcm_assert( Tags.find( t ) != Tags.end() );
  TagToValue const &ftv = GetMapping(filename);
  if( ftv.find(t) != ftv.end() )
    {
    return ftv.find(t)->second;
    }
  return nullptr;
}

const char *StrictScanner::GetFilenameFromTagToValue(Tag const &t, const char *valueref) const
{
  const char *filenameref = nullptr;
  if( valueref )
    {
    Directory::FilenamesType::const_iterator file = Filenames.begin();
    size_t len = strlen( valueref );
    if( len && valueref[ len - 1 ] == ' ' )
      {
      --len;
      }
    for(; file != Filenames.end() && !filenameref; ++file)
      {
      const char *filename = file->c_str();
      const char * value = GetValue(filename, t);
      if( value && strncmp(value, valueref, len ) == 0 )
        {
        filenameref = filename;
        }
      }
    }
  return filenameref;
}


/// Will loop over all files and return a vector of std::strings of filenames
/// where value match the reference value 'valueref'
Directory::FilenamesType
StrictScanner::GetAllFilenamesFromTagToValue(Tag const &t, const char *valueref) const
{
  Directory::FilenamesType theReturn;
  if( valueref )
    {
    const std::string valueref_str = String<>::Trim( valueref );
    Directory::FilenamesType::const_iterator file = Filenames.begin();
    for(; file != Filenames.end(); ++file)
      {
      const char *filename = file->c_str();
      const char * value = GetValue(filename, t);
      const std::string value_str = String<>::Trim( value );
      if( value_str == valueref_str )
        {
        theReturn.push_back( filename );
        }
      }
    }
  return theReturn;

}

StrictScanner::TagToValue const & StrictScanner::GetMappingFromTagToValue(Tag const &t, const char *valueref) const
{
  return GetMapping( GetFilenameFromTagToValue(t, valueref) );
}

StrictScanner::ValuesType StrictScanner::GetValues(Tag const &t) const
{
  ValuesType vt;
  Directory::FilenamesType::const_iterator file = Filenames.begin();
  for(; file != Filenames.end(); ++file)
    {
    const char *filename = file->c_str();
    TagToValue const &ttv = GetMapping(filename);
    if( ttv.find(t) != ttv.end() )
      {
      vt.insert( ttv.find(t)->second );
      }
    }
  return vt;
}


Directory::FilenamesType StrictScanner::GetOrderedValues(Tag const &t) const
{
  Directory::FilenamesType theReturn;
  Directory::FilenamesType::const_iterator file = Filenames.begin();
  for(; file != Filenames.end(); ++file)
    {
    const char *filename = file->c_str();
    TagToValue const &ttv = GetMapping(filename);
    if( ttv.find(t) != ttv.end() )
      {
        std::string theVal = std::string(ttv.find(t)->second);
        if (std::find(theReturn.begin(), theReturn.end(), theVal) == theReturn.end()){
          theReturn.push_back( theVal );//only add new tags to the list
        }
      }
    }
  return theReturn;
}

void StrictScanner::ProcessPublicTag(StringFilter &sf, const char *filename)
{
  gdcm_assert( filename );
  TagToValue &mapping = Mappings[filename];
  const File& file = sf.GetFile();

  const FileMetaInformation & header = file.GetHeader();
  const DataSet & ds = file.GetDataSet();
  TagsType::const_iterator tag = Tags.begin();
  for( ; tag != Tags.end(); ++tag )
    {
    if( tag->GetGroup() == 0x2 )
      {
      if( header.FindDataElement( *tag ) )
        {
        //std::string s;
        DataElement const & de = header.GetDataElement( *tag );
        //const ByteValue *bv = de.GetByteValue();
        ////gdcm_assert( VR::IsASCII( vr ) );
        //if( bv ) // Hum, should I store an empty string or what ?
        //  {
        //  s = std::string( bv->GetPointer(), bv->GetLength() );
        //  s.resize( std::min( s.size(), strlen( s.c_str() ) ) );
        //  }
        std::string s = sf.ToString(de.GetTag());

        // Store the potentially new value:
        Values.insert( s );
        gdcm_assert( Values.find( s ) != Values.end() );
        const char *value = Values.find( s )->c_str();
        gdcm_assert( value );
        mapping.insert(
          TagToValue::value_type(*tag, value));
        }
      }
    else
      {
      if( ds.FindDataElement( *tag ) )
        {
        //std::string s;
        DataElement const & de = ds.GetDataElement( *tag );
        //const ByteValue *bv = de.GetByteValue();
        ////gdcm_assert( VR::IsASCII( vr ) );
        //if( bv ) // Hum, should I store an empty string or what ?
        //  {
        //  s = std::string( bv->GetPointer(), bv->GetLength() );
        //  s.resize( std::min( s.size(), strlen( s.c_str() ) ) );
        //  }
        std::string s = sf.ToString(de.GetTag());

        // Store the potentially new value:
        Values.insert( s );
        gdcm_assert( Values.find( s ) != Values.end() );
        const char *value = Values.find( s )->c_str();
        gdcm_assert( value );
        mapping.insert(
          TagToValue::value_type(*tag, value));
        }
      }
    } // end for
}

} // end namespace gdcm
