#include <cstdlib>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <cstring>
#include <string>
#include <cerrno>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "Log.h"

#define APPEND_FLAG ios_base::app
#define MKDIR(x, y)	mkdir(x, y)
#define STATERR	ENOENT

const string CUR_RUN_FILE(".Log_cur_run");
const string CUR_DATA_FILE(".Log_cur_file");
const string ACTIVE_FILE(".Log_active");
const string COMMENT_FILE("Log_comments");


// try_opening_active_file() - Make sure no other Log instance is
// using this datadir by checking for the existence of the active file.
// It will throw an exception if another Log is using it. The active
// file resides in the datadir and contains the pid of the process that
// is using it.
static void
try_opening_active_file(const string& datadir, const string& active_file) throw (string)
{
	int fd = open(active_file.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0644);

	if (fd == -1) {
		// active file already exists, meaning that some other Log
		// instance is using the same datadir. Read the pid from the
		// active file and report it.
		ifstream af(active_file.c_str());
		string pid;
		if (af) {
			af >> pid;
			af.close();
		} else {
			pid = "????";
		}
		ostringstream ost;
		ost << "Log: another Log instance (process "
			<< pid << ") appears to be using '" << datadir
		<< "' to save data.";
		throw ost.str();
	}

	// Write our pid into the active file we just opened.
	ostringstream pid;
	pid << getpid() << endl;
	int ret = write(fd, pid.str().c_str(), pid.str().size());
	if (ret != pid.str().size()) {
		ostringstream ost;
		ost << "Log: try_opening_active_file: bad write to '"
			<< active_file << "' (" << strerror(errno) << ")";
		throw ost.str();
	}
	close(fd);
}


Log::Log(string _datadir, uint32_t _maxfilesize, bool _do_default_comment) throw (string)
    : datadir(_datadir), maxfilesize(_maxfilesize), do_default_comment(_do_default_comment)
{
    if (maxfilesize < 0L) {
    	switch_data_files = false;
    	maxfilesize = 0L;
    } else {
    	switch_data_files = true;
    }

    struct stat statbuf;
    if (stat(datadir.c_str(), &statbuf)) {
		if (errno == STATERR) {
			// datadir doesn't exist, try to make it.
			if (mkdir(datadir.c_str(), 0755)) {
				stringstream ost;
				ost << "Log: can't create datadir '" << datadir
					<< "' (" << strerror(errno) << ") ";
				throw ost.str();
			}
		} else {
			// bad stat
			ostringstream ost;
			ost << "Log: bad stat on datadir '" << datadir
				<< "' (" << strerror(errno) << ") ";
			throw ost.str();
		}
    } else {
		// datadir exists; make sure it is a directory
		if (!S_ISDIR(statbuf.st_mode)) {
			ostringstream ost;
			ost << "Log: datadir '" << datadir
				<< "' exists, but is not a directory!";
			throw ost.str();
		}
    }

    // Make sure no other Log instance is using this datadir by
    // checking for the existence of the active_file. It will throw an
    // exception if another Log is using it.
    active_file = datadir + "/" + ACTIVE_FILE;
    try {
    	try_opening_active_file(datadir, active_file);
    } catch (string s) {
    	throw s;
    }

    // need to read saved run file to get run number
    cur_run_name_file = datadir + "/" + CUR_RUN_FILE;
    ifstream f(cur_run_name_file.c_str());
    // if f is bad, then we assume no file; use initial run no. of -1.
    if (f) {
    	f >> run;
		f.close();
    } else {
    	run = -1;
    }

    try {
    	next_run();
    } catch (string s) {
		throw s;
    }

    totbytes = 0L;
    if (do_default_comment) {
    	comment("init");
    }
}

/* next_run - increment the run number; make the new run directory; file
 * number will be set to 0. Throws a string as an exception if there is a
 * problem. */
void
Log::next_run() throw (string)
{
    uint32_t tmprun = run + 1;

    ostringstream runost;
    runost << setw(5) << setfill('0') << tmprun;
    rundir = datadir + "/" + runost.str();

    // Create the new run directory.
    if (MKDIR(rundir.c_str(), 0755)) {
		string s = "Log: next_run: can't mkdir '" + rundir +
			"' (" + strerror(errno) + ")";
		throw s;
    }

    // Write the new run number to the run name file.
    ofstream f(cur_run_name_file.c_str());
    if (f) {
		f << runost.str();
		f.close();
    } else {
		string s = "Log: next_run: can't open '" + cur_run_name_file +
			"' (" + strerror(errno) + ")";
		throw s;
    }

    run++;
    file = -1;

    try {
    	next_file();
    } catch (string s) {
    	throw s;
    }
}

// next_file: close current data file; open next one. Returns new data
// file number if successful, else throws a string as an exception.
uint32_t Log::next_file() throw (string)
{
    if (filestream.is_open()) {
    	filestream.close();
    }

    file++;

    ostringstream fileost;
    fileost << setw(6) << setfill('0') << file;

    string filename = rundir + "/" + fileost.str();
    filestream.open(filename.c_str());
    if (!filestream) {
		string s = "Log: next_file: can't open '" + filename +
			"' (" + strerror(errno) + ")";
		file--;
		throw s;
    }

    // update the current file name file.
    string cur_file_name_file = rundir + "/" + CUR_DATA_FILE;
    ofstream f(cur_file_name_file.c_str());
    if (f) {
		f << fileost.str();
		f.close();
    } else {
		string s = "Log: next_file: can't open '" + cur_file_name_file +
			"' (" + strerror(errno) + ")";
		file--;
		throw s;
    }
    filesize = 0;
    return file;
}

void Log::flush() throw (string)
{
    if (!filestream.flush()) {
		ostringstream ost;
		ost <<  "Log: next_file: error: " <<  strerror(errno);
		throw ost.str();
    }
}

// save_data: write the data to the current file. Throws a string as an
// exception on error, 1 if we started a new data file, else 0.
// dp = pointer to data, amtb = size in bytes of data.
int Log::save_data(const char *dp, size_t amtb) throw (string)
{
    if (amtb == 0) {
    	return 0;
    }

    int retcode = 0;

    if (amtb > maxfilesize) {
    	ostringstream ost;
    	ost << "Log: save_data: amtb (" << amtb << ") too big";
    	throw ost.str();
    }

    if (switch_data_files) {
		if (filesize + amtb > maxfilesize) {
			try {
				next_file();
			} catch (string s) {
				throw s;
			}
			retcode = 1;
		}
    }

    if (!filestream.write(dp, amtb))
    {
		ostringstream ost;
		ost << "Log: save_data: bad write: " << strerror(errno);
		throw ost.str();
    }

    filesize += amtb;
    totbytes += amtb;
    return retcode;
}

void Log::comment(const string& s)
{
    comment(s.c_str());
}

void Log::comment(const char *s)
{
    // Get time.
    time_t t = time(NULL);
    string timestr = ctime(&t);
    timestr[timestr.size()-1] = '\0';	// delete \n

    // Enter comment in the datadir comment file.
    string filename = datadir + "/" + COMMENT_FILE;
    ofstream f(filename.c_str(), APPEND_FLAG);
    f << timestr << ": " << rundir << ": " << s << endl;
    f.close();

    // Enter comment in the run's comment file.
    filename = rundir + "/" + COMMENT_FILE;
    f.open(filename.c_str(), APPEND_FLAG);
    f << timestr << ": " << rundir << ": " << s << endl;
    f.close();
}

void Log::close()
{
    if (do_default_comment) {
    	comment("close");
    }
    unlink(active_file.c_str());
    filestream.close();
}

Log::~Log()
{
    close();
}

uint32_t Log::cur_run() const
{
	return run;
}

uint32_t Log::cur_file() const
{
	return file;
}

uint32_t Log::bytes_cur_file() const
{
	return filesize;
}

uint32_t Log::total_bytes() const
{
	return totbytes;
}

string Log::cur_file_str() const
{
    ostringstream ost;
    ost << setw(6) << setfill('0') << file;
    return string(ost.str());
}

string Log::cur_run_str() const
{
    ostringstream ost;
    ost << setw(5) << setfill('0') << run;
    return string(ost.str());
}

string Log::cur_file_path() const
{
    ostringstream ost;
    ost << rundir << "/" << setw(6) << setfill('0') << file;
    return string(ost.str());
}
