/*=========================================================================
 *
 *  Copyright NumFOCUS
 *
 *  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
 *
 *         https://www.apache.org/licenses/LICENSE-2.0.txt
 *
 *  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.
 *
 *=========================================================================*/
/*=========================================================================
 *
 *  Portions of this file are subject to the VTK Toolkit Version 3 copyright.
 *
 *  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
 *
 *  For complete copyright, license and disclaimer of warranty information
 *  please refer to the NOTICE file at the top of the ITK source tree.
 *
 *=========================================================================*/
#include "itkStimulateImageIO.h"
#include "itksys/SystemTools.hxx"
#include "itksys/RegularExpression.hxx"
#include "itkByteSwapper.h"
#include "itkMakeUniqueForOverwrite.h"

namespace itk
{
StimulateImageIO::StimulateImageIO()
{
  m_DisplayRange[0] = 0;
  m_DisplayRange[1] = 0;
  this->SetNumberOfDimensions(4);
  m_ByteOrder = IOByteOrderEnum::BigEndian;
  m_FileType = IOFileEnum::Binary;

  const char * extensions[] = { ".spr" };


  for (auto ext : extensions)
  {
    this->AddSupportedWriteExtension(ext);
    this->AddSupportedReadExtension(ext);
  }
}

StimulateImageIO::~StimulateImageIO() = default;

// This method will only test if the header looks like a
// Stimulate image file.
bool
StimulateImageIO::CanReadFile(const char * filename)
{
  std::ifstream file;
  char          buffer[256];
  std::string   fname(filename);

  if (fname.empty())
  {
    itkDebugMacro("No filename specified.");
    return false;
  }

  const bool extensionFound = this->HasSupportedReadExtension(filename, false);

  if (!extensionFound)
  {
    itkDebugMacro("The filename extension is not recognized");
    return false;
  }

  try
  {
    this->OpenFileForReading(file, fname);
  }
  catch (const ExceptionObject &)
  {
    return false;
  }

  // Check to see if its a spr file
  file.getline(buffer, 255);

  fname = buffer;

  if (fname.find("numDim:") < fname.length() || fname.find("dim:") < fname.length() ||
      fname.find("dataType:") < fname.length())
  {
    return true;
  }

  return false;
}

void
StimulateImageIO::Read(void * buffer)
{
  std::ifstream file;

  // read header information file:
  this->InternalReadImageInformation(file);

  // read data file
  std::ifstream file_data;

  if (m_DataFileName.empty())
  {
    // if no data filename was specified try to guess one:
    // based on filename.spr , options are:
    // 1: filename.spr.sdt
    // 2: filename.sdt

    // option 1:
    m_DataFileName = m_FileName;
    m_DataFileName.replace(m_DataFileName.length(), 4, ".sdt");

    // Actually open the file
    try
    {
      this->OpenFileForReading(file_data, m_DataFileName);
    }
    catch (const ExceptionObject &)
    {
      // option 2:
      m_DataFileName = m_FileName;
      m_DataFileName.replace(m_DataFileName.length() - 3, 3, "sdt");
      try
      {
        this->OpenFileForReading(file_data, m_DataFileName);
      }
      catch (const ExceptionObject &)
      {
        itkExceptionStringMacro("No Data file was specified in header (spr) file and guessing file data name failed.");
      }
    }
  } // a filename was found for data file

  this->OpenFileForReading(file_data, m_DataFileName);

  if (!this->ReadBufferAsBinary(file_data, buffer, this->GetImageSizeInBytes()))
  {
    itkExceptionMacro("Read failed: Wanted " << this->GetImageSizeInBytes() << " bytes, but read " << file_data.gcount()
                                             << " bytes."
                                             << " from file " << m_DataFileName);
  }

  // byte swapping depending on pixel type:
  switch (this->GetComponentType())
  {
    case IOComponentEnum::SCHAR:
      // For CHAR, it is not necessary to swap bytes. (It would not have any effect anyway.)
      break;
    case IOComponentEnum::SHORT:
      ByteSwapper<short>::SwapRangeFromSystemToBigEndian(static_cast<short *>(buffer),
                                                         static_cast<SizeValueType>(this->GetImageSizeInComponents()));
      break;
    case IOComponentEnum::INT:
      ByteSwapper<int>::SwapRangeFromSystemToBigEndian(static_cast<int *>(buffer),
                                                       static_cast<SizeValueType>(this->GetImageSizeInComponents()));
      break;
    case IOComponentEnum::FLOAT:
      ByteSwapper<float>::SwapRangeFromSystemToBigEndian(static_cast<float *>(buffer),
                                                         static_cast<SizeValueType>(this->GetImageSizeInComponents()));
      break;
    case IOComponentEnum::DOUBLE:
      ByteSwapper<double>::SwapRangeFromSystemToBigEndian(static_cast<double *>(buffer),
                                                          static_cast<SizeValueType>(this->GetImageSizeInComponents()));
      break;
    default:
      break;
  }

  // closing files:
  file.close();
  file_data.close();
}

void
StimulateImageIO::InternalReadImageInformation(std::ifstream & file)
{
  char        line[255];
  std::string text;

  // read .sdt file (header)

  // open in ascii mode
  this->OpenFileForReading(file, m_FileName, true);

  // set values in case we don't find them
  this->SetNumberOfDimensions(4);
  m_Spacing[0] = 1.0;
  m_Spacing[1] = 1.0;
  m_Spacing[2] = 1.0;
  m_Spacing[3] = 1.0;

  m_Origin[0] = 0.0;
  m_Origin[1] = 0.0;
  m_Origin[2] = 0.0;
  m_Origin[3] = 0.0;

  // char fidName[256] = "";
  // char orient[256] = "";

  bool         fov_specified = false;
  bool         origin_specified = false;
  bool         spacing_specified = false;
  float        fov[4];
  unsigned int dims[4];
  while ((static_cast<void>(file.getline(line, 255)), file.gcount() > 0))
  {
    text = line;

    if (text.find("numDim") < text.length())
    {
      unsigned int dim = 0;
      sscanf(line, "%*s %u", &dim);
      this->SetNumberOfDimensions(dim);
    }
    else if (text.find("dim") < text.length())
    {
      // extract dimensions, spacing, origin
      sscanf(line, "%*s %u %u %u %u", dims, dims + 1, dims + 2, dims + 3);
      if (m_NumberOfDimensions > 3 && dims[3] <= 1)
      {
        this->SetNumberOfDimensions(3);
      }
      if (m_NumberOfDimensions > 2 && dims[2] <= 1)
      {
        this->SetNumberOfDimensions(2);
      }
      for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
      {
        m_Dimensions[i] = dims[i];
      }
    }
    else if (text.find("origin") < text.length())
    {
      // origin
      // Position of the center of the first voxel. One value for each dimension.
      // If
      // the origin is not specified, but the fov is, then the image is assumed
      // to be centered:

      // save and reset old locale
      float             origin[4];
      const std::locale currentLocale = std::locale::global(std::locale::classic());
      sscanf(line, "%*s %f %f %f %f", origin, origin + 1, origin + 2, origin + 3);
      // reset locale
      std::locale::global(currentLocale);
      for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
      {
        m_Origin[i] = origin[i];
      }
      origin_specified = true;
    }
    else if (text.find("extent") < text.length())
    {
      // not documented
      itkDebugMacro("Extent was specified");
    }
    else if (text.find("fov") < text.length())
    {
      // fov
      // Field of view: The distance between the outside edges of the first and
      // last
      // voxel along each dimension; one value for each dimension. If the fov is
      // not
      // specified it is calculated according to: fov = interval * dim

      // save and reset old locale
      const std::locale currentLocale = std::locale::global(std::locale::classic());
      sscanf(line, "%*s %f %f %f %f", fov, fov + 1, fov + 2, fov + 3);
      // reset locale
      std::locale::global(currentLocale);
      fov_specified = true;
    }
    else if (text.find("interval") < text.length())
    {
      // interval
      // The center to center distance between adjacent voxels along each
      // dimension;
      // one value for each dimension. If the interval is not specified it is
      // calculated according to: interval = fov / dim

      // save and reset old locale
      const std::locale currentLocale = std::locale::global(std::locale::classic());
      float             spacing[4];
      sscanf(line, "%*s %f %f %f %f", spacing, spacing + 1, spacing + 2, spacing + 3);
      // reset locale
      std::locale::global(currentLocale);
      for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
      {
        m_Spacing[i] = spacing[i];
      }
      spacing_specified = true;
    }
    else if (text.find("dataType") < text.length())
    {
      char pixelType[256];
      sscanf(line, "%*s %255s", pixelType);
      text = pixelType;
      SetPixelType(IOPixelEnum::SCALAR);
      if (text.find("BYTE") < text.length())
      {
        SetComponentType(IOComponentEnum::CHAR);
      }
      else if (text.find("WORD") < text.length())
      {
        SetComponentType(IOComponentEnum::SHORT);
      }
      else if (text.find("LWORD") < text.length())
      {
        SetComponentType(IOComponentEnum::INT);
      }
      else if (text.find("REAL") < text.length())
      {
        SetComponentType(IOComponentEnum::FLOAT);
      }
      else if (text.find("COMPLEX") < text.length())
      {
        SetPixelType(IOPixelEnum::VECTOR);
        SetComponentType(IOComponentEnum::DOUBLE);
      }
      else
      {
        itkExceptionStringMacro("Unrecognized type");
      }
    } // found scalars
    else if (text.find("displayRange") < text.length())
    {
      // displayRange:
      // Two values giving the low_value and high_value. Voxel values below the
      // low_value will be displayed as black and voxels with values above the
      // high_value will be displayed as white. Voxels with values within the
      // display
      // range are displayed with a grey value that is scaled linearly between
      // the
      // low_value and high_value.

      // save and reset old locale
      const std::locale currentLocale = std::locale::global(std::locale::classic());
      float             range[2];
      sscanf(line, "%*s %f %f", range, range + 1);
      // reset locale
      std::locale::global(currentLocale);
      m_DisplayRange[0] = range[0];
      m_DisplayRange[1] = range[1];
    }
    else if (text.find("fidName") < text.length())
    {
      // Not well documented
      // This is a bit tricky to get the value as there is sometime no white
      // space
      // only a ':' separate field from value, we assume there is no other ':'
      char * pch = strchr(line, ':');
      sscanf(++pch, "%255s", m_FidName); // delete any white space left
      itkDebugMacro("fidName was specified");
    }
    else if (text.find("sdtOrient") < text.length())
    {
      // Not used now, but later when  ITK Dictionary will be ready
      // don't know for now the format in which to save this.
      // This is a bit tricky to get the value as there is sometime no white
      // space
      // only a ':' separate field from value, we assume there is no other ':'
      char * pch = strchr(line, ':');
      sscanf(++pch, "%255s", m_SdtOrient); // delete any white space left
      itkDebugMacro("Orientation was specified");
    }
    else if (text.find("dsplyThres") < text.length())
    {
      // not documented
      itkDebugMacro("Display threshold was specified");
    }
    else if (text.find("endian") < text.length())
    {
      // BigEndian ieee-be / LittleEndian: ieee-le
      if (text.find("ieee-le") < text.length())
      {
        itkExceptionStringMacro("Little Endian Stimulate files are not handled.");
      }
    }
    else if (text.find("mapParmFileName") < text.length())
    {
      // not documented
      itkDebugMacro("mapParmFileName was specified");
    }
    else if (text.find("mapTypeName") < text.length())
    {
      // not documented
      itkDebugMacro("mapTypeName was specified");
    }
    else if (text.find("stimFileName:") < text.length())
    {
      // file data name is explicitly specified
      std::string datafilename;
      // Remove leading and trailing blanks
      itksys::RegularExpression regexp("stimFileName:[ ]*(.*)[ ]*$");
      if (!regexp.find(text))
      {
        itkExceptionStringMacro("Missing value for stimFileName attribute");
      }
      datafilename = regexp.match(1);

      // if the data filename has a directory specified, use it as is,
      // otherwise prepend the path of the .spr file.
      const std::string datafilenamePath = ::itksys::SystemTools::GetFilenamePath(datafilename);
      if (datafilenamePath.empty())
      {
        const std::string fileNamePath = ::itksys::SystemTools::GetFilenamePath(m_FileName);
        m_DataFileName = fileNamePath + "/" + datafilename;
      }
      else
      {
        m_DataFileName = datafilename;
      }
    }
    else if (text.find("mapConf") < text.length())
    {
      // not documented
      itkDebugMacro("mapConf was specified");
    }
    else if (text.find("periodStr") < text.length())
    {
      // not documented
      itkDebugMacro("periodStr was specified");
    }
  }

  // compute any missing informations:
  if (!spacing_specified && fov_specified)
  {
    for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
    {
      m_Spacing[i] = fov[i] / dims[i];
    }
  }
  if (!origin_specified && fov_specified)
  {
    for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
    {
      m_Origin[i] = (m_Spacing[i] - fov[i]) / 2.;
    }
  } // otherwise default spacing & origin are used.
}

void
StimulateImageIO::ReadImageInformation()
{
  std::ifstream file;

  this->InternalReadImageInformation(file);
}

bool
StimulateImageIO::CanWriteFile(const char * name)
{
  const std::string filename = name;

  if (filename.empty())
  {
    itkDebugMacro("No filename specified.");
    return false;
  }


  const bool extensionFound = this->HasSupportedWriteExtension(name);

  if (!extensionFound)
  {
    itkDebugMacro("The filename extension is not recognized");
    return false;
  }

  return true;
}

void
StimulateImageIO::Write(const void * buffer)
{
  std::ofstream file;
  this->OpenFileForWriting(file, m_FileName);

  // Check the image region for proper dimensions, etc.
  const unsigned int numDims = this->GetNumberOfDimensions();
  if (numDims < 2 || numDims > 4)
  {
    itkExceptionStringMacro("Stimulate Writer can only write 2,3 or 4-dimensional images");
  }

  // Write the Stimulate header information
  file << "numDim: " << this->GetNumberOfDimensions();

  // Write characteristics of the data
  file << "\ndim:";
  for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
  {
    file << ' ' << m_Dimensions[i];
  }

  file << "\norigin:";
  for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
  {
    file << ' ' << m_Origin[i];
  }

  file << "\nfov:";
  for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
  {
    file << ' ' << m_Spacing[i] * m_Dimensions[i]; // fov = interval * dim
  }

  file << "\ninterval:";
  for (unsigned int i = 0; i < m_NumberOfDimensions; ++i)
  {
    file << ' ' << m_Spacing[i];
  }

  // preparation for writing buffer:
  const auto numberOfBytes = static_cast<SizeValueType>(this->GetImageSizeInBytes());
  const auto numberOfComponents = static_cast<SizeValueType>(this->GetImageSizeInComponents());

  file << "\ndataType: ";
  {
    const auto tempmemory = make_unique_for_overwrite<char[]>(numberOfBytes);
    memcpy(tempmemory.get(), buffer, numberOfBytes);
    switch (this->GetComponentType())
    {
      case IOComponentEnum::SCHAR:
        file << "BYTE";
        // For CHAR, it is not necessary to swap bytes. (It would not have any effect anyway.)
        break;
      case IOComponentEnum::SHORT:
        file << "WORD";
        ByteSwapper<short>::SwapRangeFromSystemToBigEndian(reinterpret_cast<short *>(tempmemory.get()),
                                                           numberOfComponents);
        break;
      case IOComponentEnum::INT:
        file << "LWORD";
        ByteSwapper<int>::SwapRangeFromSystemToBigEndian(reinterpret_cast<int *>(tempmemory.get()), numberOfComponents);
        break;
      case IOComponentEnum::FLOAT:
        file << "REAL";
        ByteSwapper<float>::SwapRangeFromSystemToBigEndian(reinterpret_cast<float *>(tempmemory.get()),
                                                           numberOfComponents);
        break;
      case IOComponentEnum::DOUBLE:
        file << "COMPLEX";
        ByteSwapper<double>::SwapRangeFromSystemToBigEndian(reinterpret_cast<double *>(tempmemory.get()),
                                                            numberOfComponents);
        break;
      default:
        break;
    }

    // add the data filename to the header
    // determine datafile given the spr filename
    m_DataFileName = m_FileName;
    m_DataFileName.replace(m_DataFileName.length() - 3, 3, "sdt");
    file << "\nstimFileName: " << m_DataFileName;

    // Last carrier return:
    file << '\n';

    // actually read data file
    std::ofstream file_data;
    this->OpenFileForWriting(file_data, m_DataFileName);

    // Write the actual pixel data
    file_data.write(static_cast<const char *>(tempmemory.get()), numberOfBytes);
    file_data.close();
  }
  file.close();
}

void
StimulateImageIO::PrintSelf(std::ostream & os, Indent indent) const
{
  Superclass::PrintSelf(os, indent);
  os << indent << "\nDisplayRange: " << m_DisplayRange[0] << ' ' << m_DisplayRange[1];
}
} // end namespace itk
