// Copyright (c) 2013-2020, SIB - Swiss Institute of Bioinformatics and
//                          Biozentrum - University of Basel
// 
// 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
// 
//   http://www.apache.org/licenses/LICENSE-2.0
// 
// 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 <promod3/core/tetrahedral_polytope.hh>
#include <limits>


namespace{

void Extent(const std::vector<int>& indices, 
            const std::vector<Real>& values,
            Real& mean,
            Real& extent){

  Real max_val = -std::numeric_limits<Real>::max();
  Real min_val = std::numeric_limits<Real>::max();

  for(std::vector<int>::const_iterator i = indices.begin(),
      e = indices.end(); i != e; ++i){
    Real v = values[*i];
    max_val = std::max(v, max_val);
    min_val = std::min(v, min_val);
  }

  mean = Real(0.5) * (min_val + max_val);
  extent = max_val - min_val;
}

void Fill(const std::vector<int>& indices,
          const std::vector<Real>& values,
          Real boundary,
          std::vector<int>& l,
          std::vector<int>& r){

  l.reserve(indices.size());
  r.reserve(indices.size());

  for(std::vector<int>::const_iterator i = indices.begin(),
      e = indices.end(); i != e; ++i){
    if(values[*i] < boundary) l.push_back(*i);
    else r.push_back(*i);
  }

  // make sure, that none of l and r is empty 
  if(l.empty()){
    l.push_back(r.back());
    r.pop_back();
    return;
  }
  if(r.empty()){
    r.push_back(l.back());
    l.pop_back();
  }
}

void Separate(const std::vector<Real>& x,
              const std::vector<Real>& y,
              const std::vector<Real>& z,
              const std::vector<int>& indices, 
              std::vector<int>& l,
              std::vector<int>& r){

  Real x_mean, x_extent, y_mean, y_extent, z_mean, z_extent;

  Extent(indices, x, x_mean, x_extent);
  Extent(indices, y, y_mean, y_extent);
  Extent(indices, z, z_mean, z_extent);

  if(x_extent > y_extent && x_extent > z_extent){
    Fill(indices, x, x_mean, l, r);
    return;
  }

  if(y_extent > z_extent){
    Fill(indices, y, y_mean, l, r);
    return;    
  }
  
  Fill(indices, z, z_mean, l, r);
}

}  // anon ns



namespace promod3{ namespace core{

TetrahedralPolytope::TetrahedralPolytope(Real x, Real y, Real z, Real r){
  const Real a = 0.8165;
  const Real b = 0.57735;
  const Real ra = r * a;
  const Real rb = r * b;
  bounds[0] =  (x - ra)*a + (z - rb)*b;
  bounds[1] = -(x + ra)*a + (z - rb)*b;
  bounds[2] =  (y - ra)*a - (z + rb)*b;
  bounds[3] = -(y + ra)*a - (z + rb)*b;
  bounds[4] =  (x + ra)*a + (z + rb)*b;
  bounds[5] = -(x - ra)*a + (z + rb)*b;
  bounds[6] =  (y + ra)*a - (z - rb)*b;
  bounds[7] = -(y - ra)*a - (z - rb)*b;  
}

TetrahedralPolytopeTree::TetrahedralPolytopeTree(const std::vector<Real>& x,
                                                 const std::vector<Real>& y,
                                                 const std::vector<Real>& z,
                                                 const std::vector<Real>& radii){
  this->ResetTree(x, y, z, radii);
}


void TetrahedralPolytopeTree::ResetTree(const std::vector<Real>& x,
                                        const std::vector<Real>& y,
                                        const std::vector<Real>& z,
                                        const std::vector<Real>& radii){

  ScopedTimerPtr prof = StaticRuntimeProfiler::StartScoped(
                          "TetrahedralPolytopeTree::ResetTree", 2);

  if(x.size() != y.size() || x.size() != z.size()){
    throw promod3::Error("Size inconsistency in position data!");
  }

  if(x.size() != radii.size()){
    throw promod3::Error("Size inconsistency in positions and radii!");
  }

  num_leaf_nodes_ = x.size();

  if(num_leaf_nodes_ == 0) {
    // empty tree... assign some default values, even though it doesn't matter
    // too much as any call for the Overlapp functions is guarded by a check for
    // num_leaf_nodes_ == 0
    root_node_ = 0;
    nodes_.resize(0);
    connectivity_.resize(0);
    return;
  }

  // we construct a full binary tree (each node has either 0 or 2 children)
  // the number of nodes can therefore be determined from the number of leafs
  int num_nodes = 2*num_leaf_nodes_-1;
  nodes_.resize(num_nodes);
  connectivity_.resize(num_nodes);
  std::vector<int> indices(num_leaf_nodes_);

  for(int i = 0; i < num_leaf_nodes_; ++i){
    nodes_[i] = TetrahedralPolytope(x[i], y[i], z[i], radii[i]);
    connectivity_[i] = std::make_pair(-1, -1);
    indices[i] = i;
  }

  idx_helper_ = num_leaf_nodes_;
  root_node_ = this->Generate(x, y, z, indices);
}


int TetrahedralPolytopeTree::Generate(const std::vector<Real>& x,
                                      const std::vector<Real>& y,
                                      const std::vector<Real>& z,
                                      const std::vector<int>& indices){

  // it it's a leaf, it's already contructed in the ResetTree
  if(indices.size() == 1){
    return indices[0]; 
  }

  // determine index of that node from idx_helper and construct its Polytope
  int return_idx = idx_helper_++;
  this->AssignPolytope(x, y, x, indices, return_idx);

  // separate the members and construct the children
  std::vector<int> l,r;
  Separate(x, y, z, indices, l, r);
  connectivity_[return_idx].first = this->Generate(x, y, z, l);
  connectivity_[return_idx].second = this->Generate(x, y, z, r);

  return return_idx;
}

void TetrahedralPolytopeTree::AssignPolytope(const std::vector<Real>& x,
                                             const std::vector<Real>& y,
                                             const std::vector<Real>& z,
                                             const std::vector<int>& indices,
                                             int node_idx) {

  // find extent of tetrahedral polytope containing all polytopes
  // defined in indices
  #if PM3_ENABLE_SSE && OST_DOUBLE_PRECISION == 0
  __m128 min_bounds = _mm_set1_ps(std::numeric_limits<Real>::max());
  __m128 max_bounds = _mm_set1_ps(-std::numeric_limits<Real>::max());

  for(std::vector<int>::const_iterator i = indices.begin(), e = indices.end(); 
      i != e; ++i){
    const TetrahedralPolytope& polytope = nodes_[*i];
    __m128 poly_min_bounds = _mm_load_ps(&polytope.bounds[0]);
    __m128 poly_max_bounds = _mm_load_ps(&polytope.bounds[4]);
    min_bounds = _mm_min_ps(poly_min_bounds, min_bounds);
    max_bounds = _mm_max_ps(poly_max_bounds, max_bounds);
  }
  #else
  Real initial_min = std::numeric_limits<Real>::max();   
  Real initial_max = -std::numeric_limits<Real>::max();   

  Real min_bounds[4];
  Real max_bounds[4];
  min_bounds[0] = initial_min;
  min_bounds[1] = initial_min;
  min_bounds[2] = initial_min;
  min_bounds[3] = initial_min;
  max_bounds[0] = initial_max;
  max_bounds[1] = initial_max;
  max_bounds[2] = initial_max;
  max_bounds[3] = initial_max;
 
  for(std::vector<int>::const_iterator i = indices.begin(), e = indices.end();       
      i != e; ++i){     
    const TetrahedralPolytope& polytope = nodes_[*i];     
    min_bounds[0] = std::min(min_bounds[0], polytope.bounds[0]);
    min_bounds[1] = std::min(min_bounds[1], polytope.bounds[1]);
    min_bounds[2] = std::min(min_bounds[2], polytope.bounds[2]);
    min_bounds[3] = std::min(min_bounds[3], polytope.bounds[3]);
    max_bounds[0] = std::max(max_bounds[0], polytope.bounds[4]);
    max_bounds[1] = std::max(max_bounds[1], polytope.bounds[5]);
    max_bounds[2] = std::max(max_bounds[2], polytope.bounds[6]);
    max_bounds[3] = std::max(max_bounds[3], polytope.bounds[7]);   
  }    
  #endif

  nodes_[node_idx] = TetrahedralPolytope(min_bounds, max_bounds);
}

}} // ns
