/*-------------------------------------------------------------------------------------*/
/*  NOMAD - Nonsmooth Optimization by Mesh Adaptive Direct search - version 3.0.0      */
/*                                                                                     */
/*  Copyright (C) 2001-2008  Mark Abramson        - the Boeing Company, Seattle        */
/*                           Charles Audet        - Ecole Polytechnique, Montreal      */
/*                           Gilles Couture       - Ecole Polytechnique, Montreal      */
/*                           John Dennis          - Rice University, Houston           */
/*                           Sebastien Le Digabel - Ecole Polytechnique, Montreal      */
/*                                                                                     */
/*  funded in part by AFOSR and Exxon Mobil                                            */
/*                                                                                     */
/*  Author: Sebastien Le Digabel                                                       */
/*                                                                                     */
/*  Contact information:                                                               */
/*    Ecole Polytechnique de Montreal - GERAD                                          */
/*    C.P. 6079, Succ. Centre-ville, Montreal (Quebec) H3C 3A7 Canada                  */
/*    e-mail: Sebastien.Le.Digabel@gerad.ca                                            */
/*    phone : 1-514-340-6053 #6673                                                     */
/*    fax   : 1-514-340-5665                                                           */
/*                                                                                     */
/*  This program 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.                                                                           */
/*                                                                                     */
/*  This program 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 this  */
/*  program. If not, see <http://www.gnu.org/licenses/>.                               */
/*                                                                                     */
/*  You can find information on the NOMAD software at www.gerad.ca/nomad               */
/*-------------------------------------------------------------------------------------*/
#include "Cache.hpp"
using namespace std;

/*-----------------------------------*/
/*   static members initialization   */
/*-----------------------------------*/
set<string> Cache::_locked_files;

/*------------------------------------------------------*/
/*                         sizeof                       */
/*------------------------------------------------------*/
int Cache::size_of ( void ) const {
  int sum =
    sizeof (_cache1        ) +
    sizeof (_cache2        ) +
    sizeof (_cache3        ) +
    sizeof (_locked_file   ) +
    sizeof (_display_degree) +
    sizeof (_surrogate     ) +
    sizeof (_it            );

  const Eval_Point * cur = begin();
  while (cur) {
    sum += cur->size_of();
    cur = next();
  }

  return sum;
}

/*------------------------------------------------------*/
/*  . search x in the cache                             */
/*  . return NULL if x is not in the cache              */
/*------------------------------------------------------*/
const Eval_Point * Cache::find ( const Eval_Point * x ) const {
  set<Cache_Point>::const_iterator it;
  cache_index_type                 cache_index;
  
  return find ( x , it , cache_index );
}

/*------------------------------------------------------*/
/*  . search x in the cache                             */
/*  . return NULL if x is not in the cache              */
/*  . this private version returns an iterator and      */
/*    indicates in which set the point has been found   */
/*------------------------------------------------------*/
const Eval_Point * Cache::find ( const Eval_Point                 * x           ,
				 set<Cache_Point>::const_iterator & it          ,
				 cache_index_type                 & cache_index   ) const {


  // search in _cache2 (points to write in a cache file):
  Cache_Point cp (x);
  it = _cache2.find ( cp );
  if ( it != _cache2.end() ) {
    cache_index = _CACHE_2_;
    return it->get_point();
  }

  // search in _cache3 (points saved in a file):
  it = _cache3.find ( cp );
  if ( it != _cache3.end() ) {
    cache_index = _CACHE_3_;
    return it->get_point();
  }

  // search in _cache1 (points read in an initial cache file):
  it = _cache1.find ( cp );
  if ( it != _cache1.end() ) {
    cache_index = _CACHE_1_;
    return it->get_point();
  }

  cache_index = _UNDEFINED_CACHE_;
  return NULL;
}

/*---------------------------------------------------------------------*/
/*                             erase a point                           */
/*---------------------------------------------------------------------*/
/*      return true if the point has been found and has been erased    */
/*---------------------------------------------------------------------*/
bool Cache::erase ( const Eval_Point * x ) {

  set<Cache_Point>::iterator it;
  cache_index_type           cache_index;
  
  // the point has been found:
  if ( find ( x , it , cache_index ) ) {
    delete x;
    switch (cache_index) {
    case _CACHE_1_:
      _cache1.erase(it);
      break;
    case _CACHE_2_:
      _cache2.erase(it);
      break;
    case _CACHE_3_:
      _cache3.erase(it);
      break;
    case _UNDEFINED_CACHE_:
      break;
    }
    return true;
  }
  return false;
}

/*-----------------------------------------------------*/
/*  erase all points in cache and unlock _locked_file  */
/*-----------------------------------------------------*/
void Cache::clear ( void ) {
  const Eval_Point * x = begin();
  while (x) {
    delete x;
    x = next();
  }
  _cache1.clear();
  _cache2.clear();
  _cache3.clear();
  unlock();
}

/*------------------------------------------------------------------*/
/*             . insertion of a point in the cache (_cache2)        */
/*             . supposes that x is NOT already in cache            */
/*------------------------------------------------------------------*/
void Cache::insert ( const Eval_Point * x ) {
  Cache_Point cp  (x    );
  _cache2.insert  (cp   );
  x->set_in_cache (true);
}

/*-------------------------------------------------------------------------*/
/*  . insert all point of 'c' in the current cache                         */
/*  . this empties 'c' points (to avoid Eval_Point copies)                 */
/*  . c._locked_file and this->_locked_file are different by construction  */
/*-------------------------------------------------------------------------*/
void Cache::insert ( Cache & c ) {

  if ( &c == this )
    return;

  Point              bbo_cache , bbo_cur;
  const Eval_Point * cache_x;
  const Eval_Point * cur = c.begin();

  while (cur) {
    
    cache_x = find(cur);

    // the current point is already in cache:
    if ( cache_x ) {
      update ( get_modifiable_point(*cache_x) , *cur );
      delete cur;
    }

    // point not in cache:
    else
      insert(cur);

    cur = c.next();
  }

  c._cache1.clear();
  c._cache2.clear();
  c._cache3.clear();
}

/*------------------------------------------------------------------*/
/*  . begin() and next() methods, to browse the cache               */
/*                                                                  */
/*  . example of use:                                               */
/*                                                                  */
/*        const Eval_Point * cur = cache.begin();                   */
/*        while (cur) {                                             */
/*          ...                                                     */
/*          cur = cache.next();                                     */
/*        }                                                         */
/*                                                                  */
/*  . we browse in this order: _cache2, _cache3, and _cache_1       */
/*------------------------------------------------------------------*/

// begin():
// --------
const Eval_Point * Cache::begin ( void ) const {
  if ( !_cache2.empty() ) {
    _it = _cache2.begin();
    return _it->get_point();
  }   
  if ( !_cache3.empty() ) {
    _it = _cache3.begin();
    return _it->get_point();
  }
  if ( !_cache1.empty() ) {
    _it = _cache1.begin();
    return _it->get_point();
  }
  return NULL;
}

// next():
// -------
const Eval_Point * Cache::next ( void ) const {
  
  ++_it;

  if ( _it == _cache2.end() ) {
    if ( !_cache3.empty() ) {
      _it = _cache3.begin();
      return _it->get_point();
    }
    if ( !_cache1.empty() ) {
      _it = _cache1.begin();
      return _it->get_point();
    }
    return NULL;
  }


  if ( _it == _cache3.end() ) {
    if ( !_cache1.empty() ) {
      _it = _cache1.begin();
      return _it->get_point();
    }
    return NULL;
  }

  if ( _it == _cache1.end() )
    return NULL;

  return _it->get_point();
}

/*---------------------------------------------------------------------*/
/*                        check if a file is locked                    */
/*---------------------------------------------------------------------*/
bool Cache::is_locked ( const string & file_name ) {
  if ( file_name == _locked_file )
    return true;
  return ( Cache::_locked_files.find ( file_name ) != Cache::_locked_files.end() );
}

/*---------------------------------------------------------------------*/
/*                              lock a file                            */
/*---------------------------------------------------------------------*/
bool Cache::lock ( const string & file_name ) {

  if ( is_locked(file_name) )
    return false;

  Cache::_locked_files.insert(file_name);
  _locked_file = file_name;

  return true;
}

/*---------------------------------------------------------------------*/
/*                          unlock the locked file                     */
/*---------------------------------------------------------------------*/
void Cache::unlock ( void ) {

  if ( _locked_file.empty() )
    return;

  set<string>::iterator it = Cache::_locked_files.find ( _locked_file );
  if ( it != Cache::_locked_files.end() )
    _locked_files.erase(it);

  _locked_file.clear();
}

/*---------------------------------------------------------------------*/
/*                   . read points from a cache file                   */
/*                   . fill the set _cache1                            */
/*---------------------------------------------------------------------*/
bool Cache::read_points_from_cache_file ( ifstream & in , const int * pm ) {

  Clock c;

  // the stream is placed at the first point (after the CACHE_FILE_ID tag):
  in.seekg ( sizeof(CACHE_FILE_ID) , ios::beg );

  Cache_File_Point   cfp;
  Eval_Point       * cur;
  const Eval_Point * cache_x;

  // main loop:
  while ( !in.eof() ) {

    // reading of the Cache_File_Point:
    if ( !cfp.read(in) ) {
      if (in.eof())
	break;
      return false;
    }

    // we ignore this cache file point if it has a different
    // number of black-box outputs than *pm:
    if ( pm && cfp.get_m() != *pm )
      continue;

    // creation of the Eval_Point:
    cur = new Eval_Point(cfp);

    // surrogate?
    cur->set_surrogate ( _surrogate );

    // we look if the current point is already in cache:
    cache_x = find(cur);
    
    // the current point is already in cache:
    if ( cache_x ) {
      update ( get_modifiable_point(*cache_x) , *cur );
      delete cur;
    }

    // point not in cache: insertion in _cache1:
    else {
      Cache_Point cp    (cur );
      _cache1.insert    (cp  );
      cur->set_in_cache (true);
    }

  } // end of main loop

  // display stats on the cache load:
  if ( _display_degree > 1 ) {
    _out << "\t# of loaded points : " << static_cast<int>(_cache1.size()) << endl
	 << "\tsize               : ";
    display_sizeof ( size_of() , _out );
    _out << endl
	 << "\tload time          : " << c.get_real_time() << 's' << endl;
  }

  return true;
}

/*---------------------------------------------------------------------*/
/* . load a cache file (fill the set _cache1)                          */
/* . lock the file 'file_name'                                         */
/* . return false if 'file_name' exists and is not a valid cache file  */
/* . return false if 'file_name' is already locked                     */
/* . return false if the object already locked a file                  */
/* . return true if this object is already locked with this file       */
/*   (and just do nothing)                                             */
/* . return true and create a valid cache file if 'file_name'          */
/*   does not exist                                                    */
/* . return false if the previous step did not work                    */
/*                                                                     */
/* . parameter m (default=NULL): to indicate a number of black-box     */
/*   outputs; points in file with a different value are ignored        */
/*---------------------------------------------------------------------*/
bool Cache::load ( const string & file_name , const int * pm ) {

  if ( !file_name.empty() && file_name==_locked_file )
    return true;

  if ( file_name.empty() || !_locked_file.empty() || is_locked(file_name) )
    return false;

  // the file exists:
  if ( NOMAD::check_read_file ( file_name ) ) {

    int      id;
    ifstream in ( file_name.c_str() , ios::binary );

    in.read ( (char *) &id , sizeof(int) );
    
    // it is a valid cache file:
    if ( !in.fail() && id == CACHE_FILE_ID ) {

      // display:
      if ( _display_degree > 1 )
	_out << endl << "loading of \"" << file_name << "\":" << endl;

      // read the points:
      if ( !read_points_from_cache_file ( in , pm ) ) {
	in.close();
	return false;  // it is not a valid cache file
      }

      // lock the file:
      lock(file_name);

      in.close();
      return true;      
    }

    // it is not a valid cache file:
    else {
      in.close();
      return false;
    }  
  }

  // the file does not exist:
  else {

    // display:
    if ( _display_degree > 1 )
      _out << endl << "creating cache file \"" << file_name << "\"" << endl;

    // create the file as a valid cache file:
    ofstream out ( file_name.c_str() , ios::binary );

    if ( out.fail() ) {
      out.close();
      return false;
    }      

    out.write ( (char *) &CACHE_FILE_ID , sizeof(CACHE_FILE_ID) );
    out.close();

    // lock:
    lock ( file_name );
  }

  return true;
}

/*------------------------------------------------------------------*/
/*  if overwrite == false:                                          */
/*         . write points at the end of the locked cache file       */
/*         . write only points from _cache2                         */
/*         . transfer points from _cache2 to _cache3                */
/*  else:                                                           */
/*         . write all points in the locked cache file              */
/*------------------------------------------------------------------*/
bool Cache::save ( bool overwrite ) {
  
  if ( _locked_file.empty() )
    return true;

  // display:
  if ( _display_degree > 1 )
    _out << endl << "saving cache file \"" << _locked_file << "\"" << endl;

  ofstream out;

  if ( overwrite ) {

    // open:
    out.open ( _locked_file.c_str() , ios::binary );
    if ( out.fail() ) {
      out.close();
      return false;
    }

    // cache file tag:
    out.write ( (char *) &CACHE_FILE_ID , sizeof(CACHE_FILE_ID) );

    // save all cache points:
    const Eval_Point * cur = begin();
    while (cur) {
      Cache_File_Point cfp (*cur);
      if ( !cfp.write (out) ) {
 	out.close();
 	return false;
      }
      cur = next();
    }
  }

  else {

    // open and go at the end of the file:
    out.open ( _locked_file.c_str() , ios::binary | ios::app );
    if ( out.fail() ) {
      out.close();
      return false;
    }
    
    set<Cache_Point>::iterator it = _cache2.begin();
    while ( it != _cache2.end() ) {
   
      // write it->get_point() in 'out':
      Cache_File_Point cfp (*it->get_point()); 
      if ( !cfp.write (out) ) {
	out.close();
	return false;
      }

      // transfer the point from _cache2 to _cache3:
      Cache_Point cp ( it->get_point() );
      _cache3.insert ( cp              );
      _cache2.erase  ( it++            );
    }
  }

  // close the file:
  out.close();

  return true;
}

/*------------------------------------------------------------------*/
/*  . update a point already in cache from another point with the   */
/*    same coordinates                                              */
/*  . if both points have a different number of black-box outputs,  */
/*    they are not comparable and we set cache_x = x                */
/*------------------------------------------------------------------*/
void Cache::update ( Eval_Point & cache_x , const Eval_Point & x ) const {

  const Point & bbo_x = x.get_bb_outputs();
  
  if ( &cache_x == &x || !x.is_eval_ok() || bbo_x.size() == 0 || cache_x != x )
    return;

  const Point & bbo_cache_x = cache_x.get_bb_outputs();
  int           m           = bbo_cache_x.size();

  // if the current point could not evaluate and x did,
  // or if the number of bb outputs is different, we set cache_x = x:
  if ( !cache_x.is_eval_ok() || m != bbo_x.size() ) {
    cache_x.set_eval_ok    ( true               );
    cache_x.set_bb_output  ( bbo_x              );
    cache_x.set_signature  ( x.get_signature () );
    cache_x.set_direction  ( x.get_direction () );
    cache_x.set_mesh_index ( x.get_mesh_index() );
    return;
  }
    
  // we complete _bb_outputs:
  int c1 = 0;
  int c2 = 0;
 
  for ( int i = 0 ; i < m ; ++i ) {

    if ( bbo_cache_x[i].is_defined() )
      ++c1;

    if ( bbo_x[i].is_defined() )
      ++c2;

    if ( !bbo_cache_x[i].is_defined() && bbo_x[i].is_defined() )
      cache_x.set_bb_output ( i , bbo_x[i] );
  }
  
  // the two points are 'eval_ok' and have comparable outputs:
  // we select the best as the one with the more defined bb_outputs:
  if ( c2 > c1 ) {
    cache_x.set_signature  ( x.get_signature () );
    cache_x.set_direction  ( x.get_direction () );
    cache_x.set_mesh_index ( x.get_mesh_index() );
  }

}

/*---------------------------------------------------------*/
/*                         display                         */
/*---------------------------------------------------------*/
void Cache::display ( ostream & out ) const {
  out << "\t# of cache points : " << size() << endl;
#ifdef DEBUG
  out << "\tsize in memory    : ";
  display_sizeof ( size_of() , out );
  out << endl;
#endif
  out << "\tcache file        : ";
  if ( _locked_file.empty() )
    out << "-\n";
  else
    out << _locked_file << endl;
  if ( _display_degree > 3 ) {
    out << "\tcache points      :\n";
    const Eval_Point * cur = begin();
    while (cur) {
      out << "\t\t" << *cur << endl;
      cur = next();
    }
  }
}
