/* Copyright (c) 1993 by Sanjay Ghemawat */

#include <sys/file.h>
#include <stdlib.h>

#include <stddef.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include "calfile.h"
#include "calendar.h"
#include "lexer.h"

#include <unistd.h>

extern "C" {
    extern int fsync(int);
}

const char* CalFile::lastError = "no error";

CalFile::CalFile(int ro, const char* name) {
    readOnly = ro;

    int len = strlen(name);
    char* tmp;

    tmp = new char[len+1];
    strcpy(tmp, name);
    fileName = tmp;

    tmp = new char[len+2];
    sprintf(tmp, "%s~", fileName);
    backupName = tmp;

    tmp = new char[len+20];
    sprintf(tmp, "%s%d~", fileName, getpid());
    tmpName = tmp;

    char* lastSlash = strrchr(name, '/');
    if (lastSlash == 0) {
	/* Calendar is in current directory */
	tmp = new char[2];
	strcpy(tmp, ".");
    }
    else {
	int dirlen = lastSlash + 1 - name;
	tmp = new char[dirlen+1];
	strncpy(tmp, name, dirlen);
	tmp[dirlen] = '\0';
    }
    dirName = tmp;

    lastModifyValid = 0;

    calendar = new Calendar;
    calendar->SetReadOnly(readOnly);

    modified = 0;
}

CalFile::~CalFile() {
    delete calendar;
    delete (char*) fileName;
    delete (char*) backupName;
    delete (char*) tmpName;
    delete (char*) dirName;
}

void CalFile::Modified() {
    modified = 1;
}

int CalFile::Write() {
    long mode;

    /*
     * We first write to a non-calendar file, and then rename it
     * it to the calendar file.  This helps make sure that the
     * original calendar file does not get zapped on write-errors.
     *
     * We also try to preserve file modes across writes.
     */

    struct stat buf;
    if (stat(fileName, &buf) < 0) {
	/* Could not get original file mode */
	if (errno == ENOENT) {
	    /* Original file does not even exist - write directly */
	    if (WriteTo(calendar, fileName)) {
		modified = 0;
		lastModifyValid = GetModifyTime(fileName, lastModifyTime);
		return 1;
	    }
	}
	else {
	    lastError = sys_errlist[errno];
	}
	return 0;
    }
    mode = (buf.st_mode & 07777);

    if (! WriteTo(calendar, tmpName)) {
	unlink(tmpName);
	return 0;
    }

    if (chmod(tmpName, mode) < 0) {
	/* Could not set new file mode */
	lastError = sys_errlist[errno];
	unlink(tmpName);
	return 0;
    }

    /*
     * We could conceivably do more sanity checks here.
     */

    /*
     * Create backup file.
     */
    unlink(backupName);
    if (link(fileName, backupName) < 0) {
	/* We could fail here, but that seems too paranoid */
	//lastError = sys_errlist[errno];
	//unlink(tmpName);
	//return 0;
    }

    /*
     * Now rename.
     */
    if (rename(tmpName, fileName) < 0) {
	lastError = sys_errlist[errno];
	unlink(tmpName);
	return 0;
    }

    modified = 0;
    lastModifyValid = GetModifyTime(fileName, lastModifyTime);
    return 1;
}

int CalFile::Read() {
    Time newFileTime;
    int gotTime = GetModifyTime(fileName, newFileTime);

    Calendar* cal = ReadFrom(fileName);
    if (cal != 0) {
        delete calendar;
        calendar = cal;
	calendar->SetReadOnly(readOnly);

	PerformAccessCheck();

	modified = 0;

	lastModifyValid = gotTime;
	if (gotTime) {
	    lastModifyTime = newFileTime;
	}

	return 1;
    }
    return 0;
}

int CalFile::FileHasChanged() {
    PerformAccessCheck();

    Time newModifyTime;
    int newModifyValid = GetModifyTime(fileName, newModifyTime);

    if (newModifyValid != lastModifyValid) {
	/* Ability to read file information changed */
	return 1;
    }

    return (lastModifyValid && (newModifyTime != lastModifyTime));
}

Calendar* CalFile::ReRead() {
    Time newFileTime;
    int gotTime = GetModifyTime(fileName, newFileTime);

    Calendar* cal = ReadFrom(fileName);
    Calendar* old = 0;
    if (cal != 0) {
	old = calendar;
	calendar = cal;
	calendar->SetReadOnly(readOnly);

	PerformAccessCheck();

	modified = 0;

	lastModifyValid = gotTime;
	if (gotTime) {
	    lastModifyTime = newFileTime;
	}
    }
    return old;
}
	    

Calendar* CalFile::ReadFrom(const char* name, int topLevel) {
    Calendar* cal = new Calendar;
    Lexer input(name);

    if (! cal->Read(&input)) {
	lastError = input.LastError();
	delete cal;
	cal = 0;
    }

    return cal;
}

int CalFile::WriteTo(Calendar* cal, const char* name) {
    FILE* output = fopen(name, "w");
    if (! output) {
	lastError = "could not open file for writing calendar";
        return 0;
    }

    cal->Write(output);
    fflush(output);
    if (ferror(output) || (fsync(fileno(output)) < 0)) {
	lastError = "error writing calendar file";
        fclose(output);
        return 0;
    }

    fclose(output);
    return 1;
}

int CalFile::GetModifyTime(char const* file, Time& t) {
    struct stat buf;

    int ret = stat(file, &buf);
    if (ret < 0) return 0;

    struct timeval tv;
    tv.tv_sec  = buf.st_mtime;
    tv.tv_usec = 0;

    t = Time(tv);
    return 1;
}

void CalFile::PerformAccessCheck() {
    calendar->SetReadOnly(readOnly);

    /* Check for permission to write in parent directory */
    if (access(dirName, W_OK) < 0) {
	calendar->SetReadOnly(1);
    }

    if (access(fileName, W_OK) < 0) {
	switch (errno) {
	  case ENOENT:
	    /* File does not exist */
	    break;
	  case EACCES:
	  case EROFS:
	  case ETXTBSY:
	    /* Permission denied */
	    calendar->SetReadOnly(1);
	    break;
	  default:
	    /* Should not happen if we were successfuly able to read cal */
	    break;
	}
    }
}
