/*! 
 *   \file  CMatch.cpp
 *   \brief Defines class to match signature of query image against the inventory.
 *    
 *   \details  
 *   CMatch facilitates matching of an input signature against the inventory of signatures. 
 *
 *   \date      October 24, 2011
 *   \copyright eBay Research Labs.
 */

#include <boost/lexical_cast.hpp>

#include <cstdio>   // For atoi.

#include "CMatch.h"
#include "CFeatureExtraction.h"
#include "UtilitiesWeb.h"
#include <algorithm>

// Include openCV-specific headers.
#include <highgui.h>

// Namespaces.
using namespace std;
using namespace cv;


/*!
 *   \brief Constructor. 
 */
CMatch::CMatch()
{
}


/*!
 *   \brief  Destructor.
 */
CMatch::~CMatch()
{
  // Do nothing;
}


/*!
 *   \brief  Returns sorted list of similar items in inventory for the given query image.
 *
 *   \param [in]  sigQueryIn       Signature of query image.
 *   \param [in,out]  listCategories  List of category names. When empty, all categories will be matched.
 *                                Order of appearance of categories is preserved when returning similarity scores.
 *	\param [in,out]  totalEntries	List of total entries for each category in the list of categories.
 *   \param [in]  similarity      Sorted vector of (itemID, similarity) pair for each category.
 *   \param [in]  minSimilarity   Minimum similarity score of item to be returned. Range 0-1. Default = 0.
 *   \param [in]  wtColor         Weight to balance against texture similarity. Range = [0, 1], Default = 0.5.
 *   \param [in]  pageSize        Number of matching items to return. Return all when <1. Default = 1.
 *   \param [in]  pageNumber      Items returns will be on this page. Default = 1.
 *   \param [in]  queryPartID     0-based PartID of query. Default = 0 (torso). TODO: Currently accepts only -1 or 0 or 1.
 *                                When negative, query part ID is picked dynamically based on category ID of retrieved results.
 *   \param [in]  context         Context identifier for transformation. 
 *                                Accepted values: <1 (identity map), 1 (color science), >1 (data science)
 *	\param [out] totalEntries    Returns the total number of entries when only one category is requested. Can be used for pagination.
 */

void CMatch::Match(int siteid, const CSignature &sigIn, vector < string > &listCategories, 
		   vector<int>& totalEntries, 
		   SimilarityVector &similarity, float minSimilarity, double wtColor, int queryPartID, 
		   int context, bool complimentary, int maxResults)
{
  const CInventory *pInventory = CInventory::Instance(siteid);
  int numCategories;

  numCategories = pInventory->GetCategoryCount();

  CSignature sigQueryIsTop;
  CSignature sigQueryIsBottom; 
  sigQueryIsTop.MakeDeepCopy(sigIn);
  sigQueryIsBottom.MakeDeepCopy(sigIn);


  if (listCategories.empty())
    {
      // If no categories passed, we'll search them all.
      listCategories.resize(numCategories);
      for (int i=0; i < numCategories; ++i)
	listCategories[i] = pInventory->getCategory(i)->getID();
    }
  else
    {
      // Update list of categories. This will be the intersection of input categories and the categories in the inventory.
      // Input order is preserved.
      vector<string> intersection;
      size_t reaLSize = 0;
      for (int i=0; i < listCategories.size(); ++i)
	{
	  if (pInventory->getCategory(listCategories[i])) {
	    reaLSize++;
	    intersection.push_back(listCategories[i]);
	  }
	}
      listCategories.swap(intersection);
    }
  numCategories = (int) listCategories.size();


  // Transform query to complementary space, based on context.
  if (queryPartID<0)
    {
      // Map to query partID dynamically. Get possible maps for top and bottom.
      if ( context > 0 )
        {
	  pInventory->GetFtrMapper()->TransformQuerySignatureUsingContext( sigQueryIsTop,    0, context ); // Query is Top.
	  pInventory->GetFtrMapper()->TransformQuerySignatureUsingContext( sigQueryIsBottom, 1, context ); // Query is Bottom.
        }
    }
  else
    {
      // Use query partID as is. Just map one of sigQueryIsTop and sigQueryIsBottom, as needed.
      if ( context > 0 )
        { 
	  if (queryPartID==0)
            {
	      pInventory->GetFtrMapper()->TransformQuerySignatureUsingContext( sigQueryIsTop, 0, context ); // Query is Top.
            }  
	  else
            {
	      pInventory->GetFtrMapper()->TransformQuerySignatureUsingContext( sigQueryIsBottom, 1, context ); // Query is Bottom.
            }
        }
    }


#ifdef _DEBUG
  std::ostream_iterator< std::string > output( cerr, " " );
  std::ostream_iterator< std::string > soutput( cerr, " " );
  std::cerr << "Running MatchImage for siteID=" << siteid << ", initial categories: ";

  std::copy(listCategories.begin(), listCategories.end(), output);
  std::cerr<<std::endl << ", resulting categories: ";
  std::copy(listCategories.begin(), listCategories.end(), soutput);
  std::cerr<<std::endl;
#endif

  // Calculate similarity of current query image with images in inventory database.
  similarity.clear();

  if (listCategories.empty())
    return; // No categories to match against.

  // make sure all vectors have same size
  // this also fills vectors with zeroes
  similarity.resize(numCategories);
  totalEntries.resize(numCategories);

  if (sigIn.m_version<2.0) 
    wtColor = 1.0;

  double confStripesInQuery = ConfidenceThatStripesExist(sigIn);

  // Remap wtColor based on content of query image.
  // TODO: Improve this scheme.
  if (wtColor<1.0)
    {
      double w1 = RemapIntervalWithCentralNode(wtColor, 0.1);
      double w2 = RemapIntervalWithCentralNode(wtColor, 0.25);
      wtColor   = confStripesInQuery*w1 + (1-confStripesInQuery)*w2;
    }

  time_t currTime = time(0); // Current UNIX epoch time
	
  {
    // #pragma omp parallel for  num_threads (8)
    for (int iList=0; iList < numCategories; iList++)
      {
	const CInventory::Category* cat = pInventory->getCategory(listCategories[iList]);
	if (!cat)
	  continue;

	int numImagesInCategory = (int)cat->signatureVector().size();
	 
	if (numImagesInCategory == 0 )
	  continue;
	if (maxResults == 0)
	  maxResults = numImagesInCategory;

        CMatch::SimilarityRow sim;

	const int bufSize = 64;
	// return 1000 entries by default
	size_t pageSize=1000;

	CMatch::SimilarityRow buf(bufSize);

        int         queryPartIDFinal = queryPartID<0       ? ConvertResultCategoryIDToQueryPartID(listCategories[iList]) : queryPartID; // Reassign queryPartID only when it is negative.		
        CSignature &sigQueryFinal    = queryPartIDFinal==0 ? sigQueryIsTop : sigQueryIsBottom;
		
		
	for(int totalScanned = 0; totalScanned + bufSize < numImagesInCategory; totalScanned += bufSize)
	  {

	    // lock the mutex to make sure dynamic update does not modify our vec
	    CInventory::Mutex::scoped_lock mut(cat->getMutex());
	    {

	      //#ifdef USE_OMP
	      //#pragma omp parallel for  num_threads (8)
	      //#endif	
	      for (int i=0; i < bufSize ; ++i)
		{	
		  const CSignature* sigDB = cat->signatureVector()[i+totalScanned];

		  if ( sigDB != 0 )
		    {
		      float curSimilarity = (float) CalculateSimilarity(sigQueryFinal, *sigDB, wtColor, confStripesInQuery);
		      buf[i] = SimilarityEntry(sigDB, curSimilarity);
		    }
		} 
	    } // parallel

		
	    // special case : find a single item 
	    if (maxResults == 1) {
	      for (int i=0; i < bufSize ; ++i) {
		if (buf[i].sim_ > minSimilarity) {
		  similarity[iList].resize(1);
		  similarity[iList][0]=buf[i];
		  break;	
		}	
	      }	
	    }

	    // non-parallel post-process
	    for (int i=0; i < bufSize; ++i) {
	      if (buf[i].sim_ > minSimilarity) {
		sim.push_back(buf[i]);
		if (sim.size() >= maxResults)
		  break;
	      }
	    }

	  } // numimages
		
	if (!(maxResults == 1)) 
	  {
	    // Populate variable "similarity".
	    size_t first_item=0;
	    size_t last_item=MIN(sim.size(), first_item+pageSize);
	
	    sort(sim.begin( ), sim.end( ), MoreSimilar);
		
	    first_item = MIN(sim.size(), first_item);
	    last_item = MIN(sim.size(), first_item+pageSize);

	    totalEntries[iList] = sim.size();

	    if (last_item > first_item)
	      similarity[iList].assign(sim.begin( ) + first_item, sim.begin( ) + last_item); // Sorted similarity within each category.
	  } // final results copy

      } // category loop
	
  } // parallel block


}

/*!
 *   \brief  Convenience function to map a reasonable query part ID for the result category ID.
 *
 *   \param [in]  categoryID      Category ID as string.
 *   \return Integer query part ID.
 */

int CMatch::ConvertResultCategoryIDToQueryPartID(const string &categoryID)
{
  // Convert string categoryID to integer categoryID.
  int id = 0; 
  try 
    {
      id = boost::lexical_cast<int>(categoryID.c_str());
    } 
  catch( boost::bad_lexical_cast const& ) 
    {
      // Failed to convert to integer. Do nothing. Use default value.
    }

  // Obtain reasonable query part ID for the result category ID.
  int queryPartID = 1; // Assume query is for bottom clothing.
  switch (id)
    {
    case 53159: // Tops & Blouses
    case 63861: // Dresses
    case 63869: // T-Shirts
    case 63866: // Sweaters
      queryPartID = 1; // Assume query is for bottom clothing.
      break;


    case 11554: // Jeans
    case 63862: // Coats & Jackets
    case 63863: // Pants
    case 63864: // Skirts
    case 63865: // Suits & Blazers
      queryPartID = 0; // Assume query is for top clothing.
      break;			
    }

  return queryPartID;
}


/*!
 *   \brief  Returns similarity of input pair of signatures.
 *   \param [in]  sigQuery  Signature of query image.
 *   \param [in]  sigDB     Signature of an image from the inventory.
 *   \param [in]  wtColor   Weight to balance against texture similarity. Range = [0, 1], Default = 0.5
 *   \param [in]  confStripesInQuery   Confidence that query image has stripes. Range = [0, 1]. Default = 0.
 *   \return                Similarity of input pair of signatures.
 */
double CMatch::CalculateSimilarity(const CSignature &sigQuery, const CSignature &sigDB, double wtColor, double confStripesInQuery)
{
  //***************************************************************************************************
  // Calculate similarity based on color histogram.
  //***************************************************************************************************
  double colorHistSim = 1.0 - compareHist(sigQuery.m_histColor, sigDB.m_histColor, CV_COMP_BHATTACHARYYA);

  double dClrBW  = sigQuery.m_fracColor - sigDB.m_fracColor;
  double wtClrBW = exp(-(dClrBW*dClrBW)/4); 

  // Effective similarity based on color.
  colorHistSim   = colorHistSim * wtClrBW;

  bool useTexture = (wtColor<1.0) && (sigDB.m_version>=2.0);

  // Effective similarity based on texture/patterns.
  double textureSim = 0.8; // Default value for backward compatibility (when only color is used).
  if (useTexture)
    {
      double orientHistSim = 1.0 - compareHist(sigQuery.m_histOrient, sigDB.m_histOrient, CV_COMP_BHATTACHARYYA);

      // Fraction of edge pixels.
      double dFracEdgePix    = 0.0;
      int    numPartitions   = (int) sigQuery.m_fracEdgePixels.total();
      for (int i=0; i<numPartitions; i++)
        {
	  double d = sigQuery.m_fracEdgePixels.at<float>(i) - sigDB.m_fracEdgePixels.at<float>(i);
	  dFracEdgePix += d*d;
        }
      double wtFracEdgePix = exp(-dFracEdgePix/0.0139); // 0.0139 = 2*(0.5/2/3)^2; 0.5 => 50% (for alternating stripes), 2 => # partitions, 3 => extent = 3 sigma.

      // Effective similarity based on histogram of orientation of gradients.
      orientHistSim = orientHistSim * wtFracEdgePix;

      // Similarity using FST.
      double FSTSim = 1.0 - compareHist(sigQuery.m_FST, sigDB.m_FST, CV_COMP_BHATTACHARYYA);

      // Effective similarity based on texture/patterns.
      textureSim = confStripesInQuery*orientHistSim + (1-confStripesInQuery)*FSTSim;
    }
  
  return std::pow(colorHistSim, wtColor) * std::pow(textureSim, 1-wtColor); 
     
}


/*!
 *   \brief   Returns confidence that stripes exist in the given query image.
 *   \param [in]  sigQuery  Query signature.
 *   \return           Confidence in [0, 1].
 */
double CMatch::ConfidenceThatStripesExist(const CSignature &sigQuery)
{
  if (sigQuery.m_version<2.0)
    return 0;

  // Fraction of edge pixels.
  int    numPartitions   = (int) sigQuery.m_fracEdgePixels.total();
  double sumFracEdgePix  = 0.0;
  for (int i=0; i<numPartitions; i++)
    {
      double fracEdgePixQuery = sigQuery.m_fracEdgePixels.at<float>(i);
      sumFracEdgePix += fracEdgePixQuery;
    }

  // Get maximum value of orientation histogram, after normalizing it to have unit sum.
  float sumOrientHist = 0;
  float maxOrientHist = 0;
  MatConstIterator_<float> orientHist_iter = sigQuery.m_histOrient.begin<float>();
  for(; orientHist_iter != sigQuery.m_histOrient.end<float>(); ++orientHist_iter)
    {
      sumOrientHist += *orientHist_iter;
      if (maxOrientHist < *orientHist_iter) maxOrientHist = *orientHist_iter;
    }
        
  double confMaxMeanRatio = 0;
  if (sumOrientHist > 0) 
    {
      confMaxMeanRatio = maxOrientHist / sumOrientHist / sigQuery.m_histOrient.cols; // Ratio of max to mean.
      confMaxMeanRatio = SShapedMembership(confMaxMeanRatio, 3, 4);             // Confidence based on ratio of max to mean values or orientation histogram.     
    }

  // Estimate confidence that there are stripes.
  double confFracEdge  = SShapedMembership(sumFracEdgePix, 0.05, 0.2); // Confidence based on fraction of edge pixels.
  double confStripes   = SShapedMembership((confFracEdge + confMaxMeanRatio)/2, 0.75, 0.9); // Range: [0, 1]
  confStripes          = (0.1 + confStripes*0.8); // Range: [0.1, 0.9]

  return confStripes;
}

/*!
 *   \brief   Returns membership based on an s-shaped function.
 *   \details See zmf from Matlab: http://www.mathworks.com/help/toolbox/fuzzy/zmf.html
 *            S-shaped function that is based on splines. Membership is 1 for x<=a and
 *            is 1 for x>=b. x, a, b are assumed to be in [0, 1].
 *   \param [in]  x  Input value in [0, 1].
 *   \param [in]  a  Left threshold.
 *   \param [in]  b  Right threshold.
 *   \return         Membership.
 */
double CMatch::SShapedMembership(double x, double a, double b)
{
  if (x <= a) return 0.0;
  else if(x >= b) return 1.0;
  else
    {  
      if (x <= (a+b)/2.0)
        {
	  double t = (x-a)/(b-a);
	  return 2*t*t;
        }
      else
        {
	  double t = (x-b)/(b-a);
	  return 1-2*t*t;
        }
    }
}

/*!
 *   \brief   Remaps value in [0, 1] based on 2 linear maps that meet at center.
 *   \details [0, 0.5] will be linearly mapped to [0, midValue] and
 *            [0.5, 1] will be linearly mapped to [midValue, 1].
 *   \param [in]  x         Input value in [0, 1].
 *   \param [in]  midValue  Value in [0,1]. 
 *   \return      Mapped value in [0,1].
 */
double CMatch::RemapIntervalWithCentralNode(double x, double midValue)
{
  if (midValue<0 || midValue>1)
    return x;

  if (x<=0.5)
    {
      return x*midValue/0.5;
    }
  else
    {
      return midValue + (x-0.5)*(1-midValue)/0.5;
    }
}



