/*=========================================================================
 *
 *  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.
 *
 *=========================================================================*/
#include "itkFileListVideoIO.h"

#include "itkImageIOFactory.h"
#include "itkPrintHelper.h"

namespace itk
{

FileListVideoIO::FileListVideoIO() { this->ResetMembers(); }

FileListVideoIO::~FileListVideoIO() { this->FinishReadingOrWriting(); }

void
FileListVideoIO::FinishReadingOrWriting()
{
  this->ResetMembers();
}

void
FileListVideoIO::SetFileName(const std::string & fileList)
{
  this->SetFileName(fileList.c_str());
}

void
FileListVideoIO::SetFileName(const char * fileList)
{
  m_FileNames = this->SplitFileNames(fileList);

  // Set the number of frames
  m_FrameTotal = static_cast<SizeValueType>(m_FileNames.size());

  this->Modified();
}

std::vector<std::string>
FileListVideoIO::SplitFileNames(const std::string & fileList)
{
  std::vector<std::string> out;

  size_t pos = 0;
  size_t len = fileList.length();
  while (pos != std::string::npos && len > 0)
  {
    // Get the substring
    const std::string str = fileList.substr(pos, len);

    // Update pos
    pos = str.find(',');

    // Add the filename to the list
    out.push_back(str.substr(0, pos));

    // Move past the delimiter
    if (pos != std::string::npos)
    {
      ++pos;
    }
    len -= pos;
  }

  return out;
}

void
FileListVideoIO::SetReadFromFile()
{
  if (!m_ReaderOpen && !m_WriterOpen)
  {
    m_ReadFrom = ReadFromEnum::ReadFromFile;
  }
  else
  {
    itkExceptionStringMacro("Cannot change read type while reader is open");
  }
}

void
FileListVideoIO::SetReadFromCamera()
{
  itkExceptionStringMacro("Read From Camera is not supported by this VideoIO");
}

bool
FileListVideoIO::CanReadFile(const char * filename)
{
  // Make sure file names have been specified
  const std::string        strFileName = filename;
  std::vector<std::string> fileList = this->SplitFileNames(strFileName);
  if (fileList.empty())
  {
    return false;
  }

  // Make sure all file names have the same extension
  if (!VerifyExtensions(fileList))
  {
    return false;
  }

  // Make sure we can instantiate an ImageIO to read the first file
  const ImageIOBase::Pointer ioTemp =
    ImageIOFactory::CreateImageIO(fileList[0].c_str(), ImageIOFactory::IOFileModeEnum::ReadMode);
  if (ioTemp.IsNull())
  {
    return false;
  }

  return true;
}

bool
FileListVideoIO::CanReadCamera(itk::SizeValueType itkNotUsed(cameraID)) const
{
  return false;
}

void
FileListVideoIO::ReadImageInformation()
{
  // Open from a file
  if (m_ReadFrom == ReadFromEnum::ReadFromFile)
  {

    // Make sure file can be read
    const std::string filename = m_FileNames[0];
    if (!this->CanReadFile(filename.c_str()))
    {
      itkExceptionMacro("Cannot Read File: " << filename);
    }

    // Open the internal IO (don't use local so that spacing, origin, direction
    // can be retrieved)
    if (!m_ReaderOpen)
    {
      this->OpenReader();
    }

    m_ImageIO->SetFileName(m_FileNames[0].c_str());
    m_ImageIO->ReadImageInformation();

    // No I-Frame issues to worry about
    m_IFrameInterval = 1;
    m_LastIFrame = m_FrameTotal - 1;

    // Fill dimensions and origin
    const unsigned int numberOfDimensions = m_ImageIO->GetNumberOfDimensions();
    this->SetNumberOfDimensions(numberOfDimensions);

    for (unsigned int i = 0; i < numberOfDimensions; ++i)
    {
      m_Dimensions[i] = m_ImageIO->GetDimensions(i);
      m_Origin.push_back(m_ImageIO->GetOrigin(i));
    }

    // Get other image info
    m_NumberOfComponents = m_ImageIO->GetNumberOfComponents();
    m_ComponentType = m_ImageIO->GetComponentType();
    m_PixelType = m_ImageIO->GetPixelType();

    // Make up an arbitrary FramesPerSecond of 1
    m_FramesPerSecond = 1;
  }

  // Open capture from a camera
  else if (m_ReadFrom == ReadFromEnum::ReadFromCamera)
  {
    itkExceptionStringMacro("FileListVideoIO cannot read from a camera");
  }

  // Should never get here
  else
  {
    itkExceptionStringMacro("Invalid Read Type... How did we get here?");
  }
}

void
FileListVideoIO::Read(void * buffer)
{
  // Make sure we've already called ReadImageInformation (dimensions are
  // non-zero)
  if (m_Dimensions[0] == 0)
  {
    itkExceptionStringMacro("Cannot read frame with zero dimension. May need to call ReadImageInformation");
  }

  // If video is not already open, open it and keep it open
  if (!m_ReaderOpen)
  {
    this->OpenReader();
  }

  // Read the desired frame
  m_ImageIO->SetFileName(m_FileNames[m_CurrentFrame]);
  m_ImageIO->Read(buffer);

  // Move on to the next frame
  if (m_CurrentFrame < m_FrameTotal - 1)
  {
    ++m_CurrentFrame;
  }
}

bool
FileListVideoIO::SetNextFrameToRead(FrameOffsetType frameNumber)
{
  if (frameNumber >= m_FrameTotal)
  {
    return false;
  }

  m_CurrentFrame = frameNumber;
  return true;
}

double
FileListVideoIO::GetSpacing(unsigned int i) const
{
  if (!m_ReaderOpen)
  {
    itkExceptionStringMacro("Can't get Spacing without ImageIO open for reading");
  }
  return m_ImageIO->GetSpacing(i);
}

double
FileListVideoIO::GetOrigin(unsigned int i) const
{
  if (!m_ReaderOpen)
  {
    itkExceptionStringMacro("Can't get Origin without ImageIO open for reading");
  }
  return m_ImageIO->GetOrigin(i);
}

std::vector<double>
FileListVideoIO::GetDirection(unsigned int i) const
{
  if (!m_ReaderOpen)
  {
    itkExceptionStringMacro("Can't get Direction without ImageIO open for reading");
  }
  return m_ImageIO->GetDirection(i);
}

bool
FileListVideoIO::CanWriteFile(const char * filename)
{
  // Make sure file names have been specified
  std::vector<std::string> fileList = this->SplitFileNames(filename);
  if (fileList.empty())
  {
    return false;
  }

  // Make sure all file names have the same extension
  if (!VerifyExtensions(fileList))
  {
    return false;
  }

  // Make sure we can instantiate an ImageIO to write the first file
  const ImageIOBase::Pointer ioTemp =
    ImageIOFactory::CreateImageIO(fileList[0].c_str(), ImageIOFactory::IOFileModeEnum::WriteMode);
  if (ioTemp.IsNull())
  {
    return false;
  }

  return true;
}

void
FileListVideoIO::WriteImageInformation()
{
  if (!m_WriterOpen)
  {
    this->OpenWriter();
  }
  m_ImageIO->SetFileName(m_FileNames[m_CurrentFrame]);
  m_ImageIO->WriteImageInformation();
}

void
FileListVideoIO::SetWriterParameters(TemporalRatioType                  framesPerSecond,
                                     const std::vector<SizeValueType> & dim,
                                     const char *                       itkNotUsed(fourCC),
                                     unsigned int                       nChannels,
                                     IOComponentEnum                    itkNotUsed(componentType))
{
  m_Dimensions.clear();
  m_Origin.clear();
  m_NumberOfComponents = nChannels;
  for (const auto & i : dim)
  {
    m_Dimensions.push_back(i);
    m_Origin.push_back(0);
  }

  // Set FramesPerSecond even though we're not going to use it
  m_FramesPerSecond = framesPerSecond;
}

void
FileListVideoIO::Write(const void * buffer)
{

  // Make sure parameters are specified
  if (m_Dimensions.empty())
  {
    itkExceptionStringMacro("Cannot write with empty parameters. You probably need to call SetWriterParameters");
  }

  // If the writer isn't open yet, open it
  if (!m_WriterOpen)
  {
    this->OpenWriter();
  }

  // Set the properties to the ImageIO
  m_ImageIO->SetNumberOfDimensions(static_cast<unsigned int>(m_Dimensions.size()));
  for (size_t i = 0; i < m_Dimensions.size(); ++i)
  {
    m_ImageIO->SetDimensions(static_cast<unsigned int>(i), m_Dimensions[i]);
    m_ImageIO->SetOrigin(static_cast<unsigned int>(i), m_Origin[i]);
  }
  m_ImageIO->SetNumberOfComponents(m_NumberOfComponents);

  // Write with the ImageIO
  m_ImageIO->SetFileName(m_FileNames[m_CurrentFrame]);
  m_ImageIO->Write(buffer);

  // Move to the next frame
  if (m_CurrentFrame < m_FrameTotal - 1)
  {
    ++m_CurrentFrame;
  }
}

void
FileListVideoIO::OpenReader()
{
  if (m_ReaderOpen)
  {
    itkExceptionStringMacro("Cannot open reader while video is already open for reading");
  }

  if (m_WriterOpen)
  {
    itkExceptionStringMacro("Cannot open reader while video is already open for writing");
  }

  // Make sure FileNames have been specified
  if (m_FileNames.empty())
  {
    itkExceptionStringMacro("Cannot open reader without file names set");
  }

  // If neither reader nor writer is currently open, open the reader
  if (m_ReadFrom == ReadFromEnum::ReadFromFile)
  {
    // Use the ImageIOFactory to instantiate a working ImageIO
    m_ImageIO = ImageIOFactory::CreateImageIO(m_FileNames[0].c_str(), ImageIOFactory::IOFileModeEnum::ReadMode);
    if (!m_ImageIO.IsNull())
    {
      m_ReaderOpen = true;
    }
    else
    {
      itkExceptionStringMacro("Video failed to open");
    }
  }
  else if (m_ReadFrom == ReadFromEnum::ReadFromCamera)
  {
    itkExceptionStringMacro("FileListVideoIO doesn't support reading from camera");
  }
  else
  {
    itkExceptionStringMacro("FileListVideoIO doesn't support reading from unknown ReadType");
  }
}

void
FileListVideoIO::OpenWriter()
{
  if (m_WriterOpen)
  {
    itkExceptionStringMacro("Cannot open writer while video is already open for writing");
  }

  if (m_ReaderOpen)
  {
    itkExceptionStringMacro("Cannot open writer while video is already open for reading");
  }

  // Make sure FileNames have been specified
  if (m_FileNames.empty())
  {
    itkExceptionStringMacro("Cannot open reader without file names set");
  }

  // If neither reader nor writer is currently open, open the writer
  m_ImageIO = ImageIOFactory::CreateImageIO(m_FileNames[0].c_str(), ImageIOFactory::IOFileModeEnum::WriteMode);
  if (!m_ImageIO.IsNull())
  {
    m_WriterOpen = true;
  }
  else
  {
    itkExceptionStringMacro("File failed to open for writing");
  }
}

void
FileListVideoIO::ResetMembers()
{
  m_ImageIO = nullptr;
  m_FileNames.clear();
  m_WriterOpen = false;
  m_ReaderOpen = false;
  m_FramesPerSecond = 0;
  m_Dimensions.clear();
  m_FrameTotal = 0;
  m_CurrentFrame = 0;
  m_NumberOfComponents = 0;
  m_IFrameInterval = 0;
  m_LastIFrame = 0;

  // Default to reading from a file
  m_ReadFrom = ReadFromEnum::ReadFromFile;

  // Members from ImageIOBase
  m_PixelType = IOPixelEnum::SCALAR;
  m_ComponentType = IOComponentEnum::UCHAR;
  this->SetNumberOfDimensions(2);
  m_Spacing[0] = 1.0;
  m_Spacing[1] = 1.0;
  m_Origin[0] = 0.0;
  m_Origin[1] = 0.0;
}

bool
FileListVideoIO::VerifyExtensions(const std::vector<std::string> & fileList) const
{
  for (size_t i = 1; i < fileList.size(); ++i)
  {
    const size_t prevExtPos = fileList[i - 1].rfind('.');
    const size_t extPos = fileList[i].rfind('.');
    if (prevExtPos == std::string::npos || extPos == std::string::npos)
    {
      return false;
    }
    const std::string prevExt = fileList[i - 1].substr(prevExtPos + 1, fileList[i - 1].length() - prevExtPos - 1);
    const std::string ext = fileList[i].substr(extPos + 1, fileList[i].length() - extPos - 1);

    if (strcmp(prevExt.c_str(), ext.c_str()))
    {
      return false;
    }
  }
  return true;
}

void
FileListVideoIO::PrintSelf(std::ostream & os, Indent indent) const
{
  using namespace print_helper;

  Superclass::PrintSelf(os, indent);

  itkPrintSelfObjectMacro(ImageIO);

  os << indent << "FileNames: " << m_FileNames << std::endl;
}

} // end namespace itk
