<commit_before>/**
 * @file lsh_search_impl.hpp
 * @author Parikshit Ram
 *
 * Implementation of the LSHSearch class.
 */
#ifndef __MLPACK_METHODS_NEIGHBOR_SEARCH_LSH_SEARCH_IMPL_HPP
#define __MLPACK_METHODS_NEIGHBOR_SEARCH_LSH_SEARCH_IMPL_HPP

#include <map>

#include <mlpack/core.hpp>

using namespace mlpack::neighbor;

// Construct the object.
template<typename SortPolicy, typename MetricType>
LSHSearch<SortPolicy, MetricType>::
LSHSearch(const arma::mat& referenceSet,
          const arma::mat& querySet,
          const size_t numProj,
          const size_t numTables,
          const double hashWidthIn,
          const size_t secondHashSize,
          const size_t bucketSize,
          const MetricType metric) :
  referenceSet(referenceSet),
  querySet(querySet),
  numProj(numProj),
  numTables(numTables),
  hashWidth(hashWidthIn),
  secondHashSize(secondHashSize),
  bucketSize(bucketSize),
  metric(metric)
{
  if (hashWidth == 0.0) // The user has not provided any value.
  {
    // Compute a heuristic hash width from the data.
    for (size_t i = 0; i < 25; i++)
    {
      size_t p1 = (size_t) math::RandInt(referenceSet.n_cols);
      size_t p2 = (size_t) math::RandInt(referenceSet.n_cols);

      hashWidth += MetricType::Evaluate(referenceSet.unsafe_col(p1),
                                        referenceSet.unsafe_col(p2));
    }

    hashWidth /= 25;
  }

  BuildHash();
}

template<typename SortPolicy, typename MetricType>
LSHSearch<SortPolicy, MetricType>::
LSHSearch(const arma::mat& referenceSet,
          const size_t numProj,
          const size_t numTables,
          const double hashWidthIn,
          const size_t secondHashSize,
          const size_t bucketSize,
          const MetricType metric) :
  referenceSet(referenceSet),
  querySet(referenceSet),
  numProj(numProj),
  numTables(numTables),
  hashWidth(hashWidthIn),
  secondHashSize(secondHashSize),
  bucketSize(bucketSize),
  metric(metric)
{
  if (hashWidth == 0.0) // The user has not provided any value.
  {
    // Compute a heuristic hash width from the data.
    for (size_t i = 0; i < 25; i++)
    {
      size_t p1 = (size_t) math::RandInt(referenceSet.n_cols);
      size_t p2 = (size_t) math::RandInt(referenceSet.n_cols);

      hashWidth += MetricType::Evaluate(referenceSet.unsafe_col(p1),
                                        referenceSet.unsafe_col(p2));
    }

    hashWidth /= 25;
  }

  BuildHash();
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
InsertNeighbor(const size_t queryIndex,
               const size_t pos,
               const size_t neighbor,
               const double distance)
{
  // We only memmove() if there is actually a need to shift something.
  if (pos < (distancePtr->n_rows - 1))
  {
    int len = (distancePtr->n_rows - 1) - pos;
    memmove(distancePtr->colptr(queryIndex) + (pos + 1),
        distancePtr->colptr(queryIndex) + pos,
        sizeof(double) * len);
    memmove(neighborPtr->colptr(queryIndex) + (pos + 1),
        neighborPtr->colptr(queryIndex) + pos,
        sizeof(size_t) * len);
  }

  // Now put the new information in the right index.
  (*distancePtr)(pos, queryIndex) = distance;
  (*neighborPtr)(pos, queryIndex) = neighbor;
}

template<typename SortPolicy, typename MetricType>
inline force_inline
double LSHSearch<SortPolicy, MetricType>::
BaseCase(const size_t queryIndex, const size_t referenceIndex)
{
  // If the datasets are the same, then this search is only using one dataset
  // and we should not return identical points.
  if ((&querySet == &referenceSet) && (queryIndex == referenceIndex))
    return 0.0;

  double distance = metric.Evaluate(querySet.unsafe_col(queryIndex),
                                    referenceSet.unsafe_col(referenceIndex));

  // If this distance is better than any of the current candidates, the
  // SortDistance() function will give us the position to insert it into.
  arma::vec queryDist = distancePtr->unsafe_col(queryIndex);
  size_t insertPosition = SortPolicy::SortDistance(queryDist, distance);

  // SortDistance() returns (size_t() - 1) if we shouldn't add it.
  if (insertPosition != (size_t() - 1))
    InsertNeighbor(queryIndex, insertPosition, referenceIndex, distance);

  return distance;
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
ReturnIndicesFromTable(const size_t queryIndex,
                       arma::uvec& referenceIndices,
                       size_t numTablesToSearch)
{
  // Decide on the number of tables to look into.
  if (numTablesToSearch == 0) // If no user input is given, search all.
    numTablesToSearch = numTables;

  // Sanity check to make sure that the existing number of tables is not
  // exceeded.
  if (numTablesToSearch > numTables)
    numTablesToSearch = numTables;

  // Hash the query in each of the 'numTablesToSearch' hash tables using the
  // 'numProj' projections for each table. This gives us 'numTablesToSearch'
  // keys for the query where each key is a 'numProj' dimensional integer
  // vector.

  // Compute the projection of the query in each table.
  arma::mat allProjInTables(numProj, numTablesToSearch);
  for (size_t i = 0; i < numTablesToSearch; i++)
  {
    allProjInTables.unsafe_col(i) = projections[i].t() *
        querySet.unsafe_col(queryIndex);
  }
  allProjInTables += offsets.cols(0, numTablesToSearch - 1);
  allProjInTables /= hashWidth;

  // Compute the hash value of each key of the query into a bucket of the
  // 'secondHashTable' using the 'secondHashWeights'.
  arma::rowvec hashVec = secondHashWeights.t() * arma::floor(allProjInTables);

  for (size_t i = 0; i < hashVec.n_elem; i++)
    hashVec[i] = (double) ((size_t) hashVec[i] % secondHashSize);

  Log::Assert(hashVec.n_elem == numTablesToSearch);

  // For all the buckets that the query is hashed into, sequentially
  // collect the indices in those buckets.
  arma::Col<size_t> refPointsConsidered;
  refPointsConsidered.zeros(referenceSet.n_cols);

  for (size_t i = 0; i < hashVec.n_elem; i++) // For all tables.
  {
    size_t hashInd = (size_t) hashVec[i];

    if (bucketContentSize[hashInd] > 0)
    {
      // Pick the indices in the bucket corresponding to 'hashInd'.
      size_t tableRow = bucketRowInHashTable[hashInd];
      assert(tableRow < secondHashSize);
      assert(tableRow < secondHashTable.n_rows);

      for (size_t j = 0; j < bucketContentSize[hashInd]; j++)
        refPointsConsidered[secondHashTable(tableRow, j)]++;
    }
  }

  referenceIndices = arma::find(refPointsConsidered > 0);
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
Search(const size_t k,
       arma::Mat<size_t>& resultingNeighbors,
       arma::mat& distances,
       const size_t numTablesToSearch)
{
  neighborPtr = &resultingNeighbors;
  distancePtr = &distances;

  // Set the size of the neighbor and distance matrices.
  neighborPtr->set_size(k, querySet.n_cols);
  distancePtr->set_size(k, querySet.n_cols);
  distancePtr->fill(SortPolicy::WorstDistance());
  neighborPtr->fill(referenceSet.n_cols);

  size_t avgIndicesReturned = 0;

  Timer::Start("computing_neighbors");

  // Go through every query point sequentially.
  for (size_t i = 0; i < querySet.n_cols; i++)
  {
    // Hash every query into every hash table and eventually into the
    // 'secondHashTable' to obtain the neighbor candidates.
    arma::uvec refIndices;
    ReturnIndicesFromTable(i, refIndices, numTablesToSearch);

    // An informative book-keeping for the number of neighbor candidates
    // returned on average.
    avgIndicesReturned += refIndices.n_elem;

    // Sequentially go through all the candidates and save the best 'k'
    // candidates.
    for (size_t j = 0; j < refIndices.n_elem; j++)
      BaseCase(i, (size_t) refIndices[j]);
  }

  Timer::Stop("computing_neighbors");

  avgIndicesReturned /= querySet.n_cols;
  Log::Info << avgIndicesReturned << " distinct indices returned on average." <<
      std::endl;
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
BuildHash()
{
  // The first level hash for a single table outputs a 'numProj'-dimensional
  // integer key for each point in the set -- (key, pointID)
  // The key creation details are presented below
  //
  // The second level hash is performed by hashing the key to
  // an integer in the range [0, 'secondHashSize').
  //
  // This is done by creating a weight vector 'secondHashWeights' of
  // length 'numProj' with each entry an integer randomly chosen
  // between [0, 'secondHashSize').
  //
  // Then the bucket for any key and its corresponding point is
  // given by <key, 'secondHashWeights'> % 'secondHashSize'
  // and the corresponding point ID is put into that bucket.

  // Step I: Prepare the second level hash.

  // Obtain the weights for the second hash.
  secondHashWeights = arma::floor(arma::randu(numProj) *
                                  (double) secondHashSize);

  // The 'secondHashTable' is initially an empty matrix of size
  // ('secondHashSize' x 'bucketSize'). But by only filling the buckets
  // as points land in them allows us to shrink the size of the
  // 'secondHashTable' at the end of the hashing.

  // Fill the second hash table n = referenceSet.n_cols.  This is because no
  // point has index 'n' so the presence of this in the bucket denotes that
  // there are no more points in this bucket.
  secondHashTable.set_size(secondHashSize, bucketSize);
  secondHashTable.fill(referenceSet.n_cols);

  // Keep track of the size of each bucket in the hash.  At the end of hashing
  // most buckets will be empty.
  bucketContentSize.zeros(secondHashSize);

  // Instead of putting the points in the row corresponding to the bucket, we
  // chose the next empty row and keep track of the row in which the bucket
  // lies. This allows us to stack together and slice out the empty buckets at
  // the end of the hashing.
  bucketRowInHashTable.set_size(secondHashSize);
  bucketRowInHashTable.fill(secondHashSize);

  // Keep track of number of non-empty rows in the 'secondHashTable'.
  size_t numRowsInTable = 0;

  // Step II: The offsets for all projections in all tables.
  // Since the 'offsets' are in [0, hashWidth], we obtain the 'offsets'
  // as randu(numProj, numTables) * hashWidth.
  offsets.randu(numProj, numTables);
  offsets *= hashWidth;

  // Step III: Create each hash table in the first level hash one by one and
  // putting them directly into the 'secondHashTable' for memory efficiency.
  for (size_t i = 0; i < numTables; i++)
  {
    // Step IV: Obtain the 'numProj' projections for each table.

    // For L2 metric, 2-stable distributions are used, and
    // the normal Z ~ N(0, 1) is a 2-stable distribution.
    arma::mat projMat;
    projMat.randn(referenceSet.n_rows, numProj);

    // Save the projection matrix for querying.
    projections.push_back(projMat);

    // Step V: create the 'numProj'-dimensional key for each point in each
    // table.

    // The following code performs the task of hashing each point to a
    // 'numProj'-dimensional integer key.  Hence you get a ('numProj' x
    // 'referenceSet.n_cols') key matrix.
    //
    // For a single table, let the 'numProj' projections be denoted by 'proj_i'
    // and the corresponding offset be 'offset_i'.  Then the key of a single
    // point is obtained as:
    // key = { floor( (<proj_i, point> + offset_i) / 'hashWidth' ) forall i }
    arma::mat offsetMat = arma::repmat(offsets.unsafe_col(i), 1,
                                       referenceSet.n_cols);
    arma::mat hashMat = projMat.t() * referenceSet;
    hashMat += offsetMat;
    hashMat /= hashWidth;

    // Step VI: Putting the points in the 'secondHashTable' by hashing the key.
    // Now we hash every key, point ID to its corresponding bucket.
    arma::rowvec secondHashVec = secondHashWeights.t()
      * arma::floor(hashMat);

    // This gives us the bucket for the corresponding point ID.
    for (size_t j = 0; j < secondHashVec.n_elem; j++)
      secondHashVec[j] = (double)((size_t) secondHashVec[j] % secondHashSize);

    Log::Assert(secondHashVec.n_elem == referenceSet.n_cols);

    // Insert the point in the corresponding row to its bucket in the
    // 'secondHashTable'.
    for (size_t j = 0; j < secondHashVec.n_elem; j++)
    {
      // This is the bucket number.
      size_t hashInd = (size_t) secondHashVec[j];
      // The point ID is 'j'.

      // If this is currently an empty bucket, start a new row keep track of
      // which row corresponds to the bucket.
      if (bucketContentSize[hashInd] == 0)
      {
        // Start a new row for hash.
        bucketRowInHashTable[hashInd] = numRowsInTable;
        secondHashTable(numRowsInTable, 0) = j;

        numRowsInTable++;
      }

      else
      {
        // If bucket is already present in the 'secondHashTable', find the
        // corresponding row and insert the point ID in this row unless the
        // bucket is full, in which case, do nothing.
        if (bucketContentSize[hashInd] < bucketSize)
          secondHashTable(bucketRowInHashTable[hashInd],
                          bucketContentSize[hashInd]) = j;
      }

      // Increment the count of the points in this bucket.
      if (bucketContentSize[hashInd] < bucketSize)
        bucketContentSize[hashInd]++;
    } // Loop over all points in the reference set.
  } // Loop over tables.

  // Step VII: Condensing the 'secondHashTable'.
  size_t maxBucketSize = 0;
  for (size_t i = 0; i < bucketContentSize.n_elem; i++)
    if (bucketContentSize[i] > maxBucketSize)
      maxBucketSize = bucketContentSize[i];

  Log::Info << "Final hash table size: (" << numRowsInTable << " x "
            << maxBucketSize << ")" << std::endl;
  secondHashTable.resize(numRowsInTable, maxBucketSize);
}

#endif
<commit_msg>Remove using directive and extra whitespace.<commit_after>/**
 * @file lsh_search_impl.hpp
 * @author Parikshit Ram
 *
 * Implementation of the LSHSearch class.
 */
#ifndef __MLPACK_METHODS_NEIGHBOR_SEARCH_LSH_SEARCH_IMPL_HPP
#define __MLPACK_METHODS_NEIGHBOR_SEARCH_LSH_SEARCH_IMPL_HPP

#include <mlpack/core.hpp>

namespace mlpack {
namespace neighbor {

// Construct the object.
template<typename SortPolicy, typename MetricType>
LSHSearch<SortPolicy, MetricType>::
LSHSearch(const arma::mat& referenceSet,
          const arma::mat& querySet,
          const size_t numProj,
          const size_t numTables,
          const double hashWidthIn,
          const size_t secondHashSize,
          const size_t bucketSize,
          const MetricType metric) :
  referenceSet(referenceSet),
  querySet(querySet),
  numProj(numProj),
  numTables(numTables),
  hashWidth(hashWidthIn),
  secondHashSize(secondHashSize),
  bucketSize(bucketSize),
  metric(metric)
{
  if (hashWidth == 0.0) // The user has not provided any value.
  {
    // Compute a heuristic hash width from the data.
    for (size_t i = 0; i < 25; i++)
    {
      size_t p1 = (size_t) math::RandInt(referenceSet.n_cols);
      size_t p2 = (size_t) math::RandInt(referenceSet.n_cols);

      hashWidth += MetricType::Evaluate(referenceSet.unsafe_col(p1),
                                        referenceSet.unsafe_col(p2));
    }

    hashWidth /= 25;
  }

  BuildHash();
}

template<typename SortPolicy, typename MetricType>
LSHSearch<SortPolicy, MetricType>::
LSHSearch(const arma::mat& referenceSet,
          const size_t numProj,
          const size_t numTables,
          const double hashWidthIn,
          const size_t secondHashSize,
          const size_t bucketSize,
          const MetricType metric) :
  referenceSet(referenceSet),
  querySet(referenceSet),
  numProj(numProj),
  numTables(numTables),
  hashWidth(hashWidthIn),
  secondHashSize(secondHashSize),
  bucketSize(bucketSize),
  metric(metric)
{
  if (hashWidth == 0.0) // The user has not provided any value.
  {
    // Compute a heuristic hash width from the data.
    for (size_t i = 0; i < 25; i++)
    {
      size_t p1 = (size_t) math::RandInt(referenceSet.n_cols);
      size_t p2 = (size_t) math::RandInt(referenceSet.n_cols);

      hashWidth += MetricType::Evaluate(referenceSet.unsafe_col(p1),
                                        referenceSet.unsafe_col(p2));
    }

    hashWidth /= 25;
  }

  BuildHash();
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
InsertNeighbor(const size_t queryIndex,
               const size_t pos,
               const size_t neighbor,
               const double distance)
{
  // We only memmove() if there is actually a need to shift something.
  if (pos < (distancePtr->n_rows - 1))
  {
    int len = (distancePtr->n_rows - 1) - pos;
    memmove(distancePtr->colptr(queryIndex) + (pos + 1),
        distancePtr->colptr(queryIndex) + pos,
        sizeof(double) * len);
    memmove(neighborPtr->colptr(queryIndex) + (pos + 1),
        neighborPtr->colptr(queryIndex) + pos,
        sizeof(size_t) * len);
  }

  // Now put the new information in the right index.
  (*distancePtr)(pos, queryIndex) = distance;
  (*neighborPtr)(pos, queryIndex) = neighbor;
}

template<typename SortPolicy, typename MetricType>
inline force_inline
double LSHSearch<SortPolicy, MetricType>::
BaseCase(const size_t queryIndex, const size_t referenceIndex)
{
  // If the datasets are the same, then this search is only using one dataset
  // and we should not return identical points.
  if ((&querySet == &referenceSet) && (queryIndex == referenceIndex))
    return 0.0;

  double distance = metric.Evaluate(querySet.unsafe_col(queryIndex),
                                    referenceSet.unsafe_col(referenceIndex));

  // If this distance is better than any of the current candidates, the
  // SortDistance() function will give us the position to insert it into.
  arma::vec queryDist = distancePtr->unsafe_col(queryIndex);
  size_t insertPosition = SortPolicy::SortDistance(queryDist, distance);

  // SortDistance() returns (size_t() - 1) if we shouldn't add it.
  if (insertPosition != (size_t() - 1))
    InsertNeighbor(queryIndex, insertPosition, referenceIndex, distance);

  return distance;
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
ReturnIndicesFromTable(const size_t queryIndex,
                       arma::uvec& referenceIndices,
                       size_t numTablesToSearch)
{
  // Decide on the number of tables to look into.
  if (numTablesToSearch == 0) // If no user input is given, search all.
    numTablesToSearch = numTables;

  // Sanity check to make sure that the existing number of tables is not
  // exceeded.
  if (numTablesToSearch > numTables)
    numTablesToSearch = numTables;

  // Hash the query in each of the 'numTablesToSearch' hash tables using the
  // 'numProj' projections for each table. This gives us 'numTablesToSearch'
  // keys for the query where each key is a 'numProj' dimensional integer
  // vector.

  // Compute the projection of the query in each table.
  arma::mat allProjInTables(numProj, numTablesToSearch);
  for (size_t i = 0; i < numTablesToSearch; i++)
  {
    allProjInTables.unsafe_col(i) = projections[i].t() *
        querySet.unsafe_col(queryIndex);
  }
  allProjInTables += offsets.cols(0, numTablesToSearch - 1);
  allProjInTables /= hashWidth;

  // Compute the hash value of each key of the query into a bucket of the
  // 'secondHashTable' using the 'secondHashWeights'.
  arma::rowvec hashVec = secondHashWeights.t() * arma::floor(allProjInTables);

  for (size_t i = 0; i < hashVec.n_elem; i++)
    hashVec[i] = (double) ((size_t) hashVec[i] % secondHashSize);

  Log::Assert(hashVec.n_elem == numTablesToSearch);

  // For all the buckets that the query is hashed into, sequentially
  // collect the indices in those buckets.
  arma::Col<size_t> refPointsConsidered;
  refPointsConsidered.zeros(referenceSet.n_cols);

  for (size_t i = 0; i < hashVec.n_elem; i++) // For all tables.
  {
    size_t hashInd = (size_t) hashVec[i];

    if (bucketContentSize[hashInd] > 0)
    {
      // Pick the indices in the bucket corresponding to 'hashInd'.
      size_t tableRow = bucketRowInHashTable[hashInd];
      assert(tableRow < secondHashSize);
      assert(tableRow < secondHashTable.n_rows);

      for (size_t j = 0; j < bucketContentSize[hashInd]; j++)
        refPointsConsidered[secondHashTable(tableRow, j)]++;
    }
  }

  referenceIndices = arma::find(refPointsConsidered > 0);
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
Search(const size_t k,
       arma::Mat<size_t>& resultingNeighbors,
       arma::mat& distances,
       const size_t numTablesToSearch)
{
  neighborPtr = &resultingNeighbors;
  distancePtr = &distances;

  // Set the size of the neighbor and distance matrices.
  neighborPtr->set_size(k, querySet.n_cols);
  distancePtr->set_size(k, querySet.n_cols);
  distancePtr->fill(SortPolicy::WorstDistance());
  neighborPtr->fill(referenceSet.n_cols);

  size_t avgIndicesReturned = 0;

  Timer::Start("computing_neighbors");

  // Go through every query point sequentially.
  for (size_t i = 0; i < querySet.n_cols; i++)
  {
    // Hash every query into every hash table and eventually into the
    // 'secondHashTable' to obtain the neighbor candidates.
    arma::uvec refIndices;
    ReturnIndicesFromTable(i, refIndices, numTablesToSearch);

    // An informative book-keeping for the number of neighbor candidates
    // returned on average.
    avgIndicesReturned += refIndices.n_elem;

    // Sequentially go through all the candidates and save the best 'k'
    // candidates.
    for (size_t j = 0; j < refIndices.n_elem; j++)
      BaseCase(i, (size_t) refIndices[j]);
  }

  Timer::Stop("computing_neighbors");

  avgIndicesReturned /= querySet.n_cols;
  Log::Info << avgIndicesReturned << " distinct indices returned on average." <<
      std::endl;
}

template<typename SortPolicy, typename MetricType>
void LSHSearch<SortPolicy, MetricType>::
BuildHash()
{
  // The first level hash for a single table outputs a 'numProj'-dimensional
  // integer key for each point in the set -- (key, pointID)
  // The key creation details are presented below
  //
  // The second level hash is performed by hashing the key to
  // an integer in the range [0, 'secondHashSize').
  //
  // This is done by creating a weight vector 'secondHashWeights' of
  // length 'numProj' with each entry an integer randomly chosen
  // between [0, 'secondHashSize').
  //
  // Then the bucket for any key and its corresponding point is
  // given by <key, 'secondHashWeights'> % 'secondHashSize'
  // and the corresponding point ID is put into that bucket.

  // Step I: Prepare the second level hash.

  // Obtain the weights for the second hash.
  secondHashWeights = arma::floor(arma::randu(numProj) *
                                  (double) secondHashSize);

  // The 'secondHashTable' is initially an empty matrix of size
  // ('secondHashSize' x 'bucketSize'). But by only filling the buckets
  // as points land in them allows us to shrink the size of the
  // 'secondHashTable' at the end of the hashing.

  // Fill the second hash table n = referenceSet.n_cols.  This is because no
  // point has index 'n' so the presence of this in the bucket denotes that
  // there are no more points in this bucket.
  secondHashTable.set_size(secondHashSize, bucketSize);
  secondHashTable.fill(referenceSet.n_cols);

  // Keep track of the size of each bucket in the hash.  At the end of hashing
  // most buckets will be empty.
  bucketContentSize.zeros(secondHashSize);

  // Instead of putting the points in the row corresponding to the bucket, we
  // chose the next empty row and keep track of the row in which the bucket
  // lies. This allows us to stack together and slice out the empty buckets at
  // the end of the hashing.
  bucketRowInHashTable.set_size(secondHashSize);
  bucketRowInHashTable.fill(secondHashSize);

  // Keep track of number of non-empty rows in the 'secondHashTable'.
  size_t numRowsInTable = 0;

  // Step II: The offsets for all projections in all tables.
  // Since the 'offsets' are in [0, hashWidth], we obtain the 'offsets'
  // as randu(numProj, numTables) * hashWidth.
  offsets.randu(numProj, numTables);
  offsets *= hashWidth;

  // Step III: Create each hash table in the first level hash one by one and
  // putting them directly into the 'secondHashTable' for memory efficiency.
  for (size_t i = 0; i < numTables; i++)
  {
    // Step IV: Obtain the 'numProj' projections for each table.

    // For L2 metric, 2-stable distributions are used, and
    // the normal Z ~ N(0, 1) is a 2-stable distribution.
    arma::mat projMat;
    projMat.randn(referenceSet.n_rows, numProj);

    // Save the projection matrix for querying.
    projections.push_back(projMat);

    // Step V: create the 'numProj'-dimensional key for each point in each
    // table.

    // The following code performs the task of hashing each point to a
    // 'numProj'-dimensional integer key.  Hence you get a ('numProj' x
    // 'referenceSet.n_cols') key matrix.
    //
    // For a single table, let the 'numProj' projections be denoted by 'proj_i'
    // and the corresponding offset be 'offset_i'.  Then the key of a single
    // point is obtained as:
    // key = { floor( (<proj_i, point> + offset_i) / 'hashWidth' ) forall i }
    arma::mat offsetMat = arma::repmat(offsets.unsafe_col(i), 1,
                                       referenceSet.n_cols);
    arma::mat hashMat = projMat.t() * referenceSet;
    hashMat += offsetMat;
    hashMat /= hashWidth;

    // Step VI: Putting the points in the 'secondHashTable' by hashing the key.
    // Now we hash every key, point ID to its corresponding bucket.
    arma::rowvec secondHashVec = secondHashWeights.t()
      * arma::floor(hashMat);

    // This gives us the bucket for the corresponding point ID.
    for (size_t j = 0; j < secondHashVec.n_elem; j++)
      secondHashVec[j] = (double)((size_t) secondHashVec[j] % secondHashSize);

    Log::Assert(secondHashVec.n_elem == referenceSet.n_cols);

    // Insert the point in the corresponding row to its bucket in the
    // 'secondHashTable'.
    for (size_t j = 0; j < secondHashVec.n_elem; j++)
    {
      // This is the bucket number.
      size_t hashInd = (size_t) secondHashVec[j];
      // The point ID is 'j'.

      // If this is currently an empty bucket, start a new row keep track of
      // which row corresponds to the bucket.
      if (bucketContentSize[hashInd] == 0)
      {
        // Start a new row for hash.
        bucketRowInHashTable[hashInd] = numRowsInTable;
        secondHashTable(numRowsInTable, 0) = j;

        numRowsInTable++;
      }

      else
      {
        // If bucket is already present in the 'secondHashTable', find the
        // corresponding row and insert the point ID in this row unless the
        // bucket is full, in which case, do nothing.
        if (bucketContentSize[hashInd] < bucketSize)
          secondHashTable(bucketRowInHashTable[hashInd],
                          bucketContentSize[hashInd]) = j;
      }

      // Increment the count of the points in this bucket.
      if (bucketContentSize[hashInd] < bucketSize)
        bucketContentSize[hashInd]++;
    } // Loop over all points in the reference set.
  } // Loop over tables.

  // Step VII: Condensing the 'secondHashTable'.
  size_t maxBucketSize = 0;
  for (size_t i = 0; i < bucketContentSize.n_elem; i++)
    if (bucketContentSize[i] > maxBucketSize)
      maxBucketSize = bucketContentSize[i];

  Log::Info << "Final hash table size: (" << numRowsInTable << " x "
            << maxBucketSize << ")" << std::endl;
  secondHashTable.resize(numRowsInTable, maxBucketSize);
}

}; // namespace neighbor
}; // namespace mlpack

#endif
<|endoftext|>


<commit_before>/*
 * This File is part of Davix, The IO library for HTTP based protocols
 * Copyright (C) CERN 2013
 * Author: Adrien Devresse <adrien.devresse@cern.ch>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
*/

#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>

#include <cstdlib>
#include <cstring>
#include <stdsoap2.h>

#include <sstream>

#include "delegation2H.h"
#include "delegation.hpp"
#include "../auth/davixx509cred_internal.hpp"
#include <utils/davix_logger_internal.hpp>

using namespace Davix;

const std::string Davix::DELEGATION_SCOPE = "Davix::HttpThirdPartyCopy::Delegation";

SOAP_NMAC struct Namespace namespaces[] =
{
    {"SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/", "http://www.w3.org/*/soap-envelope", NULL},
    {"SOAP-ENC", "http://schemas.xmlsoap.org/soap/encoding/", "http://www.w3.org/*/soap-encoding", NULL},
    {"xsi", "http://www.w3.org/2001/XMLSchema-instance", "http://www.w3.org/*/XMLSchema-instance", NULL},
    {"xsd", "http://www.w3.org/2001/XMLSchema", "http://www.w3.org/*/XMLSchema", NULL},
    {"tns", "http://www.gridsite.org/namespaces/delegation-1", NULL, NULL},
    {"tns2", "http://www.gridsite.org/namespaces/delegation-2", NULL, NULL},
    {NULL, NULL, NULL, NULL}
};

// Timestamp from ASN1 representation
static int get_timestamp_from_asn1(ASN1_TIME* asn1)
{
    char* data = (char*) ASN1_STRING_data(asn1);
    size_t len = strlen(data);
    struct tm time_tm;
    char zone = 0;

    memset(&time_tm, 0, sizeof(struct tm));

    if (len == 11
            && sscanf(data, "%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                      &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                      &(time_tm.tm_min), &zone) != 7) {
    }
    else if (len == 13
            && sscanf(data, "%02d%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                    &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                    &(time_tm.tm_min), &(time_tm.tm_sec), &zone) != 7) {
        return 0;
    }
    else if (len == 15
            && sscanf(data, "20%02d%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                    &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                    &(time_tm.tm_min), &(time_tm.tm_sec), &zone) != 7) {
        return 0;
    }

    if (zone != 'Z')
        return 0;

    if (time_tm.tm_year < 90)
        time_tm.tm_year += 100;
    --(time_tm.tm_mon);

    return timegm(&time_tm);
}

// Remaining lifetime from the given certificate
static int get_cert_remaining_life(const std::string& cert)
{
    FILE* f = fopen(cert.c_str(), "r");
    if (f == NULL)
        return 0;

    X509 *x509_cert = PEM_read_X509(f, NULL, NULL, NULL);
    if (x509_cert == NULL)
        return 0;

    ASN1_TIME* expiration = X509_get_notAfter(x509_cert);
    int expiration_timestamp = get_timestamp_from_asn1(expiration);
    X509_free(x509_cert);

    return (expiration_timestamp - time(NULL)) / 60;
}

// Puts into ucred the credentials paths
// Puts into capath the CA directory
// Returns true if the path should be removed at the end
// Sets err on error
bool DavixDelegation::get_credentials(const Davix::RequestParams& params,
		std::string& ucred, std::string&passwd, std::string& capath,
		int* lifetime, DavixError** err)
{
	X509Credential credentials = params.getClientCertX509();
	if (!credentials.hasCert()) {
		std::pair<authCallbackClientCertX509, void*> x509callback =
				params.getClientCertCallbackX509();

		if (!x509callback.first) {
			DavixError::setupError(err, DELEGATION_SCOPE,
					StatusCode::CredentialNotFound,
					"No callback set for getting credentials. Can not delegate");
			return false;
		}

		SessionInfo sess;

		x509callback.first(x509callback.second, sess, &credentials, err);
		if (err && *err)
			return false;
	}

	std::string ucert, ukey;
	if (!X509CredentialExtra::get_x509_info(credentials, &ucert, &ukey, &passwd)) {
		DavixError::setupError(err, DELEGATION_SCOPE,
				StatusCode::DelegationError,
				std::string("Third party copy only supports PEM certificates"));
		return false;
	}

	const std::vector<std::string> &capathList = params.listCertificateAuthorityPath();
	if (capathList.size() > 0)
		capath = capathList[0];

	// Delegation lifetime (in minutes!)
	int cert_remaining_life = get_cert_remaining_life(ucert);
	int delegation_max_life = 12 * 60; // 12 hours

	// Delegated proxy lifetime should be shorter than the current lifetime!
	*lifetime = std::min(cert_remaining_life, delegation_max_life) - 1;

	// Should at least remain one minute
	if (*lifetime <= 1) {
		DavixError::setupError(err, DELEGATION_SCOPE,
				StatusCode::DelegationError,
				std::string("The certificate expired or has less than two minutes left!"));
		return false;
	}

	// Cert and key need to be in the same file
	if (ucert == ukey) {
		ucred.assign(ucert);
		return false;
	} else {
		FILE *ifp, *ofp;
		int fd;
		int c;

		char* aux = strdup("/tmp/.XXXXXX");

		fd = mkstemp(aux);
		ofp = fdopen(fd, "w");

		ifp = fopen(ukey.c_str(), "r");
		while ((c = fgetc(ifp)) != EOF)
			fputc(c, ofp);
		fclose(ifp);

		ifp = fopen(ukey.c_str(), "r");
		while ((c = fgetc(ifp)) != EOF)
			fputc(c, ofp);
		fclose(ifp);

		fclose(ofp);

		ucred.assign(aux);
		free(aux);
		return true;
	}
}

// Guess the delegation version that should be used
static int get_delegation_version(const std::string& ucred, const std::string& passwd,
		const std::string& capath, const std::string& dlg_endpoint,
		DavixError** err)
{
    struct soap *soap_v;
    char err_buffer[512];
    int version = -1;

    soap_v = soap_new();

    if (soap_ssl_client_context(soap_v, SOAP_SSL_DEFAULT, ucred.c_str(), passwd.c_str(),
                                  ucred.c_str(), capath.c_str(), NULL) == 0) {
        delegation2::tns2__getVersionResponse response;
        delegation2::soap_call_tns2__getVersion(soap_v, dlg_endpoint.c_str(),
                "http://www.gridsite.org/namespaces/delegation-2", response);

        if (soap_v->error == 0) {
            version = atoi(response.getVersionReturn);
        }
        else {
            // Assume version 1 (does not implement the version method)
            version = 1;
        }
    }
    else {
        soap_sprint_fault(soap_v, err_buffer, sizeof(err_buffer));
        DavixError::setupError(err, DELEGATION_SCOPE,
                StatusCode::DelegationError,
                std::string("Could not connect to the delegation endpoint: ") + err_buffer);
    }

    soap_free(soap_v);
    soap_done(soap_v);

	return version;
}

static void triggerHooks(Context & context, RequestParams & params){
    RequestPreRunHook preRun = context.getHook<RequestPreRunHook>();
    Uri u;
    HttpRequest tmp_req(context, u, NULL);
    if(preRun){
        // force the run of the hook on req + params
        preRun(params, tmp_req, u);
    }
}

// Perform delegation, abstracting the version that is running on the server
std::string DavixDelegation::delegate(Context & context, const std::string &dlg_endpoint,
		const RequestParams& _p, Davix::DavixError** err)
{
	std::string ucreds, capath, passwd;
	int lifetime;

	RequestParams params(_p);
	triggerHooks(context, params);

    get_credentials(params, ucreds, passwd, capath, &lifetime, err);
	if (*err)
		return std::string();

	// Initialize SSL
	ERR_load_crypto_strings();
	OpenSSL_add_all_algorithms();

	// Figure out the version
	int delegation_version = get_delegation_version(ucreds, passwd, capath, dlg_endpoint, err);
	if (*err)
		return std::string();

    DAVIX_SLOG(DAVIX_LOG_VERBOSE, DAVIX_LOG_GRID, "Delegation version: {}", delegation_version);

    switch (delegation_version)
    {
        case 1:
            return delegate_v1(context, dlg_endpoint,
                    params, ucreds, passwd, capath, lifetime, err);
        case 2:
            return delegate_v2(context, dlg_endpoint,
                                params, ucreds, passwd, capath, lifetime, err);
        default: {
            std::ostringstream err_msg;
            err_msg << "Unknown delegation version: " << delegation_version;
            DavixError::setupError(err, DELEGATION_SCOPE,
                    StatusCode::DelegationError,
                    err_msg.str());
        }
    }

	return std::string();
}
<commit_msg>DMC-749 Fix davix-cp segfault<commit_after>/*
 * This File is part of Davix, The IO library for HTTP based protocols
 * Copyright (C) CERN 2013
 * Author: Adrien Devresse <adrien.devresse@cern.ch>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
*/

#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>

#include <cstdlib>
#include <cstring>
#include <stdsoap2.h>

#include <sstream>

#include "delegation2H.h"
#include "delegation.hpp"
#include "../auth/davixx509cred_internal.hpp"
#include <utils/davix_logger_internal.hpp>

using namespace Davix;

const std::string Davix::DELEGATION_SCOPE = "Davix::HttpThirdPartyCopy::Delegation";

SOAP_NMAC struct Namespace namespaces[] =
{
    {"SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/", "http://www.w3.org/*/soap-envelope", NULL},
    {"SOAP-ENC", "http://schemas.xmlsoap.org/soap/encoding/", "http://www.w3.org/*/soap-encoding", NULL},
    {"xsi", "http://www.w3.org/2001/XMLSchema-instance", "http://www.w3.org/*/XMLSchema-instance", NULL},
    {"xsd", "http://www.w3.org/2001/XMLSchema", "http://www.w3.org/*/XMLSchema", NULL},
    {"tns", "http://www.gridsite.org/namespaces/delegation-1", NULL, NULL},
    {"tns2", "http://www.gridsite.org/namespaces/delegation-2", NULL, NULL},
    {NULL, NULL, NULL, NULL}
};

// Timestamp from ASN1 representation
static int get_timestamp_from_asn1(ASN1_TIME* asn1)
{
    char* data = (char*) ASN1_STRING_data(asn1);
    size_t len = strlen(data);
    struct tm time_tm;
    char zone = 0;

    memset(&time_tm, 0, sizeof(struct tm));

    if (len == 11
            && sscanf(data, "%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                      &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                      &(time_tm.tm_min), &zone) != 7) {
    }
    else if (len == 13
            && sscanf(data, "%02d%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                    &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                    &(time_tm.tm_min), &(time_tm.tm_sec), &zone) != 7) {
        return 0;
    }
    else if (len == 15
            && sscanf(data, "20%02d%02d%02d%02d%02d%02d%c", &(time_tm.tm_year),
                    &(time_tm.tm_mon), &(time_tm.tm_mday), &(time_tm.tm_hour),
                    &(time_tm.tm_min), &(time_tm.tm_sec), &zone) != 7) {
        return 0;
    }

    if (zone != 'Z')
        return 0;

    if (time_tm.tm_year < 90)
        time_tm.tm_year += 100;
    --(time_tm.tm_mon);

    return timegm(&time_tm);
}

// Remaining lifetime from the given certificate
static int get_cert_remaining_life(const std::string& cert)
{
    FILE* f = fopen(cert.c_str(), "r");
    if (f == NULL)
        return 0;

    X509 *x509_cert = PEM_read_X509(f, NULL, NULL, NULL);
    if (x509_cert == NULL)
        return 0;

    ASN1_TIME* expiration = X509_get_notAfter(x509_cert);
    int expiration_timestamp = get_timestamp_from_asn1(expiration);
    X509_free(x509_cert);

    return (expiration_timestamp - time(NULL)) / 60;
}

// Puts into ucred the credentials paths
// Puts into capath the CA directory
// Returns true if the path should be removed at the end
// Sets err on error
bool DavixDelegation::get_credentials(const Davix::RequestParams& params,
		std::string& ucred, std::string&passwd, std::string& capath,
		int* lifetime, DavixError** err)
{
	X509Credential credentials = params.getClientCertX509();
	if (!credentials.hasCert()) {
		std::pair<authCallbackClientCertX509, void*> x509callback =
				params.getClientCertCallbackX509();

		if (!x509callback.first) {
			DavixError::setupError(err, DELEGATION_SCOPE,
					StatusCode::CredentialNotFound,
					"No callback set for getting credentials. Can not delegate");
			return false;
		}

		SessionInfo sess;

		x509callback.first(x509callback.second, sess, &credentials, err);
		if (err && *err)
			return false;
	}

	std::string ucert, ukey;
	if (!X509CredentialExtra::get_x509_info(credentials, &ucert, &ukey, &passwd)) {
		DavixError::setupError(err, DELEGATION_SCOPE,
				StatusCode::DelegationError,
				std::string("Third party copy only supports PEM certificates"));
		return false;
	}

	const std::vector<std::string> &capathList = params.listCertificateAuthorityPath();
	if (capathList.size() > 0)
		capath = capathList[0];

	// Delegation lifetime (in minutes!)
	int cert_remaining_life = get_cert_remaining_life(ucert);
	int delegation_max_life = 12 * 60; // 12 hours

	// Delegated proxy lifetime should be shorter than the current lifetime!
	*lifetime = std::min(cert_remaining_life, delegation_max_life) - 1;

	// Should at least remain one minute
	if (*lifetime <= 1) {
		DavixError::setupError(err, DELEGATION_SCOPE,
				StatusCode::DelegationError,
				std::string("The certificate expired or has less than two minutes left!"));
		return false;
	}

	// Cert and key need to be in the same file
	if (ucert == ukey) {
		ucred.assign(ucert);
		return false;
	} else {
		FILE *ifp, *ofp;
		int fd;
		int c;

		char* aux = strdup("/tmp/.XXXXXX");

		fd = mkstemp(aux);
		ofp = fdopen(fd, "w");

		ifp = fopen(ukey.c_str(), "r");
		while ((c = fgetc(ifp)) != EOF)
			fputc(c, ofp);
		fclose(ifp);

		ifp = fopen(ukey.c_str(), "r");
		while ((c = fgetc(ifp)) != EOF)
			fputc(c, ofp);
		fclose(ifp);

		fclose(ofp);

		ucred.assign(aux);
		free(aux);
		return true;
	}
}

// Guess the delegation version that should be used
static int get_delegation_version(const std::string& ucred, const std::string& passwd,
		const std::string& capath, const std::string& dlg_endpoint,
		DavixError** err)
{
    struct soap *soap_v;
    char err_buffer[512];
    int version = -1;

    soap_v = soap_new();

    if (soap_ssl_client_context(soap_v, SOAP_SSL_DEFAULT, ucred.c_str(), passwd.c_str(),
                                  ucred.c_str(), capath.c_str(), NULL) == 0) {
        delegation2::tns2__getVersionResponse response;
        delegation2::soap_call_tns2__getVersion(soap_v, dlg_endpoint.c_str(),
                "http://www.gridsite.org/namespaces/delegation-2", response);

        if (soap_v->error == 0) {
            version = atoi(response.getVersionReturn);
        }
        else {
            // Assume version 1 (does not implement the version method)
            version = 1;
        }
    }
    else {
        soap_sprint_fault(soap_v, err_buffer, sizeof(err_buffer));
        DavixError::setupError(err, DELEGATION_SCOPE,
                StatusCode::DelegationError,
                std::string("Could not connect to the delegation endpoint: ") + err_buffer);
    }

    soap_done(soap_v);
    soap_free(soap_v);

	return version;
}

static void triggerHooks(Context & context, RequestParams & params){
    RequestPreRunHook preRun = context.getHook<RequestPreRunHook>();
    Uri u;
    HttpRequest tmp_req(context, u, NULL);
    if(preRun){
        // force the run of the hook on req + params
        preRun(params, tmp_req, u);
    }
}

// Perform delegation, abstracting the version that is running on the server
std::string DavixDelegation::delegate(Context & context, const std::string &dlg_endpoint,
		const RequestParams& _p, Davix::DavixError** err)
{
	std::string ucreds, capath, passwd;
	int lifetime;

	RequestParams params(_p);
	triggerHooks(context, params);

    get_credentials(params, ucreds, passwd, capath, &lifetime, err);
	if (*err)
		return std::string();

	// Initialize SSL
	ERR_load_crypto_strings();
	OpenSSL_add_all_algorithms();

	// Figure out the version
	int delegation_version = get_delegation_version(ucreds, passwd, capath, dlg_endpoint, err);
	if (*err)
		return std::string();

    DAVIX_SLOG(DAVIX_LOG_VERBOSE, DAVIX_LOG_GRID, "Delegation version: {}", delegation_version);

    switch (delegation_version)
    {
        case 1:
            return delegate_v1(context, dlg_endpoint,
                    params, ucreds, passwd, capath, lifetime, err);
        case 2:
            return delegate_v2(context, dlg_endpoint,
                                params, ucreds, passwd, capath, lifetime, err);
        default: {
            std::ostringstream err_msg;
            err_msg << "Unknown delegation version: " << delegation_version;
            DavixError::setupError(err, DELEGATION_SCOPE,
                    StatusCode::DelegationError,
                    err_msg.str());
        }
    }

	return std::string();
}
<|endoftext|>


<commit_before>//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2007-2017 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//    * Redistributions of source code must retain the above copyright notice,
//      this list of conditions and the following disclaimer.
//
//    * Redistributions 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.
//
//    * Neither the name of the author nor the names of other contributors may
//      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 COPYRIGHT OWNER 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.
//
//////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "TrackListView.h"

#include <cursespp/Colors.h>
#include <cursespp/SingleLineEntry.h>
#include <cursespp/Text.h>
#include <cursespp/ToastOverlay.h>

#include <core/library/LocalLibraryConstants.h>
#include <core/audio/PlaybackService.h>

#include <glue/util/Duration.h>

#include <app/util/Hotkeys.h>
#include <app/util/Playback.h>
#include <app/overlay/PlayQueueOverlays.h>

#include <boost/format.hpp>

#define WINDOW_MESSAGE_SCROLL_TO_PLAYING 1003

using namespace musik::core;
using namespace musik::core::audio;
using namespace musik::core::db;
using namespace musik::core::library;
using namespace musik::core::runtime;
using namespace musik::core::sdk;
using namespace musik::cube;
using namespace musik::glue;
using namespace cursespp;

using namespace std::chrono;

/* if the user hasn't changed the selected index in 60 seconds
we assume he's not paying attention, and will automatically scroll
the view to the playing track if it's invisible. */
static const milliseconds AUTO_SCROLL_COOLDOWN = milliseconds(60000LL);

static inline milliseconds now() {
    return duration_cast<milliseconds>(system_clock::now().time_since_epoch());
}

TrackListView::TrackListView(
    PlaybackService& playback,
    ILibraryPtr library,
    RowFormatter formatter,
    RowDecorator decorator)
: ListWindow(nullptr)
, playback(playback) {
    this->library = library;
    this->library->QueryCompleted.connect(this, &TrackListView::OnQueryCompleted);
    this->playback.TrackChanged.connect(this, &TrackListView::OnTrackChanged);
    this->adapter = new Adapter(*this);
    this->lastQueryHash = 0;
    this->lastChanged = now();
    this->formatter = formatter;
    this->decorator = decorator;
}

TrackListView::~TrackListView() {
    delete this->adapter;
    this->adapter = nullptr;
}

void TrackListView::Requery(std::shared_ptr<TrackListQueryBase> query) {
    this->query = query;
    this->library->Enqueue(this->query);
}

void TrackListView::SelectFirstTrack() {
    this->SetSelectedIndex(this->headers.HeaderAt(0) ? 1 : 0);
}

void TrackListView::OnQueryCompleted(IQuery* query) {
    if (this->query && query == this->query.get()) {
        if (this->query->GetStatus() == IQuery::Finished) {
            bool hadTracks = this->tracks && this->tracks->Count() > 0;
            bool prevQuerySame = this->lastQueryHash == this->query->GetQueryHash();

            this->tracks = this->query->GetResult();
            this->headers.Set(this->query->GetHeaders());
            this->lastQueryHash = this->query->GetQueryHash();

            /* update our internal state */
            this->OnAdapterChanged();

            /* if the query was functionally the same as the last query, or we
            previously had no tracks, let's scroll to the top and select the first
            track */
            if (!hadTracks || !prevQuerySame) {
                this->ScrollToTop();
                this->SelectFirstTrack();
            }

            /* let external listeners know we updated */
            this->Requeried(this->query.get());

            this->query.reset();
        }
    }
}

std::shared_ptr<TrackList> TrackListView::GetTrackList() {
    return this->tracks;
}

void TrackListView::SetTrackList(std::shared_ptr<TrackList> trackList) {
    if (this->tracks != trackList) {
        this->tracks = trackList;
        this->ScrollToTop();
        this->SelectFirstTrack();
    }

    this->OnAdapterChanged();
}

void TrackListView::Clear() {
    this->query.reset();
    this->tracks.reset(new TrackList(this->library));
    this->headers.Reset();
    this->OnAdapterChanged();
}

TrackPtr TrackListView::GetSelectedTrack() {
    auto i = this->GetSelectedTrackIndex();
    return (i == ListWindow::NO_SELECTION) ? TrackPtr() : this->tracks->Get(i);
}

size_t TrackListView::GetSelectedTrackIndex() {
    auto i = this->GetSelectedIndex();
    if (i != ListWindow::NO_SELECTION) {
        auto entry = adapter->GetEntry(this, i);
        return static_cast<TrackListEntry*>(entry.get())->GetIndex();
    }
    return ListWindow::NO_SELECTION;
}

size_t TrackListView::TrackIndexToAdapterIndex(size_t index) {
    return this->headers.TrackListToAdapterIndex(index);
}

size_t TrackListView::TrackCount() {
    return this->tracks ? this->tracks->Count() : 0;
}

size_t TrackListView::EntryCount() {
    return this->GetScrollAdapter().GetEntryCount();
}

void TrackListView::ScrollToPlaying() {
    if (this->playing && this->tracks) {
        int64_t id = this->playing->GetId();
        for (size_t i = 0; i < this->tracks->Count(); i++) {
            if (this->tracks->GetId(i) == id) {
                size_t rawIndex = headers.TrackListToAdapterIndex(i);
                this->SetSelectedIndex(rawIndex);

                if (!this->IsEntryVisible(rawIndex)) {
                    this->ScrollTo(rawIndex);
                }

                return;
            }
        }
    }
}

void TrackListView::ProcessMessage(IMessage &message) {
    if (message.Type() == WINDOW_MESSAGE_SCROLL_TO_PLAYING) {
        this->ScrollToPlaying();
    }
}

bool TrackListView::KeyPress(const std::string& key) {
    bool handled = false;

    if (key == "KEY_ENTER") {
        if (headers.HeaderAt(this->GetSelectedIndex())) {
            TrackPtr track = this->GetSelectedTrack();
            PlayQueueOverlays::ShowAlbumDividerOverlay(
                MessageQueue(), this->playback, this->library, track);
        }
        else {
            playback::Play(*this, this->playback);
        }

        handled = true;
    }
    else if (Hotkeys::Is(Hotkeys::ContextMenu, key)) {
        TrackPtr track = this->GetSelectedTrack();
        if (!headers.HeaderAt(this->GetSelectedIndex())) {
            if (track) {
                PlayQueueOverlays::ShowAddTrackOverlay(
                    MessageQueue(), this->library, this->playback, track);
                handled = true;
            }
        }
        else {
            PlayQueueOverlays::ShowAlbumDividerOverlay(
                MessageQueue(), this->playback, this->library, track);
        }
    }
    else if (Hotkeys::Is(Hotkeys::NavigateJumpToPlaying, key)) {
        this->ScrollToPlaying();
        handled = true;
    }
    else if (Hotkeys::Is(Hotkeys::PlayQueueHotSwap, key)) {
        if (!headers.HeaderAt(this->GetSelectedIndex())) {
            if (playback::HotSwap(*this, this->playback)) {
                ToastOverlay::Show(_TSTR("tracklist_hot_swap_success_toast"), 2000);
            }
        }
        handled = true;
    }

    if (!handled) {
        handled = ListWindow::KeyPress(key);
    }

    if (handled) {
        this->lastChanged = now();

        this->DebounceMessage(
            WINDOW_MESSAGE_SCROLL_TO_PLAYING,
            0, 0, AUTO_SCROLL_COOLDOWN.count());
    }

    return handled;
}

void TrackListView::OnTrackChanged(size_t index, musik::core::TrackPtr track) {
    this->playing = track;
    this->OnAdapterChanged();

    if (now() - lastChanged >= AUTO_SCROLL_COOLDOWN) {
        this->ScrollToPlaying();
    }
}

IScrollAdapter& TrackListView::GetScrollAdapter() {
    return *this->adapter;
}

TrackListView::Adapter::Adapter(TrackListView &parent)
: parent(parent) {
}

/* * * * TrackListView::HeaderCalculator * * * */

void TrackListView::HeaderCalculator::Set(Headers rawOffsets) {
    this->rawOffsets = rawOffsets;
    this->absoluteOffsets.reset();
    if (rawOffsets) {
        this->absoluteOffsets.reset(new std::set<size_t>());
        size_t i = 0;
        for (auto val : (*this->rawOffsets)) {
            this->absoluteOffsets->insert(val + i);
            i++;
        }
    }
}

void TrackListView::HeaderCalculator::Reset() {
    this->absoluteOffsets.reset();
    this->rawOffsets.reset();
}

size_t TrackListView::HeaderCalculator::AdapterToTrackListIndex(size_t index) {
    return this->ApplyHeaderOffset(index, this->absoluteOffsets, -1);
}

size_t TrackListView::HeaderCalculator::TrackListToAdapterIndex(size_t index) {
    return this->ApplyHeaderOffset(index, this->rawOffsets, 1);
}

size_t TrackListView::HeaderCalculator::ApplyHeaderOffset(size_t index, Headers offsets, int delta) {
    size_t result = index;
    if (offsets) {
        for (auto offset : (*offsets)) {
            if (offset <= index) {
                result += delta;
            }
            else {
                break;
            }
        }
    }
    return result;
}

size_t TrackListView::HeaderCalculator::Count() {
    return this->absoluteOffsets ? this->absoluteOffsets->size() : 0;
}

bool TrackListView::HeaderCalculator::HeaderAt(size_t index) {
    return this->absoluteOffsets &&
        this->absoluteOffsets->find(index) != this->absoluteOffsets->end();
}

/* * * * TrackListView::Adapter * * * */

size_t TrackListView::Adapter::GetEntryCount() {
    return parent.tracks ? parent.tracks->Count() + parent.headers.Count() : 0;
}

#define TRACK_COL_WIDTH 3
#define ARTIST_COL_WIDTH 17
#define DURATION_COL_WIDTH 5 /* 00:00 */

static std::string formatWithoutAlbum(TrackPtr track, size_t width) {
    std::string trackNum = text::Align(
        track->GetString(constants::Track::TRACK_NUM),
        text::AlignRight,
        TRACK_COL_WIDTH);

    std::string duration = text::Align(
        musik::glue::duration::Duration(track->GetString(constants::Track::DURATION)),
        text::AlignRight,
        DURATION_COL_WIDTH);

    std::string artist = text::Align(
        track->GetString(constants::Track::ARTIST),
        text::AlignLeft,
        ARTIST_COL_WIDTH);

    int titleWidth =
        width -
        TRACK_COL_WIDTH -
        DURATION_COL_WIDTH -
        ARTIST_COL_WIDTH -
        (3 * 3); /* 3 = spacing */

    titleWidth = std::max(0, titleWidth);

    std::string title = text::Align(
        track->GetString(constants::Track::TITLE),
        text::AlignLeft,
        (int) titleWidth);

    return boost::str(
        boost::format("%s   %s   %s   %s")
        % trackNum % title % duration % artist);
}

IScrollAdapter::EntryPtr TrackListView::Adapter::GetEntry(cursespp::ScrollableWindow* window, size_t rawIndex) {
    bool selected = (rawIndex == parent.GetSelectedIndex());

    if (this->parent.headers.HeaderAt(rawIndex)) {
        /* the next track at the next logical index will have the album
        tracks we're interesetd in. */
        auto trackIndex = this->parent.headers.AdapterToTrackListIndex(rawIndex + 1);
        TrackPtr track = parent.tracks->Get(trackIndex);

        if (track) {
            std::string album = track->GetString(constants::Track::ALBUM);

            if (!album.size()) {
                album = _TSTR("tracklist_unknown_album");
            }

            std::shared_ptr<TrackListEntry> entry(new
                TrackListEntry(album, trackIndex, RowType::Separator));

            entry->SetAttrs(selected
                ? COLOR_PAIR(CURSESPP_LIST_ITEM_HIGHLIGHTED_HEADER)
                : COLOR_PAIR(CURSESPP_LIST_ITEM_HEADER));

            return entry;
        }
    }

    size_t trackIndex = this->parent.headers.AdapterToTrackListIndex(rawIndex);
    TrackPtr track = parent.tracks->Get(trackIndex);

    if (!track) {
        auto entry = std::shared_ptr<SingleLineEntry>(new SingleLineEntry("track missing"));

        entry->SetAttrs(COLOR_PAIR(selected
            ? CURSESPP_HIGHLIGHTED_ERROR_LIST_ITEM : CURSESPP_TEXT_ERROR));

        return entry;
    }

    int64_t attrs = CURSESPP_DEFAULT_COLOR;

    if (parent.decorator) {
        attrs = parent.decorator(track, trackIndex);
    }
    else {
        attrs = selected
            ? COLOR_PAIR(CURSESPP_HIGHLIGHTED_LIST_ITEM)
            : CURSESPP_DEFAULT_COLOR;

        TrackPtr playing = parent.playing;
        if (playing &&
            playing->GetId() == track->GetId() &&
            playing->LibraryId() == track->LibraryId())
        {
            if (selected) {
                attrs = COLOR_PAIR(CURSESPP_HIGHLIGHTED_SELECTED_LIST_ITEM);
            }
            else {
                attrs = COLOR_PAIR(CURSESPP_SELECTED_LIST_ITEM);
            }
        }
    }

    std::string text = parent.formatter
        ? parent.formatter(track, this->GetWidth())
        : formatWithoutAlbum(track, this->GetWidth());

    std::shared_ptr<TrackListEntry> entry(
        new TrackListEntry(text, trackIndex, RowType::Track));

    entry->SetAttrs(attrs);

    return entry;
}
<commit_msg>Ensure very long album names are properly truncated in TrackListView header rows.<commit_after>//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2007-2017 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//    * Redistributions of source code must retain the above copyright notice,
//      this list of conditions and the following disclaimer.
//
//    * Redistributions 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.
//
//    * Neither the name of the author nor the names of other contributors may
//      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 COPYRIGHT OWNER 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.
//
//////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "TrackListView.h"

#include <cursespp/Colors.h>
#include <cursespp/SingleLineEntry.h>
#include <cursespp/Text.h>
#include <cursespp/ToastOverlay.h>

#include <core/library/LocalLibraryConstants.h>
#include <core/audio/PlaybackService.h>

#include <glue/util/Duration.h>

#include <app/util/Hotkeys.h>
#include <app/util/Playback.h>
#include <app/overlay/PlayQueueOverlays.h>

#include <boost/format.hpp>

#define WINDOW_MESSAGE_SCROLL_TO_PLAYING 1003

using namespace musik::core;
using namespace musik::core::audio;
using namespace musik::core::db;
using namespace musik::core::library;
using namespace musik::core::runtime;
using namespace musik::core::sdk;
using namespace musik::cube;
using namespace musik::glue;
using namespace cursespp;

using namespace std::chrono;

/* if the user hasn't changed the selected index in 60 seconds
we assume he's not paying attention, and will automatically scroll
the view to the playing track if it's invisible. */
static const milliseconds AUTO_SCROLL_COOLDOWN = milliseconds(60000LL);

static inline milliseconds now() {
    return duration_cast<milliseconds>(system_clock::now().time_since_epoch());
}

TrackListView::TrackListView(
    PlaybackService& playback,
    ILibraryPtr library,
    RowFormatter formatter,
    RowDecorator decorator)
: ListWindow(nullptr)
, playback(playback) {
    this->library = library;
    this->library->QueryCompleted.connect(this, &TrackListView::OnQueryCompleted);
    this->playback.TrackChanged.connect(this, &TrackListView::OnTrackChanged);
    this->adapter = new Adapter(*this);
    this->lastQueryHash = 0;
    this->lastChanged = now();
    this->formatter = formatter;
    this->decorator = decorator;
}

TrackListView::~TrackListView() {
    delete this->adapter;
    this->adapter = nullptr;
}

void TrackListView::Requery(std::shared_ptr<TrackListQueryBase> query) {
    this->query = query;
    this->library->Enqueue(this->query);
}

void TrackListView::SelectFirstTrack() {
    this->SetSelectedIndex(this->headers.HeaderAt(0) ? 1 : 0);
}

void TrackListView::OnQueryCompleted(IQuery* query) {
    if (this->query && query == this->query.get()) {
        if (this->query->GetStatus() == IQuery::Finished) {
            bool hadTracks = this->tracks && this->tracks->Count() > 0;
            bool prevQuerySame = this->lastQueryHash == this->query->GetQueryHash();

            this->tracks = this->query->GetResult();
            this->headers.Set(this->query->GetHeaders());
            this->lastQueryHash = this->query->GetQueryHash();

            /* update our internal state */
            this->OnAdapterChanged();

            /* if the query was functionally the same as the last query, or we
            previously had no tracks, let's scroll to the top and select the first
            track */
            if (!hadTracks || !prevQuerySame) {
                this->ScrollToTop();
                this->SelectFirstTrack();
            }

            /* let external listeners know we updated */
            this->Requeried(this->query.get());

            this->query.reset();
        }
    }
}

std::shared_ptr<TrackList> TrackListView::GetTrackList() {
    return this->tracks;
}

void TrackListView::SetTrackList(std::shared_ptr<TrackList> trackList) {
    if (this->tracks != trackList) {
        this->tracks = trackList;
        this->ScrollToTop();
        this->SelectFirstTrack();
    }

    this->OnAdapterChanged();
}

void TrackListView::Clear() {
    this->query.reset();
    this->tracks.reset(new TrackList(this->library));
    this->headers.Reset();
    this->OnAdapterChanged();
}

TrackPtr TrackListView::GetSelectedTrack() {
    auto i = this->GetSelectedTrackIndex();
    return (i == ListWindow::NO_SELECTION) ? TrackPtr() : this->tracks->Get(i);
}

size_t TrackListView::GetSelectedTrackIndex() {
    auto i = this->GetSelectedIndex();
    if (i != ListWindow::NO_SELECTION) {
        auto entry = adapter->GetEntry(this, i);
        return static_cast<TrackListEntry*>(entry.get())->GetIndex();
    }
    return ListWindow::NO_SELECTION;
}

size_t TrackListView::TrackIndexToAdapterIndex(size_t index) {
    return this->headers.TrackListToAdapterIndex(index);
}

size_t TrackListView::TrackCount() {
    return this->tracks ? this->tracks->Count() : 0;
}

size_t TrackListView::EntryCount() {
    return this->GetScrollAdapter().GetEntryCount();
}

void TrackListView::ScrollToPlaying() {
    if (this->playing && this->tracks) {
        int64_t id = this->playing->GetId();
        for (size_t i = 0; i < this->tracks->Count(); i++) {
            if (this->tracks->GetId(i) == id) {
                size_t rawIndex = headers.TrackListToAdapterIndex(i);
                this->SetSelectedIndex(rawIndex);

                if (!this->IsEntryVisible(rawIndex)) {
                    this->ScrollTo(rawIndex);
                }

                return;
            }
        }
    }
}

void TrackListView::ProcessMessage(IMessage &message) {
    if (message.Type() == WINDOW_MESSAGE_SCROLL_TO_PLAYING) {
        this->ScrollToPlaying();
    }
}

bool TrackListView::KeyPress(const std::string& key) {
    bool handled = false;

    if (key == "KEY_ENTER") {
        if (headers.HeaderAt(this->GetSelectedIndex())) {
            TrackPtr track = this->GetSelectedTrack();
            PlayQueueOverlays::ShowAlbumDividerOverlay(
                MessageQueue(), this->playback, this->library, track);
        }
        else {
            playback::Play(*this, this->playback);
        }

        handled = true;
    }
    else if (Hotkeys::Is(Hotkeys::ContextMenu, key)) {
        TrackPtr track = this->GetSelectedTrack();
        if (!headers.HeaderAt(this->GetSelectedIndex())) {
            if (track) {
                PlayQueueOverlays::ShowAddTrackOverlay(
                    MessageQueue(), this->library, this->playback, track);
                handled = true;
            }
        }
        else {
            PlayQueueOverlays::ShowAlbumDividerOverlay(
                MessageQueue(), this->playback, this->library, track);
        }
    }
    else if (Hotkeys::Is(Hotkeys::NavigateJumpToPlaying, key)) {
        this->ScrollToPlaying();
        handled = true;
    }
    else if (Hotkeys::Is(Hotkeys::PlayQueueHotSwap, key)) {
        if (!headers.HeaderAt(this->GetSelectedIndex())) {
            if (playback::HotSwap(*this, this->playback)) {
                ToastOverlay::Show(_TSTR("tracklist_hot_swap_success_toast"), 2000);
            }
        }
        handled = true;
    }

    if (!handled) {
        handled = ListWindow::KeyPress(key);
    }

    if (handled) {
        this->lastChanged = now();

        this->DebounceMessage(
            WINDOW_MESSAGE_SCROLL_TO_PLAYING,
            0, 0, AUTO_SCROLL_COOLDOWN.count());
    }

    return handled;
}

void TrackListView::OnTrackChanged(size_t index, musik::core::TrackPtr track) {
    this->playing = track;
    this->OnAdapterChanged();

    if (now() - lastChanged >= AUTO_SCROLL_COOLDOWN) {
        this->ScrollToPlaying();
    }
}

IScrollAdapter& TrackListView::GetScrollAdapter() {
    return *this->adapter;
}

TrackListView::Adapter::Adapter(TrackListView &parent)
: parent(parent) {
}

/* * * * TrackListView::HeaderCalculator * * * */

void TrackListView::HeaderCalculator::Set(Headers rawOffsets) {
    this->rawOffsets = rawOffsets;
    this->absoluteOffsets.reset();
    if (rawOffsets) {
        this->absoluteOffsets.reset(new std::set<size_t>());
        size_t i = 0;
        for (auto val : (*this->rawOffsets)) {
            this->absoluteOffsets->insert(val + i);
            i++;
        }
    }
}

void TrackListView::HeaderCalculator::Reset() {
    this->absoluteOffsets.reset();
    this->rawOffsets.reset();
}

size_t TrackListView::HeaderCalculator::AdapterToTrackListIndex(size_t index) {
    return this->ApplyHeaderOffset(index, this->absoluteOffsets, -1);
}

size_t TrackListView::HeaderCalculator::TrackListToAdapterIndex(size_t index) {
    return this->ApplyHeaderOffset(index, this->rawOffsets, 1);
}

size_t TrackListView::HeaderCalculator::ApplyHeaderOffset(size_t index, Headers offsets, int delta) {
    size_t result = index;
    if (offsets) {
        for (auto offset : (*offsets)) {
            if (offset <= index) {
                result += delta;
            }
            else {
                break;
            }
        }
    }
    return result;
}

size_t TrackListView::HeaderCalculator::Count() {
    return this->absoluteOffsets ? this->absoluteOffsets->size() : 0;
}

bool TrackListView::HeaderCalculator::HeaderAt(size_t index) {
    return this->absoluteOffsets &&
        this->absoluteOffsets->find(index) != this->absoluteOffsets->end();
}

/* * * * TrackListView::Adapter * * * */

size_t TrackListView::Adapter::GetEntryCount() {
    return parent.tracks ? parent.tracks->Count() + parent.headers.Count() : 0;
}

#define TRACK_COL_WIDTH 3
#define ARTIST_COL_WIDTH 17
#define DURATION_COL_WIDTH 5 /* 00:00 */

static std::string formatWithoutAlbum(TrackPtr track, size_t width) {
    std::string trackNum = text::Align(
        track->GetString(constants::Track::TRACK_NUM),
        text::AlignRight,
        TRACK_COL_WIDTH);

    std::string duration = text::Align(
        musik::glue::duration::Duration(track->GetString(constants::Track::DURATION)),
        text::AlignRight,
        DURATION_COL_WIDTH);

    std::string artist = text::Align(
        track->GetString(constants::Track::ARTIST),
        text::AlignLeft,
        ARTIST_COL_WIDTH);

    int titleWidth =
        width -
        TRACK_COL_WIDTH -
        DURATION_COL_WIDTH -
        ARTIST_COL_WIDTH -
        (3 * 3); /* 3 = spacing */

    titleWidth = std::max(0, titleWidth);

    std::string title = text::Align(
        track->GetString(constants::Track::TITLE),
        text::AlignLeft,
        (int) titleWidth);

    return boost::str(
        boost::format("%s   %s   %s   %s")
        % trackNum % title % duration % artist);
}

IScrollAdapter::EntryPtr TrackListView::Adapter::GetEntry(cursespp::ScrollableWindow* window, size_t rawIndex) {
    bool selected = (rawIndex == parent.GetSelectedIndex());

    if (this->parent.headers.HeaderAt(rawIndex)) {
        /* the next track at the next logical index will have the album
        tracks we're interesetd in. */
        auto trackIndex = this->parent.headers.AdapterToTrackListIndex(rawIndex + 1);
        TrackPtr track = parent.tracks->Get(trackIndex);

        if (track) {
            std::string album = track->GetString(constants::Track::ALBUM);

            if (!album.size()) {
                album = _TSTR("tracklist_unknown_album");
            }

            album = text::Ellipsize(album, this->GetWidth());

            std::shared_ptr<TrackListEntry> entry(new
                TrackListEntry(album, trackIndex, RowType::Separator));

            entry->SetAttrs(selected
                ? COLOR_PAIR(CURSESPP_LIST_ITEM_HIGHLIGHTED_HEADER)
                : COLOR_PAIR(CURSESPP_LIST_ITEM_HEADER));

            return entry;
        }
    }

    size_t trackIndex = this->parent.headers.AdapterToTrackListIndex(rawIndex);
    TrackPtr track = parent.tracks->Get(trackIndex);

    if (!track) {
        auto entry = std::shared_ptr<SingleLineEntry>(new SingleLineEntry("track missing"));

        entry->SetAttrs(COLOR_PAIR(selected
            ? CURSESPP_HIGHLIGHTED_ERROR_LIST_ITEM : CURSESPP_TEXT_ERROR));

        return entry;
    }

    int64_t attrs = CURSESPP_DEFAULT_COLOR;

    if (parent.decorator) {
        attrs = parent.decorator(track, trackIndex);
    }
    else {
        attrs = selected
            ? COLOR_PAIR(CURSESPP_HIGHLIGHTED_LIST_ITEM)
            : CURSESPP_DEFAULT_COLOR;

        TrackPtr playing = parent.playing;
        if (playing &&
            playing->GetId() == track->GetId() &&
            playing->LibraryId() == track->LibraryId())
        {
            if (selected) {
                attrs = COLOR_PAIR(CURSESPP_HIGHLIGHTED_SELECTED_LIST_ITEM);
            }
            else {
                attrs = COLOR_PAIR(CURSESPP_SELECTED_LIST_ITEM);
            }
        }
    }

    std::string text = parent.formatter
        ? parent.formatter(track, this->GetWidth())
        : formatWithoutAlbum(track, this->GetWidth());

    std::shared_ptr<TrackListEntry> entry(
        new TrackListEntry(text, trackIndex, RowType::Track));

    entry->SetAttrs(attrs);

    return entry;
}
<|endoftext|>