// -*- c++ -*-

// This file is part of the Collective Variables module (Colvars).
// The original version of Colvars and its updates are located at:
// https://github.com/Colvars/colvars
// Please update all Colvars source files before making any changes.
// If you wish to distribute your changes, please submit them to the
// Colvars repository at GitHub.

#ifndef NEURALNETWORKCOMPUTE_H
#define NEURALNETWORKCOMPUTE_H

#include <vector>
#include <functional>
#include <string>
#include <cmath>
#include <memory>
#include <map>

#ifdef LEPTON
#include "Lepton.h"
#endif

namespace neuralnetworkCV {
/// mapping from a string to the activation function and its derivative
extern std::map<std::string, std::pair<std::function<double(double)>, std::function<double(double)>>> activation_function_map;

#ifdef LEPTON
// allow to define a custom activation function
class customActivationFunction {
public:
    /// empty constructor
    customActivationFunction();
    /// construct by an mathematical expression
    customActivationFunction(const std::string& expression_string);
    /// copy constructor
    customActivationFunction(const customActivationFunction& source);
    /// overload assignment operator
    customActivationFunction& operator=(const customActivationFunction& source);
    /// setter for the custom expression
    void setExpression(const std::string& expression_string);
    /// getter for the custom expression
    std::string getExpression() const;
    /// evaluate the value of an expression
    double evaluate(double x) const;
    /// evaluate the gradient of an expression
    double derivative(double x) const;
private:
    std::string expression;
    std::unique_ptr<Lepton::CompiledExpression> value_evaluator;
    std::unique_ptr<Lepton::CompiledExpression> gradient_evaluator;
    double* input_reference;
    double* derivative_reference;
};
#endif

class denseLayer {
private:
    size_t m_input_size;
    size_t m_output_size;
    std::function<double(double)> m_activation_function;
    std::function<double(double)> m_activation_function_derivative;
#ifdef LEPTON
    bool m_use_custom_activation;
    customActivationFunction m_custom_activation_function;
#else
    static const bool m_use_custom_activation = false;
#endif
    /// weights[i][j] is the weight of the i-th output and the j-th input
    std::vector<std::vector<double>> m_weights;
    /// bias of each node
    std::vector<double> m_biases;
public:
    /// empty constructor
    denseLayer() {}
    /*! @param[in]  weights_file    filename of the weights file
     *  @param[in]  biases_file     filename of the biases file
     *  @param[in]  f               activation function
     *  @param[in]  df              derivative of the activation function
     */
    denseLayer(const std::string& weights_file, const std::string& biases_file, const std::function<double(double)>& f, const std::function<double(double)>& df);
#ifdef LEPTON
    /*! @param[in]  weights_file                 filename of the weights file
     *  @param[in]  biases_file                  filename of the biases file
     *  @param[in]  custom_activation_expression the expression of the custom activation function
     */
    denseLayer(const std::string& weights_file, const std::string& biases_file, const std::string& custom_activation_expression);
#endif
    /// read data from file
    void readFromFile(const std::string& weights_file, const std::string& biases_file);
    /// setup activation function
    void setActivationFunction(const std::function<double(double)>& f, const std::function<double(double)>& df);
    /// compute the value of this layer
    void compute(const std::vector<double>& input, std::vector<double>& output) const;
    /// compute the gradient of i-th output wrt j-th input
    double computeGradientElement(const std::vector<double>& input, const size_t i, const size_t j) const;
    /// output[i][j] is the gradient of i-th output wrt j-th input
    void computeGradient(const std::vector<double>& input, std::vector<std::vector<double>>& output_grad) const;
    /// get the input size
    size_t getInputSize() const {
        return m_input_size;
    }
    /// get the output size
    size_t getOutputSize() const {
        return m_output_size;
    }
    /// getter for weights and biases
    double getWeight(size_t i, size_t j) const {
        return m_weights[i][j];
    }
    double getBias(size_t i) const {
        return m_biases[i];
    }
    ~denseLayer() {}
};

class neuralNetworkCompute {
private:
    std::vector<denseLayer> m_dense_layers;
    std::vector<double> m_input;
    /// temporary output for each layer, useful to speedup the gradients' calculation
    std::vector<std::vector<double>> m_layers_output;
    std::vector<std::vector<std::vector<double>>> m_grads_tmp;
    std::vector<std::vector<double>> m_chained_grad;
private:
    /// helper function: multiply two matrix constructed from 2D vector
    static std::vector<std::vector<double>> multiply_matrix(const std::vector<std::vector<double>>& A, const std::vector<std::vector<double>>& B);
public:
    neuralNetworkCompute(): m_dense_layers(0), m_layers_output(0) {}
    neuralNetworkCompute(const std::vector<denseLayer>& dense_layers);
    bool addDenseLayer(const denseLayer& layer);
    // for faster computation
    const std::vector<double>& input() const {return m_input;}
    std::vector<double>& input() {return m_input;}
    /// compute the values and the gradients of all output nodes
    void compute();
    double getOutput(const size_t i) const {return m_layers_output.back()[i];}
    double getGradient(const size_t i, const size_t j) const {return m_chained_grad[i][j];}
    /// get a specified layer
    const denseLayer& getLayer(const size_t i) const {return m_dense_layers[i];}
    /// get the number of layers
    size_t getNumberOfLayers() const {return m_dense_layers.size();}
};

}
#endif
