//
//  IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.
//
//  By downloading, copying, installing or using the software you agree to this license.
//  If you do not agree to this license, do not download, install,
//  copy or use the software.
//
//
//                          License Agreement
//                For Open Source Computer Vision Library
//
// Copyright (C) 2014, OpenCV Foundation, all rights reserved.
// Third party copyrights are property of their respective owners.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//   * Redistribution's of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//
//   * Redistribution's in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//   * The name of the copyright holders may not be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// This software is provided by the copyright holders and contributors "as is" and
// any express or implied warranties, including, but not limited to, the implied
// warranties of merchantability and fitness for a particular purpose are disclaimed.
// In no event shall the Intel Corporation or contributors be liable for any direct,
// indirect, incidental, special, exemplary, or consequential damages
// (including, but not limited to, procurement of substitute goods or services;
// loss of use, data, or profits; or business interruption) however caused
// and on any theory of liability, whether in contract, strict liability,
// or tort (including negligence or otherwise) arising in any way out of
// the use of this software, even if advised of the possibility of such damage.
//
// Author: Tolga Birdal <tbirdal AT gmail.com>

#include "precomp.hpp"

namespace cv
{
namespace ppf_match_3d
{

typedef cv::flann::L2<float> Distance_32F;
typedef cv::flann::GenericIndex< Distance_32F > FlannIndex;

void shuffle(int *array, size_t n);
Mat genRandomMat(int rows, int cols, double mean, double stddev, int type);
void getRandQuat(double q[4]);
void getRandomRotation(double R[9]);
void meanCovLocalPC(const float* pc, const int ws, const int point_count, double CovMat[3][3], double Mean[4]);
void meanCovLocalPCInd(const float* pc, const int* Indices, const int ws, const int point_count, double CovMat[3][3], double Mean[4]);

Mat loadPLYSimple(const char* fileName, int withNormals)
{
  Mat cloud;
  int numVertices=0;

  std::ifstream ifs(fileName);

  if (!ifs.is_open())
  {
    printf("Cannot open file...\n");
    return Mat();
  }

  std::string str;
  while (str.substr(0, 10) !="end_header")
  {
    std::string entry = str.substr(0, 14);
    if (entry == "element vertex")
    {
      numVertices = atoi(str.substr(15, str.size()-15).c_str());
    }
    std::getline(ifs, str);
  }

  if (withNormals)
    cloud=Mat(numVertices, 6, CV_32FC1);
  else
    cloud=Mat(numVertices, 3, CV_32FC1);

  for (int i = 0; i < numVertices; i++)
  {
    float* data = (float*)(&cloud.data[i*cloud.step[0]]);
    if (withNormals)
    {
      ifs >> data[0] >> data[1] >> data[2] >> data[3] >> data[4] >> data[5];

      // normalize to unit norm
      double norm = sqrt(data[3]*data[3] + data[4]*data[4] + data[5]*data[5]);
      if (norm>0.00001)
      {
        data[3]/=(float)norm;
        data[4]/=(float)norm;
        data[5]/=(float)norm;
      }
    }
    else
    {
      ifs >> data[0] >> data[1] >> data[2];
    }
  }

  //cloud *= 5.0f;
  return cloud;
}

void writePLY(Mat PC, const char* FileName)
{
  std::ofstream outFile( FileName );

  if ( !outFile )
  {
    //cerr << "Error opening output file: " << FileName << "!" << endl;
    printf("Error opening output file: %s!\n", FileName);
    exit( 1 );
  }

  ////
  // Header
  ////

  const int pointNum = ( int ) PC.rows;
  const int vertNum  = ( int ) PC.cols;

  outFile << "ply" << std::endl;
  outFile << "format ascii 1.0" << std::endl;
  outFile << "element vertex " << pointNum << std::endl;
  outFile << "property float x" << std::endl;
  outFile << "property float y" << std::endl;
  outFile << "property float z" << std::endl;
  if (vertNum==6)
  {
    outFile << "property float nx" << std::endl;
    outFile << "property float ny" << std::endl;
    outFile << "property float nz" << std::endl;
  }
  outFile << "end_header" << std::endl;

  ////
  // Points
  ////

  for ( int pi = 0; pi < pointNum; ++pi )
  {
    const float* point = (float*)(&PC.data[ pi*PC.step ]);

    outFile << point[0] << " "<<point[1]<<" "<<point[2];

    if (vertNum==6)
    {
      outFile<<" " << point[3] << " "<<point[4]<<" "<<point[5];
    }

    outFile << std::endl;
  }

  return;
}

Mat samplePCUniform(Mat PC, int sampleStep)
{
  int numRows = PC.rows/sampleStep;
  Mat sampledPC = Mat(numRows, PC.cols, PC.type());

  int c=0;
  for (int i=0; i<PC.rows && c<numRows; i+=sampleStep)
  {
    PC.row(i).copyTo(sampledPC.row(c++));
  }

  return sampledPC;
}

Mat samplePCUniformInd(Mat PC, int sampleStep, std::vector<int> &indices)
{
  int numRows = cvRound((double)PC.rows/(double)sampleStep);
  indices.resize(numRows);
  Mat sampledPC = Mat(numRows, PC.cols, PC.type());

  int c=0;
  for (int i=0; i<PC.rows && c<numRows; i+=sampleStep)
  {
    indices[c] = i;
    PC.row(i).copyTo(sampledPC.row(c++));
  }

  return sampledPC;
}

void* indexPCFlann(Mat pc)
{
  Mat dest_32f;
  pc.colRange(0,3).copyTo(dest_32f);
  return new FlannIndex(dest_32f, cvflann::KDTreeSingleIndexParams(8));
}

void destroyFlann(void* flannIndex)
{
  delete ((FlannIndex*)flannIndex);
}

// For speed purposes this function assumes that PC, Indices and Distances are created with continuous structures
void queryPCFlann(void* flannIndex, Mat& pc, Mat& indices, Mat& distances)
{
  Mat obj_32f;
  pc.colRange(0,3).copyTo(obj_32f);
  ((FlannIndex*)flannIndex)->knnSearch(obj_32f, indices, distances, 1, cvflann::SearchParams(32) );
}

// uses a volume instead of an octree
// TODO: Right now normals are required.
// This is much faster than sample_pc_octree
Mat samplePCByQuantization(Mat pc, float xrange[2], float yrange[2], float zrange[2], float sampleStep, int weightByCenter)
{
  std::vector< std::vector<int> > map;

  int numSamplesDim = (int)(1.0/sampleStep);

  float xr = xrange[1] - xrange[0];
  float yr = yrange[1] - yrange[0];
  float zr = zrange[1] - zrange[0];

  int numPoints = 0;

  map.resize((numSamplesDim+1)*(numSamplesDim+1)*(numSamplesDim+1));

  // OpenMP might seem like a good idea, but it didn't speed this up for me
  //#pragma omp parallel for
  for (int i=0; i<pc.rows; i++)
  {
    const float* point = (float*)(&pc.data[i * pc.step]);

    // quantize a point
    const int xCell =(int) ((float)numSamplesDim*(point[0]-xrange[0])/xr);
    const int yCell =(int) ((float)numSamplesDim*(point[1]-yrange[0])/yr);
    const int zCell =(int) ((float)numSamplesDim*(point[2]-zrange[0])/zr);
    const int index = xCell*numSamplesDim*numSamplesDim+yCell*numSamplesDim+zCell;

    /*#pragma omp critical
        {*/
    map[index].push_back(i);
    //  }
  }

  for (unsigned int i=0; i<map.size(); i++)
  {
    numPoints += (map[i].size()>0);
  }

  Mat pcSampled = Mat(numPoints, pc.cols, CV_32F);
  int c = 0;

  for (unsigned int i=0; i<map.size(); i++)
  {
    double px=0, py=0, pz=0;
    double nx=0, ny=0, nz=0;

    std::vector<int> curCell = map[i];
    int cn = (int)curCell.size();
    if (cn>0)
    {
      if (weightByCenter)
      {
        int xCell, yCell, zCell;
        double xc, yc, zc;
        double weightSum = 0 ;
        zCell = i % numSamplesDim;
        yCell = ((i-zCell)/numSamplesDim) % numSamplesDim;
        xCell = ((i-zCell-yCell*numSamplesDim)/(numSamplesDim*numSamplesDim));

        xc = ((double)xCell+0.5) * (double)xr/numSamplesDim + (double)xrange[0];
        yc = ((double)yCell+0.5) * (double)yr/numSamplesDim + (double)yrange[0];
        zc = ((double)zCell+0.5) * (double)zr/numSamplesDim + (double)zrange[0];

        for (int j=0; j<cn; j++)
        {
          const int ptInd = curCell[j];
          float* point = (float*)(&pc.data[ptInd * pc.step]);
          const double dx = point[0]-xc;
          const double dy = point[1]-yc;
          const double dz = point[2]-zc;
          const double d = sqrt(dx*dx+dy*dy+dz*dz);
          double w = 0;

          if (d>EPS)
          {
            // it is possible to use different weighting schemes.
            // inverse weigthing was just good for me
            // exp( - (distance/h)**2 )
            //const double w = exp(-d*d);
            w = 1.0/d;
          }

          //float weights[3]={1,1,1};
          px += w*(double)point[0];
          py += w*(double)point[1];
          pz += w*(double)point[2];
          nx += w*(double)point[3];
          ny += w*(double)point[4];
          nz += w*(double)point[5];

          weightSum+=w;
        }
        px/=(double)weightSum;
        py/=(double)weightSum;
        pz/=(double)weightSum;
        nx/=(double)weightSum;
        ny/=(double)weightSum;
        nz/=(double)weightSum;
      }
      else
      {
        for (int j=0; j<cn; j++)
        {
          const int ptInd = curCell[j];
          float* point = (float*)(&pc.data[ptInd * pc.step]);

          px += (double)point[0];
          py += (double)point[1];
          pz += (double)point[2];
          nx += (double)point[3];
          ny += (double)point[4];
          nz += (double)point[5];
        }

        px/=(double)cn;
        py/=(double)cn;
        pz/=(double)cn;
        nx/=(double)cn;
        ny/=(double)cn;
        nz/=(double)cn;

      }

      float *pcData = (float*)(&pcSampled.data[c*pcSampled.step[0]]);
      pcData[0]=(float)px;
      pcData[1]=(float)py;
      pcData[2]=(float)pz;

      // normalize the normals
      double norm = sqrt(nx*nx+ny*ny+nz*nz);

      if (norm>EPS)
      {
        pcData[3]=(float)(nx/norm);
        pcData[4]=(float)(ny/norm);
        pcData[5]=(float)(nz/norm);
      }
      //#pragma omp atomic
      c++;

      curCell.clear();
    }
  }

  map.clear();
  return pcSampled;
}

void shuffle(int *array, size_t n)
{
  size_t i;
  for (i = 0; i < n - 1; i++)
  {
    size_t j = i + rand() / (RAND_MAX / (n - i) + 1);
    int t = array[j];
    array[j] = array[i];
    array[i] = t;
  }
}

// compute the standard bounding box
void computeBboxStd(Mat pc, float xRange[2], float yRange[2], float zRange[2])
{
  Mat pcPts = pc.colRange(0, 3);
  int num = pcPts.rows;

  float* points = (float*)pcPts.data;

  xRange[0] = points[0];
  xRange[1] = points[0];
  yRange[0] = points[1];
  yRange[1] = points[1];
  zRange[0] = points[2];
  zRange[1] = points[2];

  for  ( int  ind = 0; ind < num; ind++ )
  {
    const float* row = (float*)(pcPts.data + (ind * pcPts.step));
    const float x = row[0];
    const float y = row[1];
    const float z = row[2];

    if (x<xRange[0])
      xRange[0]=x;
    if (x>xRange[1])
      xRange[1]=x;

    if (y<yRange[0])
      yRange[0]=y;
    if (y>yRange[1])
      yRange[1]=y;

    if (z<zRange[0])
      zRange[0]=z;
    if (z>zRange[1])
      zRange[1]=z;
  }
}

Mat normalizePCCoeff(Mat pc, float scale, float* Cx, float* Cy, float* Cz, float* MinVal, float* MaxVal)
{
  double minVal=0, maxVal=0;

  Mat x,y,z, pcn;
  pc.col(0).copyTo(x);
  pc.col(1).copyTo(y);
  pc.col(2).copyTo(z);

  float cx = (float) cv::mean(x).val[0];
  float cy = (float) cv::mean(y).val[0];
  float cz = (float) cv::mean(z).val[0];

  cv::minMaxIdx(pc, &minVal, &maxVal);

  x=x-cx;
  y=y-cy;
  z=z-cz;
  pcn.create(pc.rows, 3, CV_32FC1);
  x.copyTo(pcn.col(0));
  y.copyTo(pcn.col(1));
  z.copyTo(pcn.col(2));

  cv::minMaxIdx(pcn, &minVal, &maxVal);
  pcn=(float)scale*(pcn)/((float)maxVal-(float)minVal);

  *MinVal=(float)minVal;
  *MaxVal=(float)maxVal;
  *Cx=(float)cx;
  *Cy=(float)cy;
  *Cz=(float)cz;

  return pcn;
}

Mat transPCCoeff(Mat pc, float scale, float Cx, float Cy, float Cz, float MinVal, float MaxVal)
{
  Mat x,y,z, pcn;
  pc.col(0).copyTo(x);
  pc.col(1).copyTo(y);
  pc.col(2).copyTo(z);

  x=x-Cx;
  y=y-Cy;
  z=z-Cz;
  pcn.create(pc.rows, 3, CV_32FC1);
  x.copyTo(pcn.col(0));
  y.copyTo(pcn.col(1));
  z.copyTo(pcn.col(2));

  pcn=(float)scale*(pcn)/((float)MaxVal-(float)MinVal);

  return pcn;
}

Mat transformPCPose(Mat pc, double Pose[16])
{
  Mat pct = Mat(pc.rows, pc.cols, CV_32F);

  double R[9], t[3];
  poseToRT(Pose, R, t);

#if defined _OPENMP
#pragma omp parallel for
#endif
  for (int i=0; i<pc.rows; i++)
  {
    const float *pcData = (float*)(&pc.data[i*pc.step]);
    float *pcDataT = (float*)(&pct.data[i*pct.step]);
    const float *n1 = &pcData[3];
    float *nT = &pcDataT[3];

    double p[4] = {(double)pcData[0], (double)pcData[1], (double)pcData[2], 1};
    double p2[4];

    matrixProduct441(Pose, p, p2);

    // p2[3] should normally be 1
    if (fabs(p2[3])>EPS)
    {
      pcDataT[0] = (float)(p2[0]/p2[3]);
      pcDataT[1] = (float)(p2[1]/p2[3]);
      pcDataT[2] = (float)(p2[2]/p2[3]);
    }

    // Rotate the normals, too
    double n[3] = {(double)n1[0], (double)n1[1], (double)n1[2]}, n2[3];

    matrixProduct331(R, n, n2);
    double nNorm = sqrt(n2[0]*n2[0]+n2[1]*n2[1]+n2[2]*n2[2]);

    if (nNorm>EPS)
    {
      nT[0]=(float)(n2[0]/nNorm);
      nT[1]=(float)(n2[1]/nNorm);
      nT[2]=(float)(n2[2]/nNorm);
    }
  }

  return pct;
}

Mat genRandomMat(int rows, int cols, double mean, double stddev, int type)
{
  Mat meanMat = mean*Mat::ones(1,1,type);
  Mat sigmaMat= stddev*Mat::ones(1,1,type);
  RNG rng(time(0));
  Mat matr(rows, cols,type);
  rng.fill(matr, RNG::NORMAL, meanMat, sigmaMat);

  return matr;
}

void getRandQuat(double q[4])
{
  q[0] = (float)rand()/(float)(RAND_MAX);
  q[1] = (float)rand()/(float)(RAND_MAX);
  q[2] = (float)rand()/(float)(RAND_MAX);
  q[3] = (float)rand()/(float)(RAND_MAX);

  double n = sqrt(q[0]*q[0]+q[1]*q[1]+q[2]*q[2]+q[3]*q[3]);
  q[0]/=n;
  q[1]/=n;
  q[2]/=n;
  q[3]/=n;

  q[0]=fabs(q[0]);
}

void getRandomRotation(double R[9])
{
  double q[4];
  getRandQuat(q);
  quatToDCM(q, R);
}

void getRandomPose(double Pose[16])
{
  double R[9], t[3];

  srand((unsigned int)time(0));
  getRandomRotation(R);

  t[0] = (float)rand()/(float)(RAND_MAX);
  t[1] = (float)rand()/(float)(RAND_MAX);
  t[2] = (float)rand()/(float)(RAND_MAX);

  rtToPose(R,t,Pose);
}

Mat addNoisePC(Mat pc, double scale)
{
  Mat randT = genRandomMat(pc.rows,pc.cols,0,scale,CV_32FC1);
  return randT + pc;
}

/*
The routines below use the eigenvectors of the local covariance matrix
to compute the normals of a point cloud.
The algorithm uses FLANN and Joachim Kopp's fast 3x3 eigenvector computations
to improve accuracy and increase speed
Also, view point flipping as in point cloud library is implemented
*/

void meanCovLocalPC(const float* pc, const int ws, const int point_count, double CovMat[3][3], double Mean[4])
{
  int i;
  double accu[16]={0};

  // For each point in the cloud
  for (i = 0; i < point_count; ++i)
  {
    const float* cloud = &pc[i*ws];
    accu [0] += cloud[0] * cloud[0];
    accu [1] += cloud[0] * cloud[1];
    accu [2] += cloud[0] * cloud[2];
    accu [3] += cloud[1] * cloud[1]; // 4
    accu [4] += cloud[1] * cloud[2]; // 5
    accu [5] += cloud[2] * cloud[2]; // 8
    accu [6] += cloud[0];
    accu [7] += cloud[1];
    accu [8] += cloud[2];
  }

  for (i = 0; i < 9; ++i)
    accu[i]/=(double)point_count;

  Mean[0] = accu[6];
  Mean[1] = accu[7];
  Mean[2] = accu[8];
  Mean[3] = 0;
  CovMat[0][0] = accu [0] - accu [6] * accu [6];
  CovMat[0][1] = accu [1] - accu [6] * accu [7];
  CovMat[0][2] = accu [2] - accu [6] * accu [8];
  CovMat[1][1] = accu [3] - accu [7] * accu [7];
  CovMat[1][2] = accu [4] - accu [7] * accu [8];
  CovMat[2][2] = accu [5] - accu [8] * accu [8];
  CovMat[1][0] = CovMat[0][1];
  CovMat[2][0] = CovMat[0][2];
  CovMat[2][1] = CovMat[1][2];

}

void meanCovLocalPCInd(const float* pc, const int* Indices, const int ws, const int point_count, double CovMat[3][3], double Mean[4])
{
  int i;
  double accu[16]={0};

  for (i = 0; i < point_count; ++i)
  {
    const float* cloud = &pc[ Indices[i] * ws ];
    accu [0] += cloud[0] * cloud[0];
    accu [1] += cloud[0] * cloud[1];
    accu [2] += cloud[0] * cloud[2];
    accu [3] += cloud[1] * cloud[1]; // 4
    accu [4] += cloud[1] * cloud[2]; // 5
    accu [5] += cloud[2] * cloud[2]; // 8
    accu [6] += cloud[0];
    accu [7] += cloud[1];
    accu [8] += cloud[2];
  }

  for (i = 0; i < 9; ++i)
    accu[i]/=(double)point_count;

  Mean[0] = accu[6];
  Mean[1] = accu[7];
  Mean[2] = accu[8];
  Mean[3] = 0;
  CovMat[0][0] = accu [0] - accu [6] * accu [6];
  CovMat[0][1] = accu [1] - accu [6] * accu [7];
  CovMat[0][2] = accu [2] - accu [6] * accu [8];
  CovMat[1][1] = accu [3] - accu [7] * accu [7];
  CovMat[1][2] = accu [4] - accu [7] * accu [8];
  CovMat[2][2] = accu [5] - accu [8] * accu [8];
  CovMat[1][0] = CovMat[0][1];
  CovMat[2][0] = CovMat[0][2];
  CovMat[2][1] = CovMat[1][2];

}

CV_EXPORTS int computeNormalsPC3d(const Mat& PC, Mat& PCNormals, const int NumNeighbors, const bool FlipViewpoint, const double viewpoint[3])
{
  int i;

  if (PC.cols!=3 && PC.cols!=6) // 3d data is expected
  {
    //return -1;
    CV_Error(cv::Error::BadImageSize, "PC should have 3 or 6 elements in its columns");
  }

  int sizes[2] = {PC.rows, 3};
  int sizesResult[2] = {PC.rows, NumNeighbors};
  float* dataset = new float[PC.rows*3];
  float* distances = new float[PC.rows*NumNeighbors];
  int* indices = new int[PC.rows*NumNeighbors];

  for (i=0; i<PC.rows; i++)
  {
    const float* src = (float*)(&PC.data[i*PC.step]);
    float* dst = (float*)(&dataset[i*3]);

    dst[0] = src[0];
    dst[1] = src[1];
    dst[2] = src[2];
  }

  Mat PCInput(2, sizes, CV_32F, dataset, 0);

  void* flannIndex = indexPCFlann(PCInput);

  Mat Indices(2, sizesResult, CV_32S, indices, 0);
  Mat Distances(2, sizesResult, CV_32F, distances, 0);

  queryPCFlann(flannIndex, PCInput, Indices, Distances);
  destroyFlann(flannIndex);
  flannIndex = 0;

  PCNormals = Mat(PC.rows, 6, CV_32F);

  for (i=0; i<PC.rows; i++)
  {
    double C[3][3], mu[4];
    const float* pci = &dataset[i*3];
    float* pcr = (float*)(&PCNormals.data[i*PCNormals.step]);
    double nr[3];

    int* indLocal = &indices[i*NumNeighbors];

    // compute covariance matrix
    meanCovLocalPCInd(dataset, indLocal, 3, NumNeighbors, C, mu);

    // eigenvectors of covariance matrix
    eigenLowest33(C, nr);

    pcr[0] = pci[0];
    pcr[1] = pci[1];
    pcr[2] = pci[2];

    if (FlipViewpoint)
    {
      flipNormalViewpoint(pci, viewpoint[0], viewpoint[1], viewpoint[2], &nr[0], &nr[1], &nr[2]);
    }

    pcr[3] = (float)nr[0];
    pcr[4] = (float)nr[1];
    pcr[5] = (float)nr[2];
  }

  delete[] indices;
  delete[] distances;
  delete[] dataset;

  return 1;
}

} // namespace ppf_match_3d

} // namespace cv
