/* Copyright (c) 1993 by Sanjay Ghemawat */
/*
 * Calendar Operations
 *
 * <name> delete
 *
 * <name> main				-- main calendar file name
 * <name> include <file>		-- include calendar
 * <name> exclude <file>		-- exclude calendar
 * <name> forincludes <var> {...}	-- yield included calendar names
 *
 * <name> foritems <var> {...}		-- yield item handles
 * <name> add <item> [<calendar-name>]	-- add item to cal (default is main)
 * <name> remove <item>			-- remove item
 * <name> changed <item>		-- mark <item> as changed
 *
 * <name> readonly [<calendar-name>]	-- returns boolean (default main cal)
 *
 * <name> dirty  [<file>]		-- <file> modified locally?
 * <name> stale  [<file>]		-- <file> modified by somebody else?
 * <name> save   [<file>]		-- save changes
 * <name> reread [<file>]		-- read calendars from disk
 *
 * <name> query <start> <finish> <itemvar> <datevar> <body>
 *	For any item occurrence within the range of dates specified
 *	by <start> and <finish>, set <itemvar> to the item handle,
 *	set <datevar> to the occurrence date, and then evaluate <body>.
 *
 * <name> listing <start> <finish> <itemvar> <datevar> <body>
 *	For all items <i>,
 *	    For each occurrence of <i> in start..finish+[<i> earlywarning]
 *		Set <itemvar> to <i>, <datevar> to the occurrence date
 *		and execute <body>.
 *	The executions of <body> are sorted by occurrence.
 *
 * <name> incalendar <calendar> <itemvar> <body>
 *	For all items <i> in <calendar>
 *	    Set <itemvar> to <i> and execute <body>.
 *	The executions of <body> are sorted by date of first occurrence.
 */

#include <string.h>

#include "cal_tcl.h"
#include "collect.h"
#include "dispatch.h"
#include "item_tcl.h"
#include "misc_tcl.h"

#include "Array.h"
#include "calendar.h"
#include "calfile.h"

implementArray(FileList,CalFile*)

static char message_buffer[1000];

Calendar_Tcl* calendar_instance = 0;

Calendar_Tcl::Calendar_Tcl(Tcl_Interp* tcl, char const* h, char const* f)
    : Object(tcl, "Calendar", h)
{
    main = new CalFile(0, f);
    includes = new FileList;

    /* Initialize error buffer */
    msg = new charArray(1000);
    clear_error();

    if (! main->Read()) {
	add_error(main->GetName(), CalFile::LastError());
	return;
    }
    add_item_handles(main->GetCalendar());

    /* Read included calendars */
    for (int i = 0; i < main->GetCalendar()->NumIncludes(); i++) {
	CalFile* file = new CalFile(0, main->GetCalendar()->GetInclude(i));
	includes->append(file);

	if (! file->Read()) {
	    add_error(file->GetName(), CalFile::LastError());
	}
	add_item_handles(file->GetCalendar());
    }

    calendar_instance = this;
}

Calendar_Tcl::~Calendar_Tcl() {
    for (int i = 0; i < includes->size(); i++) {
	CalFile* file = includes->slot(i);
	remove_item_handles(file->GetCalendar());
	delete file;
    }
    remove_item_handles(main->GetCalendar());

    delete includes;
    delete main;
    delete msg;
}

char const* Calendar_Tcl::container(Item_Tcl* item) {
    Calendar* cal = item->value()->GetCalendar();

    if (main->GetCalendar() == cal) {
	return main->GetName();
    }

    for (int i = 0; i < includes->size(); i++) {
	CalFile* file = includes->slot(i);
	if (file->GetCalendar() == cal) {
	    return file->GetName();
	}
    }

    return 0;
}

void Calendar_Tcl::add_item_handles(Calendar* cal) {
    int count = cal->Size();
    for (int i = 0; i < count; i++) {
	new Item_Tcl(tcl(), cal->Get(i));
    }
}

void Calendar_Tcl::remove_item_handles(Calendar* cal) {
    int count = cal->Size();
    for (int i = 0; i < count; i++) {
	Item_Tcl* item = Item_Tcl::find(cal->Get(i));
	if (item != 0) {
	    delete item;
	}
    }
}

CalFile* Calendar_Tcl::find_calendar(char const* name) {
    if (name == 0) {
	return main;
    }

    if (strcmp(name, main->GetName()) == 0) {
	return main;
    }

    for (int i = 0; i < includes->size(); i++) {
	/* Extra check for find_calendar use in fix_includes */
	if (includes->slot(i) == 0) {
	    continue;
	}

	if (strcmp(includes->slot(i)->GetName(), name) == 0) {
	    return includes->slot(i);
	}
    }
    return 0;
}

void Calendar_Tcl::fix_includes() {
    /* Fixup include list */
    FileList* new_includes = new FileList;

    for (int i = 0; i < main->GetCalendar()->NumIncludes(); i++) {
	char const* name = main->GetCalendar()->GetInclude(i);

	/* Try to reuse file from old include list */
	CalFile* file = find_calendar(name);
	if (file == 0) {
	    /* Create new file */
	    file = new CalFile(0, name);
	    if (! file->Read()) {
		add_error(file->GetName(), CalFile::LastError());
	    }
	    add_item_handles(file->GetCalendar());
	}
	else {
	    /* Reuse old calendar - need to mark old include list */
	    for (int j = 0; j < includes->size(); j++) {
		if (includes->slot(j) == file) {
		    includes->slot(j) = 0;
		}
	    }
	}

	new_includes->append(file);
    }

    /* Remove old includes that were not reused */
    for (i = 0; i < includes->size(); i++) {
	CalFile* file = includes->slot(i);
	if (file == 0) {
	    /* Still included in main calendar */
	    continue;
	}

	/* Get rid of it */
	if (file->IsModified()) {
	    if (! file->Write()) {
		add_error(file->GetName(), CalFile::LastError());
	    }
	}

	remove_item_handles(file->GetCalendar());
	delete file;
    }

    delete includes;
    includes = new_includes;
}

void Calendar_Tcl::clear_error() {
    had_error = 0;
    msg->clear();
    msg->append('\0');
}

void Calendar_Tcl::add_error(char const* t1, char const* t2) {
    had_error = 1;

    /* Remove trailing null */
    msg->remove();

    msg->concat(t1, strlen(t1));
    msg->concat(": ", 2);
    msg->concat(t2, strlen(t2));
    msg->append('\n');
    msg->append('\0');
}

/*
 * Forward declaration of handler procedures.
 */
static int cal_delete	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_main	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_include	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_exclude	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_forincs	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_foritems	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_add	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_remove	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_changed	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_ronly	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_dirty	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_stale	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_save	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_reread	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_query	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_listing	(ClientData, Tcl_Interp*, int, char*[]);
static int cal_incal	(ClientData, Tcl_Interp*, int, char*[]);

static Dispatch_Entry calendar_dispatch[] = {
    { "delete",		0, 0, cal_delete	},
    { "main",		0, 0, cal_main		},
    { "include",	1, 1, cal_include	},
    { "exclude",	1, 1, cal_exclude	},
    { "forincludes",	2, 2, cal_forincs	},
    { "foritems",	2, 2, cal_foritems	},
    { "add",		1, 2, cal_add		},
    { "remove",		1, 1, cal_remove	},
    { "changed",	1, 1, cal_changed	},
    { "readonly",	0, 1, cal_ronly		},
    { "dirty",		0, 1, cal_dirty		},
    { "stale",		0, 1, cal_stale		},
    { "save",		0, 1, cal_save		},
    { "reread",		0, 1, cal_reread	},
    { "query",		5, 5, cal_query		},
    { "listing",	5, 5, cal_listing	},
    { "incalendar",	3, 3, cal_incal		},
    { 0,		0, 0, 0			}
};


int Calendar_Tcl::method(int argc, char* argv[]) {
    return Dispatch(calendar_dispatch, (ClientData)this, tcl(), argc, argv);
}

static int cal_delete(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;
    delete cal;

    TCL_Return(tcl, "");
}

static int cal_main(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;
    TCL_Return(tcl, (char*) cal->main->GetName());
}

static int cal_include(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    if (cal->main->GetCalendar()->ReadOnly()) {
	TCL_Error(tcl, "calendar is readonly");
    }

    CalFile* newFile = new CalFile(0, argv[0]);
    if (! newFile->Read()) {
	TCL_Error(tcl, (char*) CalFile::LastError());
    }
    cal->add_item_handles(newFile->GetCalendar());

    cal->main->GetCalendar()->Include(argv[0]);
    cal->includes->append(newFile);
    cal->main->Modified();

    TCL_Return(tcl, "");
}

static int cal_exclude(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    if (cal->main->GetCalendar()->ReadOnly()) {
	TCL_Error(tcl, "calendar is readonly");
    }

    for (int i = 0; i < cal->includes->size(); i++) {
	CalFile* f = cal->includes->slot(i);
	if (strcmp(f->GetName(), argv[0]) == 0) {
	    /* Remove it */
	    cal->remove_item_handles(f->GetCalendar());
	    delete f;

	    cal->main->GetCalendar()->Exclude(argv[0]);
	    for (int j = i+1; j < cal->includes->size(); j++) {
		cal->includes->slot(j-1) = cal->includes->slot(j);
	    }
	    cal->includes->remove();
	    cal->main->Modified();

	    TCL_Return(tcl, "");
	}
    }

    TCL_Error(tcl, "no such calendar");
}

static int cal_forincs(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    char* var = argv[0];
    char* body = argv[1];
    for (int i = 0; i < cal->includes->size(); i++) {
	if (Tcl_SetVar(tcl, var, (char*) (cal->includes->slot(i)->GetName()),
		       0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	int result = Tcl_Eval(tcl, body, 0, NULL);
	if (result == TCL_BREAK) break;
	if ((result == TCL_OK) || (result == TCL_CONTINUE)) continue;
	return result;
    }

    TCL_Return(tcl, "");
}

static int cal_foritems(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    char* var = argv[0];
    char* body = argv[1];

    for (int i = 0; i <= cal->includes->size(); i++) {
	Calendar* calendar;
	if (i >= cal->includes->size()) {
	    calendar = cal->main->GetCalendar();
	}
	else {
	    calendar = cal->includes->slot(i)->GetCalendar();
	}

	for (int j = 0; j < calendar->Size(); j++) {
	    Item_Tcl* item = Item_Tcl::find(calendar->Get(j));
	    if (item == 0) {
		TCL_Error(tcl, "unknown item in calendar");
	    }

	    if (Tcl_SetVar(tcl, var, (char*) item->handle(), 0) == NULL) {
		TCL_Error(tcl, "could not set loop variable");
	    }

	    int result = Tcl_Eval(tcl, body, 0, NULL);
	    switch (result) {
	      case TCL_BREAK:
		TCL_Return(tcl, "");
	      case TCL_OK:
		continue;
	      case TCL_CONTINUE:
		continue;
	      default:
		return result;
	    }
	}
    }

    TCL_Return(tcl, "");
}

static int cal_add(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    Object* obj = (Item_Tcl*) Object::find(argv[0]);
    if ((obj == 0) || (strcmp(obj->type(), "Item") != 0)) {
	TCL_Error(tcl, "no such item");
    }
    Item_Tcl* item = (Item_Tcl*) obj;

    CalFile* file = cal->find_calendar(argv[1]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    if (file->GetCalendar()->ReadOnly()) {
	TCL_Error(tcl, "calendar is readonly");
    }

    file->GetCalendar()->Add(item->value());
    file->Modified();

    TCL_Return(tcl, "");
}

static int cal_remove(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    Object* obj = (Item_Tcl*) Object::find(argv[0]);
    if ((obj == 0) || (strcmp(obj->type(), "Item") != 0)) {
	TCL_Error(tcl, "no such item");
    }
    Item_Tcl* item = (Item_Tcl*) obj;

    Calendar* calendar = item->value()->GetCalendar();
    CalFile* file = 0;
    if (cal->main->GetCalendar() == calendar) {
	file = cal->main;
    }
    else {
	for (int i = 0; i < cal->includes->size(); i++) {
	    if (cal->includes->slot(i)->GetCalendar() == calendar) {
		file = cal->includes->slot(i);
		break;
	    }
	}
    }

    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    if (file->GetCalendar()->ReadOnly()) {
	TCL_Error(tcl, "calendar is readonly");
    }

    file->GetCalendar()->Remove(item->value());
    file->Modified();
    delete item;

    TCL_Return(tcl, "");
}

static int cal_changed(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    Object* obj = (Item_Tcl*) Object::find(argv[0]);
    if ((obj == 0) || (strcmp(obj->type(), "Item") != 0)) {
	TCL_Error(tcl, "no such item");
    }
    Item_Tcl* item = (Item_Tcl*) obj;

    Calendar* calendar = item->value()->GetCalendar();
    CalFile* file = 0;
    if (cal->main->GetCalendar() == calendar) {
	file = cal->main;
    }
    else {
	for (int i = 0; i < cal->includes->size(); i++) {
	    if (cal->includes->slot(i)->GetCalendar() == calendar) {
		file = cal->includes->slot(i);
		break;
	    }
	}
    }

    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    if (file->GetCalendar()->ReadOnly()) {
	TCL_Error(tcl, "calendar is readonly");
    }

    file->Modified();

    TCL_Return(tcl, "");
}

static int cal_ronly(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    TCL_Return(tcl, file->GetCalendar()->ReadOnly() ? "1" : "0");
}

static int cal_dirty(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    TCL_Return(tcl, (file->IsModified() ? "1" : "0"));
}

static int cal_stale(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    TCL_Return(tcl, (file->FileHasChanged() ? "1" : "0"));
}

static int cal_save(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    if (! file->Write()) {
	TCL_Error(tcl, (char*) CalFile::LastError());
    }
    TCL_Return(tcl, "");
}

static int cal_reread(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }

    Calendar* old = file->ReRead();
    if (old == 0) {
	TCL_Error(tcl, (char*) CalFile::LastError());
    }

    /* Fixup items */
    cal->remove_item_handles(old);
    cal->add_item_handles(file->GetCalendar());
    delete old;

    if (file == cal->main) {
	cal->clear_error();
	cal->fix_includes();
	/* XXX Report error??? */
    }

    TCL_Return(tcl, "");
}

static int cal_query(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    int startDays;
    if (Tcl_GetInt(tcl, argv[0], &startDays) != TCL_OK) {
	TCL_Error(tcl, "illegal start date");
    }
    Date start(startDays);

    int finishDays;
    if (Tcl_GetInt(tcl, argv[1], &finishDays) != TCL_OK) {
	TCL_Error(tcl, "illegal finish date");
    }
    Date finish(finishDays);

    char* ivar = argv[2];
    char* dvar = argv[3];
    char* body = argv[4];

    Occurrences list;
    collect_occurrences(list, start, finish, 0);

    for (int i = 0; i < list.size(); i++) {
	/* Yield occurrence */
	if (Tcl_SetVar(tcl, ivar, (char*) list[i].item->handle(), 0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	char buffer[20];
	sprintf(buffer, "%d", list[i].date.EpochDays());
	if (Tcl_SetVar(tcl, dvar, buffer, 0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	int result = Tcl_Eval(tcl, body, 0, NULL);
	switch (result) {
	  case TCL_BREAK:
	    TCL_Return(tcl, "");
	  case TCL_OK:
	    continue;
	  case TCL_CONTINUE:
	    continue;
	  default:
	    return result;
	}
    }

    TCL_Return(tcl, "");
}

static int cal_listing(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    int startDays;
    if (Tcl_GetInt(tcl, argv[0], &startDays) != TCL_OK) {
	TCL_Error(tcl, "illegal start date");
    }
    Date start(startDays);

    int finishDays;
    if (Tcl_GetInt(tcl, argv[1], &finishDays) != TCL_OK) {
	TCL_Error(tcl, "illegal finish date");
    }
    Date finish(finishDays);

    Occurrences list;
    collect_occurrences(list, start, finish, 1);
    sort_occurrences(list);

    /* Yield */

    char* ivar = argv[2];
    char* dvar = argv[3];
    char* body = argv[4];

    for (int i = 0; i < list.size(); i++) {
	if (Tcl_SetVar(tcl, ivar, (char*) list[i].item->handle(), 0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	char buffer[20];
	sprintf(buffer, "%d", list[i].date.EpochDays());
	if (Tcl_SetVar(tcl, dvar, buffer, 0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	int result = Tcl_Eval(tcl, body, 0, NULL);
	switch (result) {
	  case TCL_BREAK:
	    TCL_Return(tcl, "");
	  case TCL_OK:
	    continue;
	  case TCL_CONTINUE:
	    continue;
	  default:
	    return result;
	}
    }

    TCL_Return(tcl, "");
}

static int cal_incal(ClientData c, Tcl_Interp* tcl, int argc, char* argv[]){
    Calendar_Tcl* cal = (Calendar_Tcl*) c;

    CalFile* file = cal->find_calendar(argv[0]);
    if (file == 0) {
	TCL_Error(tcl, "no such calendar");
    }
    Calendar* calendar = file->GetCalendar();

    /* Collect items */
    Occurrences list;
    for (int i = 0; i < calendar->Size(); i++) {
	Item_Tcl* item = Item_Tcl::find(calendar->Get(i));
	if (item == 0) {
	    TCL_Error(tcl, "unknown item");
	}

	Date d;
	if (item->value()->Dates()->first(d)) {
	    Occurrence o;
	    o.item = item;
	    o.date = d;
	    list.append(o);
	}
    }

    sort_occurrences(list);

    /* Yield */

    char* ivar = argv[1];
    char* body = argv[2];

    for (i = 0; i < list.size(); i++) {
	if (Tcl_SetVar(tcl, ivar, (char*) list[i].item->handle(), 0) == NULL) {
	    TCL_Error(tcl, "could not set loop variable");
	}

	int result = Tcl_Eval(tcl, body, 0, NULL);
	switch (result) {
	  case TCL_BREAK:
	    TCL_Return(tcl, "");
	  case TCL_OK:
	    continue;
	  case TCL_CONTINUE:
	    continue;
	  default:
	    return result;
	}
    }

    TCL_Return(tcl, "");
}
