/*
 *  Copyright (C) 2004-2019 Edward F. Valeev
 *
 *  This file is part of Libint.
 *
 *  Libint is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Libint 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Libint.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#ifndef _libint2_src_bin_libint_entity_h_
#define _libint2_src_bin_libint_entity_h_

#include <iomanip>
#include <limits>
#include <string>
#include <sstream>
#include <dgvertex.h>
#include <class_registry.h>

namespace libint2 {

  /**
     EntityTypes enumerates the types of objects Entity can represent
  */
  namespace EntityTypes {
    typedef enum {fp, integer} EntityTypeEnum;

    template <unsigned int TypeIndex>
    struct EntityType {
      static unsigned int type2int() {
        return TypeIndex;
      }
    };

    typedef EntityType<fp> FP;
    typedef EntityType<integer> Int;

    static const unsigned int ntypes = 2;
  };

  /// Product of 2 types
  template <typename T, typename U>
    struct ProductType;
  template <> struct ProductType<int,int>         { typedef int result; };
  template <> struct ProductType<int,double>      { typedef double result; };
  template <> struct ProductType<double,int>      { typedef double result; };
  template <> struct ProductType<double,double>   { typedef double result; };
  template <> struct ProductType<EntityTypes::Int,int>     { typedef EntityTypes::Int result; };
  template <> struct ProductType<EntityTypes::Int,double>  { typedef EntityTypes::FP result; };
  template <> struct ProductType<EntityTypes::FP,int>      { typedef EntityTypes::FP result; };
  template <> struct ProductType<EntityTypes::FP,double>   { typedef EntityTypes::FP result; };
  template <> struct ProductType<int,EntityTypes::Int>     { typedef EntityTypes::Int result; };
  template <> struct ProductType<double,EntityTypes::Int>  { typedef EntityTypes::FP result; };
  template <> struct ProductType<int,EntityTypes::FP>      { typedef EntityTypes::FP result; };
  template <> struct ProductType<double,EntityTypes::FP>   { typedef EntityTypes::FP result; };
  template <> struct ProductType<EntityTypes::Int,EntityTypes::Int>     { typedef EntityTypes::Int result; };
  template <> struct ProductType<EntityTypes::Int,EntityTypes::FP>      { typedef EntityTypes::FP result; };
  template <> struct ProductType<EntityTypes::FP,EntityTypes::Int>      { typedef EntityTypes::FP result; };
  template <> struct ProductType<EntityTypes::FP,EntityTypes::FP>       { typedef EntityTypes::FP result; };

  /// Converts x to its string representation
  template <typename T>
    std::string to_string(const T& x) {
    std::ostringstream oss;  oss << std::scientific << std::setprecision(std::numeric_limits<T>::digits10 + 1) << x;  return oss.str();
  }

  /**
  Entity is a base class for all objects that exist at compile or runtime of the generated code.
  */
  class Entity
  {
    public:
    virtual ~Entity() {}
    /// Return id string
    const std::string& id() const { return id_; }

    protected:
    Entity(const std::string& id) : id_(id) {}

    private:
    /// Short id label
    std::string id_;

  };

  /**
     RTimeEntity is an Entity of type T that exists at runtime of the generated code (hence
     has no value known at compile-time)
  */
  template <class T>
  class RTimeEntity :
  public Entity,
  public DGVertex
  {
    public:
    typedef typename DGVertex::KeyType key_type;

    RTimeEntity(const std::string& id, bool p = true) :
      Entity(id), DGVertex(ClassInfo<RTimeEntity>::Instance().id()), precomputed_(p)
    {
      FNVStringHash SH;
      key_ = KeyTypes::cast(SH.hash(id));
#if DEBUG
std::cout << "Allocated RTimeEntity id = " << this->id() << std::endl;
#endif
    }

    virtual ~RTimeEntity()
    {
#if DEBUG
      std::cout << "Deallocated RTimeEntity id = " << this->id() << std::endl;
#endif
    }

    /// Implementation of DGVertex::size()
    unsigned int size() const { return 1; }

    /// Implementation of DGVertex::equiv()
    bool equiv(const SafePtr<DGVertex>& a) const
    {
      if (a->typeid_ == typeid_) {
#if USE_INT_KEY_TO_COMPARE
        return key() == a->key() && label() == a->label();
#else
        SafePtr<RTimeEntity> a_cast = static_pointer_cast<RTimeEntity,DGVertex>(a);
        return id() == a_cast->id();
#endif
      }
      else
        return false;
    }

    /// Implementation of DGVertex::label()
    const std::string& label() const
    {
      return Entity::id();
    }
    /// Implementation of DGVertex::id()
    const std::string& id() const
    {
      return label();
    }
    /// Implementation of DGVertex::description()
    std::string description() const
    {
      ostringstream os;
      os << "RTimeEntity: " << id();
      const std::string descr = os.str();
      return descr;
    }
    /// Implements Hashable::key()
    typename DGVertex::KeyReturnType key() const {
      return key_;
    }

    private:
    /// Implementation of DGVertex::this_precomputed()
    bool this_precomputed() const
    {
      return precomputed_;
    }

    key_type key_;
    /// RTimeEntity can go either way. Example of a precomputed quartity is a quantity passed by the user to the library.
    /// Example of a quantity that is not precomputed and thus must be evaluated explicitly by the graph is
    /// a product of precomputed RTimeEntity with a CTimeEntity
    bool precomputed_;
  };

  /**
     CTimeEntity is an Entity of type T that exists at compile-time of the generated code (hence
     has a value known at compile-time)
  */
  template <class T>
    class CTimeEntity :
    public Entity,
    public DGVertex
    {
      public:
      CTimeEntity(const T& val) :
        Entity(to_string(val)), DGVertex(ClassInfo<CTimeEntity>::Instance().id()), value_(val)
        {
#if DEBUG
          std::cout << "Allocated CTimeEntity id = " << this->id() << " value = " << value() << std::endl;
#endif
        }

      virtual ~CTimeEntity()
        {
#if DEBUG
          std::cout << "Deallocated CTimeEntity id = " << this->id() << " value = " << value() << std::endl;
#endif
        }

      /// Implementation of DGVertex::size()
      unsigned int size() const { return 1; }

      /// Implementation of DGVertex::equiv()
      bool equiv(const SafePtr<DGVertex>& a) const
      {
	if (a->typeid_ == typeid_) {
#if USE_INT_KEY_TO_COMPARE
          return key() == a->key();
#else
          SafePtr<CTimeEntity> a_cast = static_pointer_cast<CTimeEntity,DGVertex>(a);
	  return id() == a_cast->id();
#endif
	}
	else
	  return false;
      }

      /// Implementation of DGVertex::label()
      const std::string& label() const
      {
        return Entity::id();
      }
      /// Implementation of DGVertex::id()
      const std::string& id() const
      {
        return label();
      }
      /// Implementation of DGVertex::description()
      std::string description() const
      {
        ostringstream os;
        os << "CTimeEntity: " << id();
        const std::string descr = os.str();
        return descr;
      }

      /// returns the value
      typename KeyTraits<T>::ReturnType value() const { return value_; }

      /// Implements Hashable::key()
      typename DGVertex::KeyReturnType key() const {
        if (std::is_floating_point<T>::value) {
          if (not std::is_same<T,double>::value)
            throw std::runtime_error("CTimeEntity<Real> only supported when Real==double");
          return static_cast<typename DGVertex::KeyReturnType>(*reinterpret_cast<const unsigned long*>(&value_));
        }
        else
          return static_cast<typename DGVertex::KeyReturnType>(value());
      }

    private:
      T value_;

      /// Implementation of DGVertex::this_precomputed()
      bool this_precomputed() const
      {
        return true;
      }

    };


  /** Creates product A*B. Exact type depends on type of A -- if A is a runtime-entity,
      then the result is a runtime entity as well. Otherwise the result is a compile-time entity.
  */
//  template <typename T>
//    SafePtr<Entity>
//    operator*(const SafePtr<Entity>& A, const SafePtr< CTimeEntity<T> >& B);

  /** Creates product A*B.
  */
  template <typename T, typename U>
    SafePtr< CTimeEntity< typename ProductType<T,U>::result > >
    operator*(const SafePtr< CTimeEntity<T> >& A, const SafePtr< CTimeEntity<U> >& B)
    {
      typedef CTimeEntity< typename ProductType<T,U>::result > prodtype;
      return SafePtr<prodtype>(new prodtype(A->value()*B->value()));
    }

  /** Creates product A*B.
  */
  template <typename T, typename U>
    SafePtr< RTimeEntity< typename ProductType<T,U>::result > >
    operator*(const SafePtr< RTimeEntity<T> >& A, const SafePtr< CTimeEntity<U> >& B)
    {
      typedef RTimeEntity< typename ProductType<T,U>::result > prodtype;
      ostringstream oss;
      oss << A->id() << "*" << B->id();
      // TODO this should be false, but the logic of DirectedGraph construction depends on this being true
      const bool not_precomputed = true;
      return SafePtr<prodtype>(new prodtype(oss.str(), not_precomputed));
    }
  // TODO should be possible to enable this, but this creates RTimeEntities that should not be precomputed, see the comment above
#if 0
  /** Creates product B*A.
  */
  template <typename T, typename U>
    SafePtr< RTimeEntity< typename ProductType<T,U>::result > >
    operator*(const SafePtr< CTimeEntity<U> >& B, const SafePtr< RTimeEntity<T> >& A)
    {
      return A * B;
    }
#endif

};

#endif

