// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
// SPDX-License-Identifier: BSD-3-Clause

// This test checks the SplitExtent method of vtkThreadedImageAlgorithm.

#include "vtkNew.h"
#include "vtkObjectFactory.h"
#include "vtkThreadedImageAlgorithm.h"

#include <iostream>

#define TEST_SUCCESS 0
#define TEST_FAILURE 1

// A simple subclass of vtkThreadedImageAlgorithm to test
class ThreadedImageAlgorithmTester : public vtkThreadedImageAlgorithm
{
public:
  static ThreadedImageAlgorithmTester* New();
  vtkTypeMacro(ThreadedImageAlgorithmTester, vtkThreadedImageAlgorithm);
  void SetSplitPath(const int path[3], int len);
  bool TestSplitExtent(int extent[6], vtkIdType pieces);
};

vtkStandardNewMacro(ThreadedImageAlgorithmTester);

// The SplitPath is protected, so add a method to set it.
void ThreadedImageAlgorithmTester::SetSplitPath(const int path[3], int len)
{
  len = (len < 0 ? 0 : len);
  len = (len > 3 ? 3 : len);
  for (int i = 0; i < len; i++)
  {
    this->SplitPath[i] = path[i];
  }
  this->SplitPathLength = len;
}

// Test splitting the extent into the given number of pieces.
bool ThreadedImageAlgorithmTester::TestSplitExtent(int extent[6], vtkIdType pieces)
{
  bool success = true;

  int size[3] = { extent[1] - extent[0] + 1, extent[3] - extent[2] + 1, extent[5] - extent[4] + 1 };

  vtkIdType n = this->SplitExtent(nullptr, extent, 0, pieces);

  int divs[3] = { 1, 1, 1 };
  vtkIdType inc = 1;
  int checkAxis = 0;
  vtkIdType i;
  for (i = 0; i < n; i += inc)
  {
    int split[6];
    vtkIdType m = this->SplitExtent(split, extent, i, pieces);

    // accelerate check by increasing increment after first row
    if (checkAxis < 1 && split[2] > 0)
    {
      divs[checkAxis] = i;
      inc = i;
      checkAxis = 1;
    }
    if (checkAxis < 2 && split[4] > 0)
    {
      divs[checkAxis] = i / inc;
      inc = i;
      checkAxis = 2;
    }

    // check for constancy of division
    if (m != n)
    {
      std::cerr << "SplitExtent changed the number of pieces from " << n << " to " << m << "!\n";
      success = false;
      break;
    }

    // check that SplitPath was used
    if (this->SplitPathLength < 3)
    {
      int k;
      for (k = 0; k < 3; k++)
      {
        if ((this->SplitPathLength == 1 && k != this->SplitPath[0]) ||
          (k != this->SplitPath[0] && k != this->SplitPath[1]))
        {
          if (split[2 * k] != extent[2 * k] || split[2 * k + 1] != extent[2 * k + 1])
          {
            success = false;
            break;
          }
        }
      }
      if (!success)
      {
        std::cerr << "Split axis " << k << " is not in split path!\n";
        break;
      }
    }

    // check that MinimumPieceSize was used
    for (int j = 0; j < 3; j++)
    {
      if (size[j] >= this->MinimumPieceSize[j] &&
        split[2 * j + 1] - split[2 * j] + 1 < this->MinimumPieceSize[j])
      {
        success = false;
      }
    }
    if (!success)
    {
      std::cerr << "Split piece [" << split[0] << " " << split[1] << " " << split[2] << " "
                << split[3] << " " << split[4] << " " << split[5]
                << "] smaller than MinimumPieceSize!\n";
      break;
    }
  }

  // check that the maximum possible split was used
  if (success)
  {
    divs[checkAxis] = i / inc;

    for (int j = 0; j < this->SplitPathLength; j++)
    {
      int k = this->SplitPath[j];
      if ((divs[k] + 1) * this->MinimumPieceSize[k] <= size[k] &&
        (divs[k] + 1) * divs[(k + 1) % 3] * divs[(k + 2) % 3] <= pieces)
      {
        std::cerr << "Divisions [" << divs[0] << " " << divs[1] << " " << divs[2];
        std::cerr << "] could be increased along axis " << k << "\n";
        success = false;
      }
    }
  }

  if (!success)
  {
    std::cerr << "Extent: " << extent[0] << " " << extent[1] << " " << extent[2] << " " << extent[3]
              << " " << extent[4] << " " << extent[5] << "\n";
    std::cerr << "Piece: " << i << " of " << n << "\n";
    std::cerr << "MinimumPieceSize: " << this->MinimumPieceSize[0] << " "
              << this->MinimumPieceSize[1] << " " << this->MinimumPieceSize[2] << "\n";
    switch (this->SplitMode)
    {
      case SLAB:
        std::cerr << "SplitMode: Slab\n";
        break;
      case BEAM:
        std::cerr << "SplitMode: Beam\n";
        break;
      case BLOCK:
        std::cerr << "SplitMode: Block\n";
        break;
      default:
        std::cerr << "SplitMode: Unknown\n";
        break;
    }
    std::cerr << "SplitPath:";
    for (int k = 0; k < this->SplitPathLength; k++)
    {
      std::cerr << " " << this->SplitPath[k];
    }
    std::cerr << "\n";
  }

  return success;
}

int TestThreadedImageAlgorithmSplitExtent(int, char*[])
{
  vtkNew<ThreadedImageAlgorithmTester> tester;

  constexpr int splitPaths[6][3] = {
    { 2, 1, 0 },
    { 1, 2, 0 },
    { 0, 2, 1 },
    { 2, 0, 1 },
    { 1, 0, 2 },
    { 0, 1, 2 },
  };

  for (int mode = 0; mode < 3; mode++)
  {
    if (mode == 0)
    {
      tester->SetSplitModeToSlab();
    }
    else if (mode == 1)
    {
      tester->SetSplitModeToBeam();
    }
    else
    {
      tester->SetSplitModeToBlock();
    }

    for (int xmin = 1; xmin <= 8; xmin++)
    {
      for (int ymin = 1; ymin <= 7; ymin += 3)
      {
        for (int zmin = 1; zmin <= 8; zmin += 7)
        {
          tester->SetMinimumPieceSize(xmin, ymin, zmin);

          for (int path = 0; path < 15; path++)
          {
            tester->SetSplitPath(splitPaths[path % 6], 3 - path / 6);

            for (int xsize = 1; xsize <= 100; xsize += 99)
            {
              for (int ysize = 1; ysize <= 92; ysize += 13)
              {
                for (int zsize = 1; zsize <= 10; zsize++)
                {
                  int extent[6] = { 0, xsize - 1, 0, ysize - 1, 0, zsize - 1 };

                  vtkIdType maxpieces = xsize * ysize * zsize;
                  maxpieces = (maxpieces > 100 ? 100 : maxpieces);
                  vtkIdType inc = 1 + maxpieces / 5;
                  for (vtkIdType pieces = 1; pieces <= maxpieces; pieces += inc)
                  {
                    if (!tester->TestSplitExtent(extent, pieces))
                    {
                      return TEST_FAILURE;
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  return TEST_SUCCESS;
}
