/*=========================================================================
 *
 *  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 "itkVideoStream.h"

// Set up type alias
static constexpr unsigned int Dimension = 2;
using PixelType = unsigned char;
using FrameType = itk::Image<PixelType, Dimension>;
using VideoType = itk::VideoStream<FrameType>;
using SizeValueType = itk::SizeValueType;

/** Set up a spatial region with the given dimensions */
FrameType::RegionType
SetUpSpatialRegion(unsigned int x, unsigned int y)
{
  FrameType::RegionType            out;
  FrameType::RegionType::SizeType  size;
  FrameType::RegionType::IndexType start;

  size[0] = x;
  size[1] = y;
  start.Fill(0);
  out.SetSize(size);
  out.SetIndex(start);
  return out;
}

/**
 * Test the basic functionality of temporal data objects
 */
int
itkVideoStreamTest(int, char *[])
{
  //////
  // Test Instantiation and grafting
  //////

  // Instantiate a new VideoStream
  auto video1 = VideoType::New();

  // Check dimension
  if (video1->GetFrameDimension() != Dimension)
  {
    std::cerr << "GetFrameDimesion failed" << std::endl;
    return EXIT_FAILURE;
  }
  if (VideoType::FrameDimension != Dimension)
  {
    std::cerr << "VideoType::FrameDimension failed" << std::endl;
    return EXIT_FAILURE;
  }

  // Create a second VideoStream
  auto video2 = VideoType::New();

  // Use GetFrameBuffer and SetFrameBuffer to make video1 point to the same
  // buffer as video2
  video1->SetFrameBuffer(video2->GetFrameBuffer());
  if (video1->GetFrameBuffer() != video2->GetFrameBuffer())
  {
    std::cerr << "Failed to properly Get/Set frame buffer" << std::endl;
    return EXIT_FAILURE;
  }

  // Reset video1
  video1 = VideoType::New();

  // Set up regions for video2
  itk::TemporalRegion largestRegion;
  itk::TemporalRegion requestedRegion;
  itk::TemporalRegion bufferedRegion;
  largestRegion.SetFrameStart(0);
  largestRegion.SetFrameDuration(10);
  requestedRegion.SetFrameStart(1);
  requestedRegion.SetFrameDuration(8);
  bufferedRegion.SetFrameStart(0);
  bufferedRegion.SetFrameDuration(3);
  video2->SetLargestPossibleTemporalRegion(largestRegion);
  video2->SetRequestedTemporalRegion(requestedRegion);
  video2->SetBufferedTemporalRegion(bufferedRegion);

  // Test setting number of buffers on video2
  VideoType::BufferType::Pointer frameBuffer = video2->GetFrameBuffer();
  frameBuffer->SetNumberOfBuffers(4);

  // Check that number of buffers got set right
  frameBuffer = video2->GetFrameBuffer();
  if (frameBuffer->GetNumberOfBuffers() != 4)
  {
    std::cerr << "Number of buffers not correctly set" << std::endl;
    return EXIT_FAILURE;
  }

  // Test appending frames to video2
  auto frame1 = FrameType::New();
  auto frame2 = FrameType::New();
  auto frame3 = FrameType::New();
  video2->SetFrame(0, frame1);
  video2->SetFrame(1, frame2);
  video2->SetFrame(2, frame3);

  // Test retreiving frames
  FrameType::Pointer outFrame1 = video2->GetFrame(0);
  FrameType::Pointer outFrame2 = video2->GetFrame(1);
  FrameType::Pointer outFrame3 = video2->GetFrame(2);
  if (outFrame3 != frame3 || outFrame2 != frame2 || outFrame1 != frame1)
  {
    std::cerr << "Frames not retreived correctly" << std::endl;
    return EXIT_FAILURE;
  }

  // Make sure frames actually got buffered
  if (!frameBuffer->BufferIsFull(0) || !frameBuffer->BufferIsFull(1) || !frameBuffer->BufferIsFull(2) ||
      frameBuffer->BufferIsFull(3))
  {
    std::cerr << "Frames not correctly appended" << std::endl;
    return EXIT_FAILURE;
  }

  // Graft video2 onto video1 and check results
  video1->Graft(video2);
  if (video1->GetLargestPossibleTemporalRegion() != video2->GetLargestPossibleTemporalRegion() ||
      video1->GetRequestedTemporalRegion() != video2->GetRequestedTemporalRegion() ||
      video1->GetBufferedTemporalRegion() != video2->GetBufferedTemporalRegion())
  {
    std::cerr << "Graft failed to copy meta information" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameBuffer() != video2->GetFrameBuffer())
  {
    std::cerr << "Graft failed to graft frame buffer" << std::endl;
    return EXIT_FAILURE;
  }

  // For good measure, make sure that video1's frame buffer has 4 buffers
  if (video1->GetFrameBuffer()->GetNumberOfBuffers() != 4)
  {
    std::cerr << "Problem with graft's handling of frame buffer" << std::endl;
    return EXIT_FAILURE;
  }

  //////
  // Test Initialization, region setting, and allocation
  //////

  video1 = VideoType::New();

  // Set the buffered temporal region
  VideoType::TemporalRegionType temporalRegion;
  SizeValueType                 startFrame = 0;
  SizeValueType                 numFrames = 5;
  temporalRegion.SetFrameStart(startFrame);
  temporalRegion.SetFrameDuration(numFrames);
  video1->SetLargestPossibleTemporalRegion(temporalRegion);
  video1->SetRequestedTemporalRegion(temporalRegion);
  video1->SetBufferedTemporalRegion(temporalRegion);

  // Initialize all frames in the buffered temporal region
  video1->InitializeEmptyFrames();

  // Set the buffered spatial region for each frame
  FrameType::RegionType largestSpatialRegion = SetUpSpatialRegion(100, 100);
  FrameType::RegionType requestedSpatialRegion = SetUpSpatialRegion(40, 40);
  FrameType::RegionType bufferedSpatialRegion = SetUpSpatialRegion(50, 40);
  video1->SetAllLargestPossibleSpatialRegions(largestSpatialRegion);
  video1->SetAllRequestedSpatialRegions(requestedSpatialRegion);
  video1->SetAllBufferedSpatialRegions(bufferedSpatialRegion);

  // Make sure regions were set correctly
  for (SizeValueType i = startFrame; i < startFrame + numFrames; ++i)
  {
    if (video1->GetFrameLargestPossibleSpatialRegion(i) != largestSpatialRegion)
    {
      std::cerr << "Frame " << i << " largest possible spatial region not set correctly" << std::endl;
      return EXIT_FAILURE;
    }
    if (video1->GetFrameRequestedSpatialRegion(i) != requestedSpatialRegion)
    {
      std::cerr << "Frame " << i << " requested spatial region not set correctly" << std::endl;
      return EXIT_FAILURE;
    }
    if (video1->GetFrameBufferedSpatialRegion(i) != bufferedSpatialRegion)
    {
      std::cerr << "Frame " << i << " buffered spatial region not set correctly" << std::endl;
      return EXIT_FAILURE;
    }
  }

  // Allocate memory for the frames
  video1->Allocate();

  // Try to set and get a pixel on each frame
  FrameType::IndexType idx;
  idx[0] = 49;
  idx[1] = 39;
  for (SizeValueType i = startFrame; i < startFrame + numFrames; ++i)
  {
    FrameType * frame = video1->GetFrame(i);
    frame->SetPixel(idx, i);
  }
  for (SizeValueType i = startFrame; i < startFrame + numFrames; ++i)
  {
    FrameType * frame = video1->GetFrame(i);
    if (frame->GetPixel(idx) != i)
    {
      std::cerr << "Error setting/getting pixel for frame " << i << std::endl;
      return EXIT_FAILURE;
    }
  }

  //////
  // Test InitializeNewFrames with larger requested region. Make sure buffered
  // frames are still valid
  //////

  temporalRegion.SetFrameDuration(temporalRegion.GetFrameDuration() + 1);
  video1->SetLargestPossibleTemporalRegion(temporalRegion);
  video1->SetRequestedTemporalRegion(temporalRegion);
  video1->InitializeEmptyFrames();

  for (SizeValueType i = startFrame; i < startFrame + numFrames; ++i)
  {
    FrameType * frame = video1->GetFrame(i);
    if (frame->GetPixel(idx) != i)
    {
      std::cerr << "Error setting/getting pixel for frame " << i << std::endl;
      return EXIT_FAILURE;
    }
  }

  //////
  // Test meta data caching
  //////

  // Reset video1
  video1 = VideoType::New();
  itk::TemporalRegion tempReg = video1->GetLargestPossibleTemporalRegion();
  if (tempReg.GetFrameStart() != 0 || tempReg.GetFrameDuration() != 0)
  {
    std::cerr << "video1 initialized with non-empty temporal region" << std::endl;
    return EXIT_FAILURE;
  }

  // Set the cached meta-data for a non-buffered frame
  FrameType::RegionType            spatReg;
  FrameType::RegionType::SizeType  sz;
  FrameType::RegionType::IndexType start;
  FrameType::SpacingType           space;
  FrameType::PointType             orgn;
  FrameType::DirectionType         direction;
  sz[0] = 10;
  sz[1] = 20;
  start.Fill(0);
  spatReg.SetSize(sz);
  spatReg.SetIndex(start);
  space[0] = 0.1;
  space[1] = 0.5;
  orgn[0] = 5.432;
  orgn[1] = -23.4;
  direction[0][0] = 1;
  direction[0][1] = 2;
  direction[1][0] = -2;
  direction[1][1] = 1;
  video1->SetFrameLargestPossibleSpatialRegion(0, spatReg);
  video1->SetFrameRequestedSpatialRegion(0, spatReg);
  video1->SetFrameBufferedSpatialRegion(0, spatReg);
  video1->SetFrameSpacing(0, space);
  video1->SetFrameOrigin(0, orgn);
  video1->SetFrameDirection(0, direction);

  // Check retrieval while still unbuffered
  if (video1->GetFrameLargestPossibleSpatialRegion(0) != spatReg)
  {
    std::cerr << "video1 LargestPossibleSpatialRegion not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameRequestedSpatialRegion(0) != spatReg)
  {
    std::cerr << "video1 RequestedSpatialRegion not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameBufferedSpatialRegion(0) != spatReg)
  {
    std::cerr << "video1 BufferedSpatialRegion not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameSpacing(0) != space)
  {
    std::cerr << "video1 Spacing not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameOrigin(0) != orgn)
  {
    std::cerr << "video1 Origin not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (video1->GetFrameDirection(0) != direction)
  {
    std::cerr << "video1 Direction not cached correctly" << std::endl;
    return EXIT_FAILURE;
  }

  // Check meta data after frame initialized
  tempReg.SetFrameStart(0);
  tempReg.SetFrameDuration(1);
  video1->SetLargestPossibleTemporalRegion(tempReg);
  video1->SetRequestedTemporalRegion(tempReg);
  video1->SetBufferedTemporalRegion(tempReg);
  video1->InitializeEmptyFrames();
  FrameType * frame = video1->GetFrame(0);
  if (frame->GetLargestPossibleRegion() != spatReg)
  {
    std::cerr << "frame LargestPossibleRegion not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (frame->GetRequestedRegion() != spatReg)
  {
    std::cerr << "frame RequestedRegion not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (frame->GetBufferedRegion() != spatReg)
  {
    std::cerr << "frame BufferedRegion not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (frame->GetSpacing() != space)
  {
    std::cerr << "frame Spacing not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (frame->GetOrigin() != orgn)
  {
    std::cerr << "frame Origin not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }
  if (frame->GetDirection() != direction)
  {
    std::cerr << "frame Direction not initialized correctly" << std::endl;
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
