/*
  This file is part of Ingen.
  Copyright 2016 David Robillard <http://drobilla.net/>

  Ingen is free software: you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free
  Software Foundation, either version 3 of the License, or any later version.

  Ingen 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 Affero General Public License for details.

  You should have received a copy of the GNU Affero General Public License
  along with Ingen.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <ctime>

#include "ingen/URIMap.hpp"
#include "ingen/URIs.hpp"
#include "lv2/lv2plug.in/ns/ext/atom/util.h"
#include "lv2/lv2plug.in/ns/ext/patch/patch.h"
#include "serd/serd.h"
#include "sratom/sratom.h"

#include "UndoStack.hpp"

#define NS_RDF (const uint8_t*)"http://www.w3.org/1999/02/22-rdf-syntax-ns#"

#define USTR(s) ((const uint8_t*)(s))

namespace Ingen {
namespace Server {

int
UndoStack::start_entry()
{
	if (_depth == 0) {
		time_t now;
		time(&now);
		_stack.push_back(Entry(now));
	}
	return ++_depth;
}

bool
UndoStack::write(const LV2_Atom* msg, int32_t default_id)
{
	_stack.back().push_event(msg);
	return true;
}

bool
UndoStack::ignore_later_event(const LV2_Atom* first,
                              const LV2_Atom* second) const
{
	if (first->type != _uris.atom_Object || first->type != second->type) {
		return false;
	}

	const LV2_Atom_Object* f = (const LV2_Atom_Object*)first;
	const LV2_Atom_Object* s = (const LV2_Atom_Object*)second;
	if (f->body.otype == _uris.patch_Set && f->body.otype == s->body.otype) {
		const LV2_Atom* f_subject  = nullptr;
		const LV2_Atom* f_property = nullptr;
		const LV2_Atom* s_subject  = nullptr;
		const LV2_Atom* s_property = nullptr;
		lv2_atom_object_get(f,
		                    (LV2_URID)_uris.patch_subject,  &f_subject,
		                    (LV2_URID)_uris.patch_property, &f_property,
		                    0);
		lv2_atom_object_get(s,
		                    (LV2_URID)_uris.patch_subject,  &s_subject,
		                    (LV2_URID)_uris.patch_property, &s_property,
		                    0);
		return (lv2_atom_equals(f_subject,  s_subject) &&
		        lv2_atom_equals(f_property, s_property));
	}

	return false;
}

int
UndoStack::finish_entry()
{
	if (--_depth > 0) {
		return _depth;
	} else if (_stack.back().events.empty()) {
		// Disregard empty entry
		_stack.pop_back();
	} else if (_stack.size() > 1 && _stack.back().events.size() == 1) {
		// This entry and the previous one have one event, attempt to merge
		auto i = _stack.rbegin();
		++i;
		if (i->events.size() == 1) {
			if (ignore_later_event(i->events[0], _stack.back().events[0])) {
				_stack.pop_back();
			}
		}
	}

	return _depth;
}

UndoStack::Entry
UndoStack::pop()
{
	Entry top;
	if (!_stack.empty()) {
		top = _stack.back();
		_stack.pop_back();
	}
	return top;
}

struct BlankIDs {
	BlankIDs(char c='b') : n(0), c(c) {}

	SerdNode get() {
		snprintf(buf, sizeof(buf), "%c%u", c, n++);
		return serd_node_from_string(SERD_BLANK, USTR(buf));
	}

	char       buf[16];
	unsigned   n;
	const char c;
};

struct ListContext {
	explicit ListContext(BlankIDs& ids, unsigned flags, const SerdNode* s, const SerdNode* p)
		: ids(ids)
		, s(*s)
		, p(*p)
		, flags(flags | SERD_LIST_O_BEGIN)
	{}

	SerdNode start_node(SerdWriter* writer) {
		const SerdNode node = ids.get();
		serd_writer_write_statement(writer, flags, nullptr, &s, &p, &node, nullptr, nullptr);
		return node;
	}

	void append(SerdWriter* writer, unsigned oflags, const SerdNode* value) {
		// s p node
		const SerdNode node = start_node(writer);

		// node rdf:first value
		p     = serd_node_from_string(SERD_URI, NS_RDF "first");
		flags = SERD_LIST_CONT;
		serd_writer_write_statement(writer, flags|oflags, nullptr, &node, &p, value, nullptr, nullptr);

		end_node(writer, &node);
	}

	void end_node(SerdWriter* writer, const SerdNode* node) {
		// Prepare for next call: node rdf:rest ...
		s = *node;
		p = serd_node_from_string(SERD_URI, NS_RDF "rest");
	}

	void end(SerdWriter* writer) {
		const SerdNode nil = serd_node_from_string(SERD_URI, NS_RDF "nil");
		serd_writer_write_statement(writer, flags, nullptr, &s, &p, &nil, nullptr, nullptr);
	}

	BlankIDs& ids;
	SerdNode  s;
	SerdNode  p;
	unsigned  flags;
};

void
UndoStack::write_entry(Sratom*                 sratom,
                       SerdWriter*             writer,
                       const SerdNode* const   subject,
                       const UndoStack::Entry& entry)
{
	char time_str[24];
	strftime(time_str, sizeof(time_str), "%FT%T", gmtime(&entry.time));

	// entry rdf:type ingen:UndoEntry
	SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "time"));
	SerdNode o = serd_node_from_string(SERD_LITERAL, USTR(time_str));
	serd_writer_write_statement(writer, SERD_ANON_CONT, nullptr, subject, &p, &o, nullptr, nullptr);

	p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "events"));

	BlankIDs    ids('e');
	ListContext ctx(ids, SERD_ANON_CONT, subject, &p);

	for (const LV2_Atom* atom : entry.events) {
		const SerdNode node = ctx.start_node(writer);

		p         = serd_node_from_string(SERD_URI, NS_RDF "first");
		ctx.flags = SERD_LIST_CONT;
		sratom_write(sratom, &_map.urid_unmap_feature()->urid_unmap, SERD_LIST_CONT,
		             &node, &p,
		             atom->type, atom->size, LV2_ATOM_BODY_CONST(atom));

		ctx.end_node(writer, &node);
	}

	ctx.end(writer);
}

void
UndoStack::save(FILE* stream, const char* name)
{
	SerdEnv* env = serd_env_new(nullptr);
	serd_env_set_prefix_from_strings(env, USTR("atom"),  USTR(LV2_ATOM_PREFIX));
	serd_env_set_prefix_from_strings(env, USTR("ingen"), USTR(INGEN_NS));
	serd_env_set_prefix_from_strings(env, USTR("patch"), USTR(LV2_PATCH_PREFIX));

	const SerdNode base = serd_node_from_string(SERD_URI, USTR("ingen:/"));
	SerdURI        base_uri;
	serd_uri_parse(base.buf, &base_uri);

	SerdWriter* writer = serd_writer_new(
		SERD_TURTLE,
		(SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED|SERD_STYLE_CURIED),
		env,
		&base_uri,
		serd_file_sink,
		stream);

	// Configure sratom to write directly to the writer (and thus the socket)
	Sratom* sratom = sratom_new(&_map.urid_map_feature()->urid_map);
	sratom_set_sink(sratom,
	                (const char*)base.buf,
	                (SerdStatementSink)serd_writer_write_statement,
	                (SerdEndSink)serd_writer_end_anon,
	                writer);

	SerdNode s = serd_node_from_string(SERD_BLANK, (const uint8_t*)name);
	SerdNode p = serd_node_from_string(SERD_URI, USTR(INGEN_NS "entries"));

	BlankIDs    ids('u');
	ListContext ctx(ids, 0, &s, &p);
	for (const Entry& e : _stack) {
		const SerdNode entry = ids.get();
		ctx.append(writer, SERD_ANON_O_BEGIN, &entry);
		write_entry(sratom, writer, &entry, e);
		serd_writer_end_anon(writer, &entry);
	}
	ctx.end(writer);

	sratom_free(sratom);
	serd_writer_finish(writer);
	serd_writer_free(writer);
}

} // namespace Server
} // namespace Ingen