/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   Copyright (c) 2015-2019 The plumed team
   (see the PEOPLE file at the root of the distribution for a list of names)

   See http://www.plumed.org for more information.

   This file is part of plumed, version 2.

   plumed 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 3 of the License, or
   (at your option) any later version.

   plumed 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 plumed.  If not, see <http://www.gnu.org/licenses/>.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
#include "AnalysisBase.h"
#include "tools/PDB.h"
#include "core/ActionRegister.h"
#include "reference/MetricRegister.h"
#include "reference/ReferenceConfiguration.h"

//+PLUMEDOC ANALYSIS EUCLIDEAN_DISSIMILARITIES
/*
Calculate the matrix of dissimilarities between a trajectory of atomic configurations.

\par Examples

*/
//+ENDPLUMEDOC

namespace PLMD {
namespace analysis {

class EuclideanDissimilarityMatrix : public AnalysisBase {
private:
  PDB mypdb;
  std::string mtype;
  Matrix<double> dissimilarities;
public:
  static void registerKeywords( Keywords& keys );
  EuclideanDissimilarityMatrix( const ActionOptions& ao );
/// Do the analysis
  void performAnalysis();
/// This ensures that classes that use this data know that dissimilarities were set
  bool dissimilaritiesWereSet() const { return true; }
/// Get information on how to calculate dissimilarities
  std::string getDissimilarityInstruction() const ;
/// Get the squared dissimilarity between two reference configurations
  double getDissimilarity( const unsigned& i, const unsigned& j );
/// This is just to deal with ActionWithVessel
  void performTask( const unsigned&, const unsigned&, MultiValue& ) const { plumed_error(); }
};

PLUMED_REGISTER_ACTION(EuclideanDissimilarityMatrix,"EUCLIDEAN_DISSIMILARITIES")

void EuclideanDissimilarityMatrix::registerKeywords( Keywords& keys ) {
  AnalysisBase::registerKeywords( keys ); keys.use("ARG"); keys.reset_style("ARG","optional");
  keys.add("compulsory","METRIC","EUCLIDEAN","the method that you are going to use to measure the distances between points");
  keys.add("atoms","ATOMS","the list of atoms that you are going to use in the measure of distance that you are using");
}

EuclideanDissimilarityMatrix::EuclideanDissimilarityMatrix( const ActionOptions& ao ):
  Action(ao),
  AnalysisBase(ao)
{
  parse("METRIC",mtype); std::vector<AtomNumber> atoms;
  if( my_input_data->getNumberOfAtoms()>0 ) {
    parseAtomList("ATOMS",atoms);
    if( atoms.size()!=0 ) {
      mypdb.setAtomNumbers( atoms );
      for(unsigned i=0; i<atoms.size(); ++i) {
        bool found=false;
        for(unsigned j=0; j<my_input_data->getAtomIndexes().size(); ++j) {
          if( my_input_data->getAtomIndexes()[j]==atoms[i] ) { found=true; break; }
        }
        if( !found ) {
          std::string num; Tools::convert( atoms[i].serial(), num );
          error("atom number " + num + " is not stored in any action that has been input");
        }
      }
      mypdb.addBlockEnd( atoms.size() );
    } else if( getNumberOfArguments()==0 ) {
      mypdb.setAtomNumbers( my_input_data->getAtomIndexes() );
      mypdb.addBlockEnd( my_input_data->getAtomIndexes().size() );
      if( mtype=="EUCLIDEAN" ) mtype="OPTIMAL";
    }
  }
  log.printf("  measuring distances using %s metric \n",mtype.c_str() );
  if( my_input_data->getArgumentNames().size()>0 ) {
    if( getNumberOfArguments()==0 && atoms.size()==0 ) {
      std::vector<std::string> argnames( my_input_data->getArgumentNames() );
      mypdb.setArgumentNames( argnames ); requestArguments( my_input_data->getArgumentList() );
    } else {
      std::vector<Value*> myargs( getArguments() );
      std::vector<std::string> inargnames( my_input_data->getArgumentNames() );
      std::vector<std::string> argnames( myargs.size() );
      for(unsigned i=0; i<myargs.size(); ++i) {
        argnames[i]=myargs[i]->getName();
        bool found=false;
        for(unsigned j=0; j<inargnames.size(); ++j) {
          if( argnames[i]==inargnames[j] ) { found=true; break; }
        }
        if( !found ) error("input named " + my_input_data->getLabel() + " does not store/calculate quantity named " + argnames[i] );
      }
      mypdb.setArgumentNames( argnames ); requestArguments( myargs );
    }
  }
}

void EuclideanDissimilarityMatrix::performAnalysis() {
  // Resize dissimilarities matrix and set all elements to zero
  if( !usingLowMem() ) {
    dissimilarities.resize( getNumberOfDataPoints(), getNumberOfDataPoints() ); dissimilarities=0;
  }
}

std::string EuclideanDissimilarityMatrix::getDissimilarityInstruction() const {
  return "TYPE=" + mtype;
}

double EuclideanDissimilarityMatrix::getDissimilarity( const unsigned& iframe, const unsigned& jframe ) {
  plumed_dbg_assert( iframe<getNumberOfDataPoints() && jframe<getNumberOfDataPoints() );
  if( !usingLowMem() ) {
    if( dissimilarities(iframe,jframe)>0. ) { return dissimilarities(iframe,jframe); }
  }
  if( iframe!=jframe ) {
    double dd;
    getStoredData( iframe, true ).transferDataToPDB( mypdb );
    auto myref1=metricRegister().create<ReferenceConfiguration>(mtype, mypdb);
    getStoredData( jframe, true ).transferDataToPDB( mypdb );
    auto myref2=metricRegister().create<ReferenceConfiguration>(mtype, mypdb);
    if( !usingLowMem() ) dd=dissimilarities(iframe,jframe) = dissimilarities(jframe,iframe) = distance( getPbc(), getArguments(), myref1.get(), myref2.get(), true );
    else dd=distance( getPbc(), getArguments(), myref1.get(), myref2.get(), true );
    return dd;
  }
  return 0.0;
}

}
}
