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

  Program:   Visualization Toolkit
  Module:    vtkJoinTables.txx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm 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.

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

//------------------------------------------------------------------------------
template <typename KeyColType, typename KeyValues>
KeyValues GetCell(KeyColType*, int);

//------------------------------------------------------------------------------
template <typename KeyColType, typename KeyValues>
void InsertNext(KeyColType*, KeyValues);

//------------------------------------------------------------------------------
template <>
inline void InsertNext<vtkDataArray, double>(vtkDataArray* col, double val)
{
  col->InsertNextTuple(&val);
}

//------------------------------------------------------------------------------
template <>
inline void InsertNext<vtkStringArray, vtkStdString>(vtkStringArray* col, vtkStdString val)
{
  col->InsertNextValue(val);
}

//------------------------------------------------------------------------------
template <>
inline double GetCell<vtkDataArray, double>(vtkDataArray* col, int index)
{
  return col->GetTuple(index)[0];
}

//------------------------------------------------------------------------------
template <>
inline vtkStdString GetCell<vtkStringArray, vtkStdString>(vtkStringArray* col, int index)
{
  return (col->GetValue(index));
}

//------------------------------------------------------------------------------
template <typename ColType, typename KeyColType, typename KeyValues>
void vtkJoinTables::MergeColumn(ColType* outputColumn, ColType* Column, KeyColType* outputKeyCol,
  const char* name, std::map<KeyValues, int> map)
{

  outputColumn->SetName(name);
  outputColumn->SetNumberOfComponents(Column->GetNumberOfComponents());
  // Loop over numeric tuples
  for (int i = 0; i < outputKeyCol->GetNumberOfValues(); i++)
  {
    KeyValues id = GetCell<KeyColType, KeyValues>(outputKeyCol, i);
    if (auto numericColumn = vtkDataArray::SafeDownCast(Column))
    {
      auto outputNumericColumn = vtkDataArray::SafeDownCast(outputColumn);
      if (map.count(id))
      {
        double* val = numericColumn->GetTuple(map.find(id)->second);
        outputNumericColumn->InsertNextTuple(val);
      }
      else
      {
        double val = this->ReplacementValue;
        outputNumericColumn->InsertNextTuple(&val);
      }
    }
    else if (auto stringColumn = vtkStringArray::SafeDownCast(Column))
    {
      auto outputStringColumn = vtkStringArray::SafeDownCast(outputColumn);
      if (map.count(id))
      {
        auto val = stringColumn->GetValue(map.find(id)->second);
        outputStringColumn->InsertNextValue(val);
      }
      else
      {
        auto val = vtkStdString("");
        outputStringColumn->InsertNextValue(val);
      }
    }
    else
    {
      vtkWarningMacro("Key column type is unsupported.");
    }
  }
}

//------------------------------------------------------------------------------
template <typename KeyColType, typename KeyValues>
void vtkJoinTables::JoinAlgorithm(vtkTable* left, vtkTable* right, vtkTable* output,
  KeyColType* leftKeyCol, KeyColType* rightKeyCol, Maps<KeyValues>* maps)
{
  /*
   * 1- First column of the output table will be the key. Start by filling this column with adequate
   *    values according to the join method.
   */
  vtkIdType leftSize = leftKeyCol->GetNumberOfTuples();
  vtkIdType rightSize = rightKeyCol->GetNumberOfTuples();
  vtkSmartPointer<KeyColType> outputKeyCol = leftKeyCol->NewInstance();
  outputKeyCol->SetName(this->LeftKey.c_str());

  switch (this->Mode)
  {
    case INTERSECTION:
    {
      for (int i = 0; i < leftSize; i++)
      {
        for (int j = 0; j < rightSize; j++)
        {
          if (GetCell<KeyColType, KeyValues>(leftKeyCol, i) ==
            GetCell<KeyColType, KeyValues>(rightKeyCol, j))
          {
            auto val = GetCell<KeyColType, KeyValues>(leftKeyCol, i);
            InsertNext<KeyColType, KeyValues>(outputKeyCol, val);
            maps->left.insert(std::make_pair(val, i));
            maps->right.insert(std::make_pair(val, j));
            break;
          }
        }
      }
      break;
    }

    case UNION:
    {
      for (int i = 0; i < rightSize; i++)
      {
        auto val = GetCell<KeyColType, KeyValues>(rightKeyCol, i);
        maps->right.insert(std::make_pair(val, i));
        bool existsInLeftKeyCol = false;
        for (int j = 0; j < leftSize; j++)
        {
          if (i == 0)
          {
            auto val0 = GetCell<KeyColType, KeyValues>(leftKeyCol, j);
            InsertNext<KeyColType, KeyValues>(outputKeyCol, val0);
            maps->left.insert(std::make_pair(val0, j));
          }

          if (!existsInLeftKeyCol &&
            GetCell<KeyColType, KeyValues>(leftKeyCol, j) ==
              GetCell<KeyColType, KeyValues>(rightKeyCol, i))
          {
            // Flag the value from the right table when it's a duplicate from the
            // left table
            existsInLeftKeyCol = true;
          }
        }
        if (!existsInLeftKeyCol)
        {
          // Insert only the values of right that do not exist in left
          InsertNext<KeyColType, KeyValues>(outputKeyCol, val);
        }
      }
      break;
    }

    case LEFT:
    {
      for (int i = 0; i < leftSize; i++)
      {
        auto val = GetCell<KeyColType, KeyValues>(leftKeyCol, i);
        InsertNext<KeyColType, KeyValues>(outputKeyCol, val);
        maps->left.insert(std::make_pair(val, i));
      }
      for (int j = 0; j < rightSize; j++)
      {
        auto val = GetCell<KeyColType, KeyValues>(rightKeyCol, j);
        maps->right.insert(std::make_pair(val, j));
      }
      break;
    }

    case RIGHT:
    {
      for (int i = 0; i < leftSize; i++)
      {
        auto val = GetCell<KeyColType, KeyValues>(leftKeyCol, i);
        maps->left.insert(std::make_pair(val, i));
      }
      for (int j = 0; j < rightSize; j++)
      {
        auto val = GetCell<KeyColType, KeyValues>(rightKeyCol, j);
        InsertNext<KeyColType, KeyValues>(outputKeyCol, val);
        maps->right.insert(std::make_pair(val, j));
      }
      break;
    }
    default:
      vtkErrorMacro("Unknown Mode");
  }
  output->AddColumn(outputKeyCol);

  /*
   * 2- Now fill the rest of the columns. Add the columns from the left table, and then the
   *    columns from the right table.
   */

  // left table and LeftKey
  for (int lc = 0; lc < left->GetNumberOfColumns(); lc++)
  {
    if (std::string(left->GetColumnName(lc)) == this->LeftKey)
    {
      // do not copy the original key column
      continue;
    }
    vtkAbstractArray* currentColumn = left->GetColumn(lc);
    if (auto numericColumn = vtkDataArray::SafeDownCast(currentColumn))
    {
      vtkDataArray* outputColumn = numericColumn->NewInstance();
      this->MergeColumn<vtkDataArray, KeyColType, KeyValues>(
        outputColumn, numericColumn, outputKeyCol, left->GetColumnName(lc), maps->left);
      output->AddColumn(outputColumn);
      outputColumn->Delete();
    }
    else if (auto stringColumn = vtkStringArray::SafeDownCast(currentColumn))
    {
      vtkStringArray* outputColumn = stringColumn->NewInstance();
      this->MergeColumn<vtkStringArray, KeyColType, KeyValues>(
        outputColumn, stringColumn, outputKeyCol, left->GetColumnName(lc), maps->left);
      output->AddColumn(outputColumn);
      outputColumn->Delete();
    }
  }

  // right table and RightKey
  for (int lc = 0; lc < right->GetNumberOfColumns(); lc++)
  {
    if (std::string(right->GetColumnName(lc)) == this->RightKey)
    {
      // do not copy the original key column
      continue;
    }
    auto currentColumn = right->GetColumn(lc);
    if (output->GetColumnByName(right->GetColumnName(lc)))
    {
      // We have a duplicate column name. Add suffixes.
      output->GetColumnByName(right->GetColumnName(lc))
        ->SetName((std::string(right->GetColumnName(lc)) + "_0").c_str());
      currentColumn->SetName((std::string(right->GetColumnName(lc)) + "_1").c_str());
    }
    else
    {
      currentColumn->SetName(right->GetColumnName(lc));
    }

    if (auto numericColumn = vtkDataArray::SafeDownCast(currentColumn))
    {
      vtkDataArray* outputColumn = numericColumn->NewInstance();
      this->MergeColumn<vtkDataArray, KeyColType, KeyValues>(
        outputColumn, numericColumn, outputKeyCol, right->GetColumnName(lc), maps->right);
      output->AddColumn(outputColumn);
      outputColumn->Delete();
    }
    else if (auto stringColumn = vtkStringArray::SafeDownCast(currentColumn))
    {
      vtkStringArray* outputColumn = stringColumn->NewInstance();
      this->MergeColumn<vtkStringArray, KeyColType, KeyValues>(
        outputColumn, stringColumn, outputKeyCol, right->GetColumnName(lc), maps->right);
      output->AddColumn(outputColumn);
      outputColumn->Delete();
    }
  }
  outputKeyCol->Delete();
}
