/* Copyright (C) 2014 InfiniDB, Inc.

   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; version 2 of
   the License.

   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, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA. */

#include <time.h>
#include <string>
#include <sys/types.h>
#include <map>
#include "boost/filesystem/operations.hpp"
#include "boost/filesystem/path.hpp"
namespace fs = boost::filesystem;

using namespace std;

#include "unistd.h"
#include "stdint.h"
#include "sys/time.h"
#include "syslog.h"
#include "idbregistry.h"
#include "WinSysLog.h"

// WinSyslog class encapsulates what substitutes for the syslog in the
// InfiniDB Windows version.

// Singleton instance
WinSyslog* WinSyslog::fpSysLog = WinSyslog::instance();
// For archive coordination.
// We have two mutexes. Only one will be used. We want to use the inter-process
// mutex name fhMutex. If creation succeeds, we set fbGoodIPMutex so the code knows
// it's good. If we fail, fMutex (Only this processes threads) will be used - it's
// better than nothing.
boost::mutex WinSyslog::fMutex;  // Failsafe Intra process mutex
HANDLE WinSyslog::fhMutex;       // Interprocess named mutex
bool WinSyslog::fbGoodIPMutex = false;
#define WINSYSLOG_MUTEX_NAME "WinSysLogMutex"  // For the Windows Named Mutex

// Constructor, wherein we initialize everything
WinSyslog::WinSyslog() : fLastArchiveTime(0), fLastArchiveDay(0), fLogLineheaderSize(0)
{
  fLogDirName = IDBreadRegistry("") + "\\log";
  fLogFileName = IDBreadRegistry("") + "\\log\\InfiniDBLog.txt";
  fTimeFileName = IDBreadRegistry("") + "\\log\\InfiniDBLastArchive.dat";

  // Set up a buffer with all the line header except the timestamp.
  // Leave room in the front for the timestamp.
  // This header will approximate the linux standard syslog log header.
  memset(fLogLineHeader, 0, LOG_BUF_SIZE);
  char* pEnd = fLogLineHeader + LOG_BUF_SIZE;
  char* pHeader = fLogLineHeader + TIMESTAMP_SIZE;
  char exePath[LOG_BUF_SIZE];
  DWORD nameSize = (DWORD)(pEnd - pHeader);
  GetComputerName(pHeader, &nameSize);
  pHeader += nameSize;
  *pHeader++ = ' ';
  GetModuleFileName(0, exePath, LOG_BUF_SIZE);
  _splitpath_s(exePath, NULL, 0, NULL, 0, pHeader, pEnd - pHeader, NULL, 0);
  pHeader += strlen(pHeader);
  ;
  *pHeader++ = '[';
  int pid = GetCurrentProcessId();
  snprintf(pHeader, pEnd - pHeader, "%d", pid);
  pHeader += strlen(pHeader);
  ;
  *pHeader++ = ']';
  *pHeader++ = ':';
  fLogLineheaderSize = (int)(pHeader - fLogLineHeader);
  // Set up the date we compare against for archiving.
  // This will either be today (if fTimeFileName doesn't exist)
  // or the the date stored in fTimeFileName.
  time_t now = time(0);
  FILE* lf;
  lf = fopen(fTimeFileName.c_str(), "r+b");

  if (lf == NULL)  // First time archiving.
  {
    fLastArchiveTime = now;
    // Persist the timestamp of last archive.
    lf = fopen(fTimeFileName.c_str(), "w+b");

    if (lf != 0)
    {
      fwrite(&fLastArchiveTime, sizeof(time_t), 1, lf);
      fclose(lf);
    }
  }
  else
  {
    fread(&fLastArchiveTime, sizeof(time_t), 1, lf);
    fclose(lf);
  }

  struct tm lasttm;

  localtime_s(&lasttm, &fLastArchiveTime);

  fLastArchiveDay = lasttm.tm_yday;

  // Create an interprocess mutex to coordinate the archiving function
  fhMutex = CreateMutex(NULL, FALSE, WINSYSLOG_MUTEX_NAME);

  if (fhMutex == NULL)
  {
    syslog(LOG_ERR, "WinSyslog CreateMutex error: %d\n", GetLastError());
  }
  else
  {
    fbGoodIPMutex = true;
  }
}

WinSyslog::~WinSyslog()
{
  CloseHandle(fhMutex);
}

// Log() is called by ::syslog() to actually write stuff to the file
int WinSyslog::Log(int priority, const char* format, va_list& args)
{
  struct tm nowtm;
  FILE* f;
  time_t now = time(0);

  localtime_s(&nowtm, &now);

  // If now isn't the same day as the last archive date, archve.
  if (nowtm.tm_yday != fLastArchiveDay)
  {
    Archive(nowtm);
  }

  // Log the line.
  strftime(fLogLineHeader, TIMESTAMP_SIZE, "%b %d %H:%M:%S", &nowtm);
  fLogLineHeader[TIMESTAMP_SIZE - 1] = ' ';
  f = fopen(fLogFileName.c_str(), "a+");

  if (f == 0)
    return -1;

  fwrite(fLogLineHeader, 1, fLogLineheaderSize, f);
  vfprintf(f, format, args);
  fwrite("\n", 1, 1, f);
  fclose(f);
  return 0;
}

// Archive() is called by Log() when the date changes
// Here we rename our log file to an archive name and delete
// any old files.
void WinSyslog::Archive(const tm& nowtm)
{
  struct tm yesterdaytm;
  bool bArchive = true;
  time_t writeTime;
  char ctimebuf[TIME_BUF_SIZE] = {0};
  DWORD dwWaitResult;
  char* szError = "";

  // Lock it so we don't have two threads archiving.
  if (fbGoodIPMutex)
  {
    // We have a good interprocess mutex. Try for one second to
    // lock it. Abandon trying after a second. Something's stuck.
    dwWaitResult = WaitForSingleObject(fhMutex, 1000);

    switch (dwWaitResult)
    {
      // The thread got ownership of the mutex. Continue on.
      case WAIT_OBJECT_0: break;

      // The thread got ownership of an abandoned mutex, most likely because
      // some process crashed in the middle of archiving. Continue on.
      case WAIT_ABANDONED: break;

      // We timed out. Something's not right. Don't archive.
      case WAIT_TIMEOUT:
        szError = "WinSyslog::Archive WAIT_TIMEOUT";
        bArchive = false;
        break;

      // Horrible failure. Don't archive
      case WAIT_FAILED:
        szError = "WinSyslog::Archive WAIT_FAILED";
        bArchive = false;
        break;
    }
  }
  else
  {
    boost::mutex::scoped_lock lock(fMutex);
  }

  try
  {
    // Check to see if any other thread or process already archived.
    // If the value in fTimeFileName is different than our saved value
    // Then somebody else beat us to it. Don't do it again.
    time_t storedLastArchiveTime;
    FILE* lf;
    lf = fopen(fTimeFileName.c_str(), "r+b");

    if (lf != NULL)
    {
      fread(&storedLastArchiveTime, sizeof(time_t), 1, lf);
      fclose(lf);

      if (storedLastArchiveTime != fLastArchiveTime)
      {
        bArchive = false;
      }
    }
    else
    {
      // If we had something go wrong earlier, and then failed to read the Time file,
      // this should prevent retrying to archive every log line.
      storedLastArchiveTime = fLastArchiveTime = time(NULL);
    }

    if (bArchive == false)
    {
      // Something's not right. Try to set the internals so we don't attempt
      // archiving again.
      fLastArchiveTime = storedLastArchiveTime;
      fLastArchiveDay = nowtm.tm_yday;

      if (szError)
      {
        syslog(LOG_ERR, szError);
      }

      return;
    }

    // Get last archive date (usually yesterday)
    yesterdaytm = nowtm;
    yesterdaytm.tm_mday -= 1;  // May be 0. mktime() adjusts accordingly.
    time_t yesterday = mktime(&yesterdaytm);
    localtime_s(&yesterdaytm, &yesterday);
    string archiveFileName = fLogFileName;
    size_t tail = archiveFileName.find(".txt");
    archiveFileName.erase(tail, 4);
    strftime(ctimebuf, TIME_BUF_SIZE, "-%Y-%m-%d.txt", &yesterdaytm);
    archiveFileName += ctimebuf;
    rename(fLogFileName.c_str(), archiveFileName.c_str());

    // Persist the timestamp of last archive. In case of reboot, we
    // can still archive properly
    lf = fopen(fTimeFileName.c_str(), "w+b");

    if (lf != 0)
    {
      fLastArchiveTime = time(0);
      fwrite(&fLastArchiveTime, sizeof(time_t), 1, lf);
      fclose(lf);
    }

    fLastArchiveDay = nowtm.tm_yday;

    // Get the dates of all archive files.
    fs::path sourceDir(fLogDirName);
    fs::directory_iterator iter(sourceDir);
    fs::directory_iterator end_iter;
    std::multimap<std::time_t, fs::path> fileSet;  // Stays sorted

    while (iter != end_iter)
    {
      fs::path archiveFile = *iter;

      if (fs::is_regular_file(archiveFile))
      {
        if (archiveFile.extension() == ".txt")
        {
          writeTime = fs::last_write_time(archiveFile);
          fileSet.insert(make_pair(writeTime, archiveFile));
        }
      }

      ++iter;
    }

    // Delete anything past the first seven newest files
    typedef std::multimap<std::time_t, fs::path>::iterator PATH_ITER;
    PATH_ITER fileIter = fileSet.begin();
    PATH_ITER fileIterEnd = fileSet.end();
    std::reverse_iterator<PATH_ITER> revEnd(fileIter);
    std::reverse_iterator<PATH_ITER> revIter(fileIterEnd);
    int cnt = 0;

    for (; revIter != revEnd; ++revIter)
    {
      if (cnt++ < 7)
        continue;

      _wunlink(revIter->second.c_str());
    }
  }
  catch (exception& e)
  {
    syslog(LOG_ERR, "WinSyslog::Archive Exception %s", e.what());
  }
  catch (...)
  {
    syslog(LOG_ERR, "WinSyslog::Archive Exception ...");
  }

  if (fbGoodIPMutex)
  {
    if (!ReleaseMutex(fhMutex))
      syslog(LOG_ERR, "WinSyslog::Archive ReleaseMutex failed %d", GetLastError());
  }
}
