/*
 * Copyright (C) 2012-2013 Robin Haberkorn
 *
 * 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, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see .
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "sciteco.h"
#include "interface.h"
#include "undo.h"
#include "parser.h"
#include "expressions.h"
#include "ring.h"
#ifdef HAVE_WINDOWS_H
/* here it shouldn't cause conflicts with other headers */
#include 
/* still need to clean up */
#ifdef interface
#undef interface
#endif
#endif
namespace States {
	StateEditFile	editfile;
	StateSaveFile	savefile;
}
#ifdef G_OS_WIN32
typedef DWORD FileAttributes;
/* INVALID_FILE_ATTRIBUTES already defined */
static inline FileAttributes
get_file_attributes(const gchar *filename)
{
	return GetFileAttributes((LPCTSTR)filename);
}
static inline void
set_file_attributes(const gchar *filename, FileAttributes attrs)
{
	SetFileAttributes((LPCTSTR)filename, attrs);
}
#else
typedef int FileAttributes;
#define INVALID_FILE_ATTRIBUTES (-1)
static inline FileAttributes
get_file_attributes(const gchar *filename)
{
	struct stat buf;
	return g_stat(filename, &buf) ? INVALID_FILE_ATTRIBUTES : buf.st_mode;
}
static inline void
set_file_attributes(const gchar *filename, FileAttributes attrs)
{
	g_chmod(filename, attrs);
}
#endif /* !G_OS_WIN32 */
void
Buffer::UndoTokenClose::run(void)
{
	ring.close(buffer);
	/* NOTE: the buffer is NOT deleted on Token destruction */
	delete buffer;
}
/*
 * The following simple implementation of file reading is actually the
 * most efficient and useful in the common case of editing small files,
 * since
 * a) it works with minimal number of syscalls and
 * b) small files cause little temporary memory overhead.
 * Reading large files however could be very inefficient since the file
 * must first be read into memory and then copied in-memory. Also it could
 * result in thrashing.
 * Alternatively we could iteratively read into a smaller buffer trading
 * in speed against (temporary) memory consumption.
 * The best way to do it could be memory mapping the file as we could
 * let Scintilla copy from the file's virtual memory directly.
 * Unfortunately since every page of the mapped file is
 * only touched once by Scintilla TLB caching is useless and the TLB is
 * effectively thrashed with entries of the mapped file.
 * This results in the doubling of page faults and weighs out the other
 * advantages of memory mapping (has been benchmarked).
 *
 * So in the future, the following approach could be implemented:
 * 1.) On Unix/Posix, mmap() one page at a time, hopefully preventing
 *     TLB thrashing.
 * 2.) On other platforms read into and copy from a statically sized buffer
 *     (perhaps page-sized)
 */
bool
Buffer::load(const gchar *filename)
{
	gchar *contents;
	gsize size;
	if (!g_file_get_contents(filename, &contents, &size, NULL))
		return false;
	edit();
	interface.ssm(SCI_BEGINUNDOACTION);
	interface.ssm(SCI_CLEARALL);
	interface.ssm(SCI_APPENDTEXT, size, (sptr_t)contents);
	interface.ssm(SCI_ENDUNDOACTION);
	g_free(contents);
	/* NOTE: currently buffer cannot be dirty */
#if 0
	interface.undo_info_update(this);
	undo.push_var(dirty);
	dirty = false;
#endif
	set_filename(filename);
	return true;
}
void
Ring::UndoTokenEdit::run(void)
{
	/*
	 * assumes that buffer still has correct prev/next
	 * pointers
	 */
	if (buffer->next())
		TAILQ_INSERT_BEFORE(buffer->next(), buffer, buffers);
	else
		TAILQ_INSERT_TAIL(&ring->head, buffer, buffers);
	ring->current = buffer;
	buffer->edit();
	buffer = NULL;
}
Buffer *
Ring::find(const gchar *filename)
{
	gchar *resolved = get_absolute_path(filename);
	Buffer *cur;
	TAILQ_FOREACH(cur, &head, buffers)
		if (!g_strcmp0(cur->filename, resolved))
			break;
	g_free(resolved);
	return cur;
}
Buffer *
Ring::find(tecoInt id)
{
	Buffer *cur;
	TAILQ_FOREACH(cur, &head, buffers)
		if (!--id)
			break;
	return cur;
}
void
Ring::dirtify(void)
{
	if (!current || current->dirty)
		return;
	interface.undo_info_update(current);
	undo.push_var(current->dirty);
	current->dirty = true;
	interface.info_update(current);
}
bool
Ring::is_any_dirty(void)
{
	Buffer *cur;
	TAILQ_FOREACH(cur, &head, buffers)
		if (cur->dirty)
			return true;
	return false;
}
bool
Ring::edit(tecoInt id)
{
	Buffer *buffer = find(id);
	if (!buffer)
		return false;
	current_doc_update();
	QRegisters::current = NULL;
	current = buffer;
	buffer->edit();
	QRegisters::hook(QRegisters::HOOK_EDIT);
	return true;
}
void
Ring::edit(const gchar *filename)
{
	Buffer *buffer = find(filename);
	current_doc_update();
	QRegisters::current = NULL;
	if (buffer) {
		current = buffer;
		buffer->edit();
		QRegisters::hook(QRegisters::HOOK_EDIT);
	} else {
		buffer = new Buffer();
		TAILQ_INSERT_TAIL(&head, buffer, buffers);
		current = buffer;
		undo_close();
		if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) {
			buffer->load(filename);
			interface.msg(Interface::MSG_INFO,
				      "Added file \"%s\" to ring", filename);
		} else {
			buffer->edit();
			buffer->set_filename(filename);
			if (filename)
				interface.msg(Interface::MSG_INFO,
					      "Added new file \"%s\" to ring",
					      filename);
			else
				interface.msg(Interface::MSG_INFO,
					      "Added new unnamed file to ring.");
		}
		QRegisters::hook(QRegisters::HOOK_ADD);
	}
}
#if 0
/*
 * TODO: on UNIX it may be better to open() the current file, unlink() it
 * and keep the file descriptor in the UndoToken.
 * When the operation is undone, the file descriptor's contents are written to
 * the file (which should be efficient enough because it is written to the same
 * filesystem). This way we could avoid messing around with save point files.
 */
#else
class UndoTokenRestoreSavePoint : public UndoToken {
	gchar	*savepoint;
	Buffer	*buffer;
public:
#ifdef G_OS_WIN32
	FileAttributes orig_attrs;
#endif
	UndoTokenRestoreSavePoint(gchar *_savepoint, Buffer *_buffer)
				 : savepoint(_savepoint), buffer(_buffer) {}
	~UndoTokenRestoreSavePoint()
	{
		if (savepoint)
			g_unlink(savepoint);
		g_free(savepoint);
		buffer->savepoint_id--;
	}
	void
	run(void)
	{
		if (!g_rename(savepoint, buffer->filename)) {
			g_free(savepoint);
			savepoint = NULL;
#ifdef G_OS_WIN32
			if (orig_attrs != INVALID_FILE_ATTRIBUTES)
				set_file_attributes(buffer->filename,
						    orig_attrs);
#endif
		} else {
			interface.msg(Interface::MSG_WARNING,
				      "Unable to restore save point file \"%s\"",
				      savepoint);
		}
	}
};
static inline FileAttributes
make_savepoint(Buffer *buffer)
{
	gchar *dirname, *basename, *savepoint;
	gchar savepoint_basename[FILENAME_MAX];
	FileAttributes attributes = get_file_attributes(buffer->filename);
	basename = g_path_get_basename(buffer->filename);
	g_snprintf(savepoint_basename, sizeof(savepoint_basename),
		   ".teco-%s-%d", basename, buffer->savepoint_id);
	g_free(basename);
	dirname = g_path_get_dirname(buffer->filename);
	savepoint = g_build_filename(dirname, savepoint_basename, NIL);
	g_free(dirname);
	if (!g_rename(buffer->filename, savepoint)) {
		UndoTokenRestoreSavePoint *token;
		buffer->savepoint_id++;
		token = new UndoTokenRestoreSavePoint(savepoint, buffer);
#ifdef G_OS_WIN32
		token->orig_attrs = attributes;
		if (attributes != INVALID_FILE_ATTRIBUTES)
			set_file_attributes(savepoint,
					    attributes | FILE_ATTRIBUTE_HIDDEN);
#endif
		undo.push(token);
	} else {
		interface.msg(Interface::MSG_WARNING,
			      "Unable to create save point file \"%s\"",
			      savepoint);
		g_free(savepoint);
	}
	return attributes;
}
#endif /* !G_OS_UNIX */
bool
Ring::save(const gchar *filename)
{
	const void *buffer;
	sptr_t gap;
	size_t size;
	FILE *file;
#ifdef G_OS_UNIX
	struct stat file_stat;
	file_stat.st_uid = -1;
	file_stat.st_gid = -1;
#endif
	FileAttributes attributes = INVALID_FILE_ATTRIBUTES;
	if (!current)
		return false;
	if (!filename)
		filename = current->filename;
	if (!filename)
		return false;
	if (undo.enabled) {
		if (current->filename &&
		    g_file_test(current->filename, G_FILE_TEST_IS_REGULAR)) {
#ifdef G_OS_UNIX
			g_stat(current->filename, &file_stat);
#endif
			attributes = make_savepoint(current);
		} else {
			undo.push(new UndoTokenRemoveFile(filename));
		}
	}
	/* leaves mode intact if file exists */
	file = g_fopen(filename, "w");
	if (!file)
		return false;
	/* write part of buffer before gap */
	gap = interface.ssm(SCI_GETGAPPOSITION);
	if (gap > 0) {
		buffer = (const void *)interface.ssm(SCI_GETRANGEPOINTER,
						     0, gap);
		if (!fwrite(buffer, (size_t)gap, 1, file)) {
			fclose(file);
			return false;
		}
	}
	/* write part of buffer after gap */
	size = interface.ssm(SCI_GETLENGTH) - gap;
	if (size > 0) {
		buffer = (const void *)interface.ssm(SCI_GETRANGEPOINTER,
						     gap, size);
		if (!fwrite(buffer, size, 1, file)) {
			fclose(file);
			return false;
		}
	}
	/* if file existed but has been renamed, restore attributes */
	if (attributes != INVALID_FILE_ATTRIBUTES)
		set_file_attributes(filename, attributes);
#ifdef G_OS_UNIX
	/*
	 * only a good try to inherit owner since process user must have
	 * CHOWN capability traditionally reserved to root only
	 */
	fchown(fileno(file), file_stat.st_uid, file_stat.st_gid);
#endif
	fclose(file);
	interface.undo_info_update(current);
	undo.push_var(current->dirty);
	current->dirty = false;
	/*
	 * FIXME: necessary also if the filename was not specified but the file
	 * is (was) new, in order to canonicalize the filename.
	 * May be circumvented by cananonicalizing without requiring the file
	 * name to exist (like readlink -f)
	 */
	//if (filename) {
	undo.push_str(current->filename);
	current->set_filename(filename);
	//}
	return true;
}
void
Ring::close(Buffer *buffer)
{
	TAILQ_REMOVE(&head, buffer, buffers);
	if (buffer->filename)
		interface.msg(Interface::MSG_INFO,
			      "Removed file \"%s\" from the ring",
			      buffer->filename);
	else
		interface.msg(Interface::MSG_INFO,
			      "Removed unnamed file from the ring.");
}
void
Ring::close(void)
{
	Buffer *buffer = current;
	buffer->update();
	close(buffer);
	current = buffer->next() ? : buffer->prev();
	/* transfer responsibility to UndoToken object */
	undo.push(new UndoTokenEdit(this, buffer));
	if (current) {
		current->edit();
		QRegisters::hook(QRegisters::HOOK_EDIT);
	} else {
		edit((const gchar *)NULL);
	}
}
Ring::~Ring()
{
	Buffer *buffer, *next;
	TAILQ_FOREACH_SAFE(buffer, &head, buffers, next)
		delete buffer;
}
/*
 * Auxiliary functions
 */
#ifdef G_OS_UNIX
gchar *
get_absolute_path(const gchar *path)
{
	gchar buf[PATH_MAX];
	gchar *resolved;
	if (!path)
		return NULL;
	if (!realpath(path, buf)) {
		if (g_path_is_absolute(path)) {
			resolved = g_strdup(path);
		} else {
			gchar *cwd = g_get_current_dir();
			resolved = g_build_filename(cwd, path, NIL);
			g_free(cwd);
		}
	} else {
		resolved = g_strdup(buf);
	}
	return resolved;
}
#elif defined(G_OS_WIN32)
gchar *
get_absolute_path(const gchar *path)
{
	TCHAR buf[MAX_PATH];
	gchar *resolved = NULL;
	if (path && GetFullPathName(path, sizeof(buf), buf, NULL))
		resolved = g_strdup(buf);
	return resolved;
}
#else
/*
 * FIXME: I doubt that works on any platform...
 */
gchar *
get_absolute_path(const gchar *path)
{
	return path ? g_file_read_link(path, NULL) : NULL;
}
#endif /* !G_OS_UNIX && !G_OS_WIN32 */
/*
 * Command states
 */
void
StateEditFile::do_edit(const gchar *filename) throw (Error)
{
	if (ring.current)
		ring.undo_edit();
	else /* QRegisters::current != NULL */
		QRegisters::undo_edit();
	ring.edit(filename);
}
void
StateEditFile::do_edit(tecoInt id) throw (Error)
{
	if (ring.current)
		ring.undo_edit();
	else /* QRegisters::current != NULL */
		QRegisters::undo_edit();
	if (!ring.edit(id))
		throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, id);
}
/*$
 * [n]EB[file]$ -- Open or edit file
 * nEB$
 *
 * Opens or edits the file with name .
 * If  is not in the buffer ring it is opened,
 * added to the ring and set as the currently edited
 * buffer.
 * If it already exists in the ring, it is merely
 * made the current file.
 *  may be omitted in which case the default
 * unnamed buffer is created/edited.
 * If an argument is specified as 0, EB will additionally
 * display the buffer ring contents in the window's popup
 * area.
 * Naturally this only has any effect in interactive
 * mode.
 *
 *  may also be a glob-pattern, in which case
 * all files matching the pattern are opened/edited.
 *
 * File names of buffers in the ring are normalized
 * by making them absolute.
 * Any comparison on file names is performed using
 * guessed or actual absolute file paths, so that
 * one file may be referred to in many different ways
 * (paths).
 *
 *  does not have to exist on disk.
 * In this case, an empty buffer is created and its
 * name is guessed from .
 * When the newly created buffer is first saved,
 * the file is created on disk and the buffer's name
 * will be updated to the absolute path of the file
 * on disk.
 *
 * File names may also be tab-completed and string building
 * characters are enabled by default.
 *
 * If  is greater than zero, the string argument
 * must be empty.
 * Instead  selects a buffer from the ring to edit.
 * A value of 1 denotes the first buffer, 2 the second,
 * ecetera.
 */
void
StateEditFile::initial(void) throw (Error)
{
	tecoInt id = expressions.pop_num_calc(1, -1);
	allowFilename = true;
	if (id == 0) {
		for (Buffer *cur = ring.first(); cur; cur = cur->next())
			interface.popup_add(Interface::POPUP_FILE,
					    cur->filename ? : "(Unnamed)",
					    cur == ring.current);
		interface.popup_show();
	} else if (id > 0) {
		allowFilename = false;
		do_edit(id);
	}
}
State *
StateEditFile::done(const gchar *str) throw (Error)
{
	BEGIN_EXEC(&States::start);
	if (!allowFilename) {
		if (*str)
			throw Error("If a buffer is selected by id, the  "
				    "string argument must be empty");
		return &States::start;
	}
	if (is_glob_pattern(str)) {
		gchar *dirname;
		GDir *dir;
		dirname = g_path_get_dirname(str);
		dir = g_dir_open(dirname, 0, NULL);
		if (dir) {
			const gchar *basename;
			GPatternSpec *pattern;
			basename = g_path_get_basename(str);
			pattern = g_pattern_spec_new(basename);
			g_free((gchar *)basename);
			while ((basename = g_dir_read_name(dir))) {
				if (g_pattern_match_string(pattern, basename)) {
					gchar *filename;
					filename = g_build_filename(dirname,
								    basename,
								    NIL);
					do_edit(filename);
					g_free(filename);
				}
			}
			g_pattern_spec_free(pattern);
			g_dir_close(dir);
		}
		g_free(dirname);
	} else {
		do_edit(*str ? str : NULL);
	}
	return &States::start;
}
/*$
 * EW$ -- Save or rename current buffer
 * EWfile$
 *
 * Saves the current buffer to disk.
 * If the buffer was dirty, it will be clean afterwards.
 * If the string argument  is not empty,
 * the buffer is saved with the specified file name
 * and is renamed in the ring.
 *
 * In interactive mode, EW is executed immediately and
 * may be rubbed out.
 * In order to support that, \*(ST creates so called
 * save point files.
 * It does not merely overwrite existing files when saving
 * but moves them to save point files instead.
 * Save point files are called \(lq.teco--\(rq
 * where  is the name of the saved file and  is
 * a number that is increased with every save operation.
 * Save point files are always created in the same directory
 * as the original file to ensure that no copying of the file
 * on disk is necessary but only a rename of the file.
 * When rubbing out the EW command, \*(ST restores the latest
 * save point file by moving (renaming) it back to its
 * original path - also not requiring any on-disk copying.
 * \*(ST is impossible to crash, but just in case it still
 * does it may leave behind these save point files which
 * must be manually deleted by the user.
 * Otherwise save point files are deleted on command line
 * termination.
 *
 * File names may also be tab-completed and string building
 * characters are enabled by default.
 */
State *
StateSaveFile::done(const gchar *str) throw (Error)
{
	BEGIN_EXEC(&States::start);
	if (!ring.save(*str ? str : NULL))
		throw Error("Unable to save file");
	return &States::start;
}