//=====================================================================================================================
// Includes
//=====================================================================================================================
#include "stdafx.h"
#include "StringUtil.h"
#include "CMantis.h"
#include "Base64.h"
#include <boost/algorithm/string.hpp>
#include <cstdlib>
#include <fstream>

// CMantis lib includes
#include "CFeatureExtraction.h"
#include "CFeatureMapper.hpp"
#include "CMD5AndUrl.h"
#include "UtilitiesWeb.h"
#include "CInventory.h"
#include "CMatch.h"
using namespace Mantis;

//=====================================================================================================================
// CMantis
//=====================================================================================================================

//---------------------------------------------------------------------------------------------------------------------
// Constructor
//---------------------------------------------------------------------------------------------------------------------
CMantis::CMantis(bool noxml) 
{
  noXML = noxml;
}

//---------------------------------------------------------------------------------------------------------------------
// Public methods
//---------------------------------------------------------------------------------------------------------------------
void CMantis::ProcessCommand(const string& cmd, const string& remainder)
{
  std::string error;
  std::string result;

  try 
    {
      if (cmd == "match")
	result = Match(remainder);
      else 
	if (cmd == "createsignature")
	  result = CreateSignature(remainder);
	else
	  if (cmd == "loadinventory")
	    result = LoadInventory(remainder);
	  else
	      error= "Unknown command: " + cmd;
    }
  
  catch(std::exception& ex)
    {
      error = std::string("Exception: ") + ex.what();
    }

  if (error.length() == 0 && result.empty()) 
    error = "Internal Error";

  if (!error.empty())
    cout << CreateErrorResponse(error) << endl << flush;
  else
    cout << CreateSuccessResponse(result) << endl << flush;
  
}

//---------------------------------------------------------------------------------------------------------------------
string CMantis::LoadInventory(const string& remainder)
{
  CKeyValueMap parms = GetLoadInventoryDefaults();
  ApplyKeyValuePairs(remainder, parms);
  // DumpKeyValuePairs(parms);
  ValidateLoadInventoryParms(parms);
  string ret;

  std::istream& is(cin);

  std::string header;
  vector<std::string> dump_parms;
  getline(is, header);
  boost::split(dump_parms, header, boost::is_any_of(" \t\n"));
  
  if (dump_parms.size()==3 && dump_parms[0]=="BEGIN_VISION_DUMP")
    {
      int siteID = getNumericSiteID(dump_parms[1]);
      CInventory* inventory = CInventory::Instance(siteID);
      
      if (inventory && inventory->LoadInventory(is, dump_parms[2]))
	ret = "success";
    }
  
  if (ret.empty())
    throw std::runtime_error(string("Failed to load inventory for site id ") + parms["siteID"]);
  
  return ret;
}


//---------------------------------------------------------------------------------------------------------------------
// Implementation
//---------------------------------------------------------------------------------------------------------------------

//---------------------------------------------------------------------------------------------------------------------
// Match
//
// Match command line:
//
// match key=value [key=value ...]
//
// Note that all values are TEXT on the incoming command line, and are parsed into the "Value type" shown in the table.
//
// Key            Value type      Description
// -------------- --------------- -------------------------------------------------------------------------------------
// colorWeight    float           Color weight (0.0 - 1.0)
// category       int list        Empty or comma-delimited list of eBay category IDs
// context        string          similar|color-science|tar|hybrid|street-only|nicole-richie|paris-hilton
// part           string          auto|torso|legs
// rankThreshold  float           Rank threshold (0.0 - 1.0)
// pageSize       int             Results per page
// pageNumber     int             Page number starting at 0
// imageMime      string          MIME type of image (e.g. image/png)
// image          base64          Base-64-encoded binary image data for the swatch image
//---------------------------------------------------------------------------------------------------------------------
string CMantis::Match(const string& remainder)
{
  CKeyValueMap parms = GetMatchDefaults();
  ApplyKeyValuePairs(remainder, parms);
  DumpKeyValuePairs(parms);
  ValidateMatchParms(parms);

  // Decode (Base64) image
  std::vector<BYTE> imageData = base64_decode(parms.find("image")->second);
  cerr << "Match decoded an image of type " << parms["imageMime"] << " of " << imageData.size() << " bytes.\n";
  
  // Match...
  cv::Mat mat = readImage(imageData);
  // 
  int siteID = getNumericSiteID(parms["siteID"]);
  CFeatureMapper* mapper = CInventory::Instance(siteID)->GetFtrMapper();
  CFeatureExtraction fx(mat, *mapper);

  // Determine categories to match against.
  vector<string> listCategories;
  if (!parms["category"].empty())
    boost::split(listCategories, parms["category"], boost::is_any_of(", "));
     
  // check for optional parameter "rankThreshold" (minimum similar score), 
  // default to 0 as threshold
  float minSimilarity = (float) std::max(0.0F, (float)std::atof(parms["rankThreshold"].c_str()));
 
  // Read weight for color similarity. Range: [0, 1]
  double colorWeight = (double) std::min(1.0, std::max(0.0, std::atof(parms["colorWeight"].c_str())));

  // Read context for complementary recommendation.
  int context = 0;  // Default: Do not use complementary recommendation.
  bool complimentary=false;

  if (parms["context"]=="hybrid") {
    complimentary = true; context=3;
  }
  
  // Read partID for complementary recommendation.
  int part = -1; // std::min(1, std::atoi(parms["partID"].c_str())); // Default: Assume query image corresponds to torso.

  // pass in and store the total entries per category
  vector<int> totalEntries;
  
  // Match query image against requested categories.
  CMatch fm;
  CMatch::SimilarityVector similarity;

  std::string maxResultsStr   = parms["maxResults"];
  int maxResults = atol(maxResultsStr.c_str());
  cerr << "Running Match: minSimilarity=" << minSimilarity << ", colorWeight=" << colorWeight << 
    ", part=" << part << ", context=" << context << ", complimentary=" << complimentary << ", maxResults=" << maxResults << "\n"; 
  fm.Match(siteID, fx.getSignature(), listCategories, totalEntries, similarity, minSimilarity, colorWeight, part, context, complimentary, maxResults);

  // Prepare output.            
  
  string itemList("{ \"results\": [\n");
  
  for (int icat=0; icat < (int) similarity.size(); icat++)        
    {       
      if (icat)
	itemList.append(",");

      itemList.append("{\"category\": ").append(listCategories[icat]).append(", \"matches\": [");
      
      for (CMatch::SimilarityRow::const_iterator it =  similarity[icat].begin(); 
	   it != similarity[icat].end(); it++)
	{
	  if (it != similarity[icat].begin())
	    itemList.append(", ");
	  // There is no ftoa in <cstdlib>
	  char ftoa[16];
	  snprintf(ftoa, sizeof(ftoa), "%.02f", it->sim_);
	  itemList.append("{\"rank\": ").append(ftoa).append(", \"ids\": [").append(it->sig_->m_itemIDs).append("]}");
	}
      itemList.append("]}\n");
    }
  itemList.append("\n]}\n");

  return itemList;
}


//---------------------------------------------------------------------------------------------------------------------
// CreateSignature
//
// CreateSignature command line:
//
// createSignature key=value [key=value ...]
//
// Note that all values are TEXT on the incoming command line, and are parsed into the "Value type" shown in the table.
//
// Key            Value type      Description
// -------------- --------------- -------------------------------------------------------------------------------------
// crop           boolean         Crop image or not
// image          base64          Base-64-encoded binary image data for the swatch image
//---------------------------------------------------------------------------------------------------------------------
string CMantis::CreateSignature(const string& remainder)
{
  CKeyValueMap parms = GetCreateSignatureDefaults();
  ApplyKeyValuePairs(remainder, parms);
  //  DumpKeyValuePairs(parms);

  ValidateCreateSignatureParms(parms);
  // Decode (Base64) image
  vector<BYTE> imageData = base64_decode(parms.find("image")->second);
  //  cerr << "CreateSignature decoded an image of type " << parms["imageMime"] << " of " << imageData.size() << " bytes.\n";
  
  int siteID = getNumericSiteID(parms["siteID"]);
  cv::Mat mat = readImage(imageData);
  CFeatureMapper* mapper = CInventory::Instance(siteID)->GetFtrMapper();
  CFeatureExtraction cex(mat, *mapper);  
  std::ostringstream os;
  cex.SerializeSignatureJSON(os);
  return os.str();
}



//---------------------------------------------------------------------------------------------------------------------
CKeyValueMap CMantis::GetMatchDefaults()
{
  CKeyValueMap result;
  result["siteID"] = "EBAY-US";
  result["colorWeight"] = "0.5";
  result["category"] = "";
  result["context"] = "similar";
  result["part"] = "auto";
  result["rankThreshold"] = "0.7";
  return result;
}

//---------------------------------------------------------------------------------------------------------------------
CKeyValueMap CMantis::GetCreateSignatureDefaults()
{
  CKeyValueMap result;
  result["siteID"] = "EBAY-US";
  result["cropImage"] = "yes";
  return result;
}

//---------------------------------------------------------------------------------------------------------------------
CKeyValueMap CMantis::GetLoadInventoryDefaults()
{
  CKeyValueMap result;
  // default is : us site
  result["siteID"] = "EBAY-US";
  // extra descriptor used to communicate
  result["fd"] = 3;
  return result;
}


//---------------------------------------------------------------------------------------------------------------------
void CMantis::ApplyKeyValuePairs(const string& remainder, CKeyValueMap& pairs)
{
  CStringVec pairstrings = Tokenize(remainder, " ");
  CStringVec::const_iterator it = pairstrings.begin();
  for (CStringVec::const_iterator it = pairstrings.begin(); it != pairstrings.end(); it++)
  {
    string source(*it), key = "", value = "";

    // I don't use Tokenize here because I only care about the first "=". Some values (like the the base64 image) will
    // contain = signs too, and I don't care about those.
    size_t pos = source.find('=');
    if (pos != string::npos)
    {
      key = source.substr(0, pos);
      if (source.length() > pos)
      {
        value = source.substr(pos + 1, source.length() - pos + 1);
      }
      if (key.length() > 0)
      {
        pairs[key] = value;
      }
    }
  }
}

//---------------------------------------------------------------------------------------------------------------------
void CMantis::DumpKeyValuePairs(const CKeyValueMap& pairs)
{
  CKeyValueMap::const_iterator it = pairs.begin();
  while (it != pairs.end())
  {
    if (it->second.length() > 40)
    {
      cerr << it->first << ": " << it->second.substr(0, 40) << "..." << endl;
    }
    else
    {
      cerr << it->first << ": " << it->second << endl;
    }
    it++;
  }
}

//---------------------------------------------------------------------------------------------------------------------

void CMantis::CheckRequired(const CKeyValueMap& pairs, const string& key)
{
  if (pairs.find(key) == pairs.end())
    {
      string msg = "ERROR: Required parameter not found: " + key;
      throw std::runtime_error(msg);
    }
 }


//---------------------------------------------------------------------------------------------------------------------
void CMantis::ValidateCreateSignatureParms(const CKeyValueMap& pairs)
{
  CheckRequired(pairs, "siteID");
  CheckRequired(pairs, "imageMime");
  CheckRequired(pairs, "image");
}

//---------------------------------------------------------------------------------------------------------------------
void CMantis::ValidateMatchParms(const CKeyValueMap& pairs)
{
  CheckRequired(pairs, "siteID");
  CheckRequired(pairs, "imageMime");
  CheckRequired(pairs, "image");
}

//---------------------------------------------------------------------------------------------------------------------
void CMantis::ValidateLoadInventoryParms(const CKeyValueMap& pairs)
{
  CheckRequired(pairs, "fd");
}


//---------------------------------------------------------------------------------------------------------------------
// Note: This function escapes the "errorText" value - it is assumed to be just a textual description of the error
// condition.
string CMantis::CreateErrorResponse(const string& str)
{
  std::string errorText = str;
  errorText.erase (std::remove(errorText.begin(), errorText.end(), '\n'), errorText.end());
  if (noXML)
    return "{ \"error\": \"" + errorText + "\"}";

  ostringstream result;
  
  result << "<?xml version=\"1.0\"?><!--Response from CMantis Index Service -->";
  result << "<response><status>ERROR</status><data>" << EncodeForXml(errorText) << "</data></response>\n";
  return result.str();
}

//---------------------------------------------------------------------------------------------------------------------
// Note: This function assumes that the text in "contents" is XML and includes it verbatim (without further escaping)
// within the stock response XML wrapper.
//---------------------------------------------------------------------------------------------------------------------

string CMantis::CreateSuccessResponse(const string& contents)
{
  if (noXML)
    return contents;

  ostringstream result;

  result << "<?xml version=\"1.0\"?><!--Response from CMantis Index Service -->";
  result << "<response><status>SUCCESS</status><data>" << contents << "</data></response>\n";
  return result.str();
}

bool CMantis::noXML = false;
