// -*- mode: c++ -*-
//
//  Copyright(C) 2009-2013 Taro Watanabe <taro.watanabe@nict.go.jp>
//

#ifndef __UTILS__MAP_FILE__HPP__
#define __UTILS__MAP_FILE__HPP__ 1

#include <stdint.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>

#include <cstdlib>
#include <string>
#include <iostream>
#include <stdexcept>

#include <boost/shared_ptr.hpp>
#include <boost/filesystem.hpp>

#include <utils/config.hpp>
#include <utils/filesystem.hpp>
#include <utils/compress_stream.hpp>

namespace utils
{
  struct __map_file_enum
  {
    typedef enum {
      MAP_FILE_NONE     = 0,
      MAP_FILE_WRITE    = (1 << 0),
      MAP_FILE_POPULATE = (1 << 1), // on Linux, forced "map-failure" at load time..
    } flag_type;    
  };

  class __map_file_impl : public __map_file_enum
  {
  public:
    typedef std::streamoff off_type;
    typedef size_t         size_type;
    typedef char           byte_type;
    
    typedef boost::filesystem::path path_type;
    
  public:
    __map_file_impl(const std::string& file, const flag_type flag=MAP_FILE_NONE)
      : mmapped(), filesize(), filename() { open(file, flag); }
    __map_file_impl(const boost::filesystem::path& file, const flag_type flag=MAP_FILE_NONE)
      : mmapped(), filesize(), filename() { open(file, flag); }
    ~__map_file_impl() { close(); }
    
  public:
    bool is_open() const { return mmapped; }
    
    const void* begin() const { return static_cast<void*>(mmapped); }
    const void* end() const { return static_cast<void*>(mmapped + filesize); }

    void* begin() { return static_cast<void*>(mmapped); }
    void* end() { return static_cast<void*>(mmapped + filesize); }
    
    off_type size() const { return filesize; }
    const boost::filesystem::path& path() const { return filename; }
  
    void populate()
    {
      if (! mmapped || filesize <= 0) return;
      
      const off_type page_size  = 4096;
      const off_type block_size = page_size * 64;
      
      char buf[page_size];
      
      const byte_type* last = mmapped + filesize;
      for (const byte_type* first = mmapped; first + block_size < last; first += block_size) {
	const byte_type* begin = first + (random() & (64 - 1)) * page_size;
	
	std::copy(begin, begin + page_size, buf);
      }
    }
      
  private:
    void open(const boost::filesystem::path& file, const flag_type flag=MAP_FILE_NONE)
    {
      filename = file;
      filesize = static_cast<off_type>(boost::filesystem::file_size(file));
      modifiable = (flag & MAP_FILE_WRITE);

      if (! filesize) return;

      const bool writable = (flag & MAP_FILE_WRITE);
      
#if BOOST_FILESYSTEM_VERSION == 2
      int fd = ::open(file.file_string().c_str(), (writable ? O_RDWR | O_NDELAY : O_RDONLY | O_NDELAY));
      if (fd < 0)
	fd = ::open(file.file_string().c_str(), (writable ? O_RDWR : O_RDONLY));
#else
      int fd = ::open(file.string().c_str(), (writable ? O_RDWR | O_NDELAY : O_RDONLY | O_NDELAY));
      if (fd < 0)
	fd = ::open(file.string().c_str(), (writable ? O_RDWR : O_RDONLY));
#endif

      if (fd < 0)
	throw std::runtime_error("map_file::open() open()");
      
      int mmap_flag = MAP_SHARED;
#ifdef MAP_POPULATE
      if (flag & MAP_FILE_POPULATE)
	mmap_flag |= MAP_POPULATE;
#endif
      
      // First, try map_shared
      byte_type* x = static_cast<byte_type*>(::mmap(0, filesize, writable ? PROT_WRITE : PROT_READ, mmap_flag, fd, 0));

      // Second, try map_private
      if (! (x + 1)) {
	mmap_flag = MAP_PRIVATE;
#ifdef MAP_POPULATE
	if (flag & MAP_FILE_POPULATE)
	  mmap_flag |= MAP_POPULATE;
#endif
	x = static_cast<byte_type*>(::mmap(0, filesize, writable ? PROT_WRITE : PROT_READ, mmap_flag, fd, 0));
      }

      // no need to keep file-descriptor
      ::close(fd);
      
      // If successful, use mmap. Otherwise pread|seek/read access...
      if (x + 1)
	mmapped = x;
      else
	throw std::runtime_error(std::string("map_file::open() mmap()") + strerror(errno));
    }

    void close()
    {
      if (mmapped) {
	if (modifiable)
	  ::msync(mmapped, filesize, MS_SYNC);
	
	::munmap(mmapped, filesize);
      }
      
      mmapped = 0;
      filesize = 0;
      filename = std::string();
      
      modifiable = false;
    }

  private:
    byte_type* mmapped;
    
    off_type filesize;
    
    boost::filesystem::path filename;

    bool modifiable;
  };
  
  template <typename _Tp, typename _Alloc=std::allocator<_Tp> >
  class map_file : public __map_file_enum
  {
  private:
    typedef __map_file_impl impl_type;
    
  public:
    typedef _Tp              value_type;
    typedef const _Tp*       const_iterator;
    typedef const_iterator   iterator;
    typedef const _Tp&       const_reference;
    typedef const_reference  reference;

    typedef impl_type::off_type  off_type;
    typedef impl_type::size_type size_type;
    typedef impl_type::byte_type byte_type;
    typedef impl_type::path_type path_type;

  public:
    // we will allow copying, but actually, this will not be copied...
    
    map_file(const path_type& file, const flag_type flag=MAP_FILE_NONE) : pimpl(new impl_type(file, flag)) { }
    map_file() {}
    
  public:
    const_reference front() const { return *(begin()); }
    const_reference back() const { return *(end() - 1); }
    const_reference operator[](size_type pos) const { return *(begin() + pos); }
    
    const_iterator begin() const { return static_cast<const_iterator>(pimpl->begin()); }
    const_iterator end()   const { return static_cast<const_iterator>(pimpl->end()); }

    iterator begin() { return static_cast<iterator>(pimpl->begin()); }
    iterator end()   { return static_cast<iterator>(pimpl->end()); }
    
    bool is_open() const { return pimpl && pimpl->is_open(); }
    bool empty() const { return ! is_open() || size() == 0; }
    size_type size() const { return static_cast<size_type>(pimpl->size() / sizeof(value_type)); }
    off_type file_size() const { return pimpl->size(); }

    uint64_t size_bytes() const { return file_size(); }
    uint64_t size_compressed() const { return file_size(); }
    uint64_t size_cache() const { return 0; }
    
    path_type path() const { return pimpl->path(); }

    static bool exists(const path_type& path)
    {
      return boost::filesystem::exists(path);
    }
    
    void open(const std::string& file, const flag_type flag=MAP_FILE_NONE) { pimpl.reset(new impl_type(file, flag)); }
    void open(const path_type& file, const flag_type flag=MAP_FILE_NONE) { pimpl.reset(new impl_type(file, flag)); }
    
    void write(const path_type& file) const
    {
      if (file == path()) return;
      utils::filesystem::copy_file(path(), file);
    }
    
    void close() { pimpl.reset(); }
    void clear() { close(); }
    
    void swap(map_file& x)
    {
      pimpl.swap(x.pimpl);
    }
    
    void populate()
    {
      if (pimpl)
	pimpl->populate();
    }

  private:
    boost::shared_ptr<impl_type> pimpl;
  };

  template <typename T, typename A>
  inline
  bool operator==(const map_file<T,A>& x, const map_file<T,A>& y)
  {
    return ((! x.is_open() && ! y.is_open())
	    || (x.is_open() && y.is_open()
		&& (x.path() == y.path()
		    || (x.size() == y.size() && std::equal(x.begin(), x.end(), y.begin())))));
  }

  template <typename T, typename A>
  inline
  bool operator!=(const map_file<T,A>& x, const map_file<T,A>& y)
  {
    return !(x == y);
  }

};

namespace std
{
  template <typename T, typename A>
  inline
  void swap(utils::map_file<T,A>& x, utils::map_file<T,A>& y)
  {
    x.swap(y);
  }
};

#endif
