/*
  This file is part of Ingen.
  Copyright 2007-2012 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 <assert.h>
#include <errno.h>
#include <string.h>

#include <iostream>

#include <glibmm/fileutils.h>
#include <glibmm/miscutils.h>

#include "ingen/Configuration.hpp"
#include "raul/fmt.hpp"
#include "sord/sordmm.hpp"
#include "sratom/sratom.h"

#define NS_INGEN "http://drobilla.net/ns/ingen#"

namespace Ingen {

Configuration::Configuration(Forge& forge)
	: _forge(forge)
	, _shortdesc("A realtime modular audio processor.")
	, _desc(
"Ingen is a flexible modular system that be used in various ways.\n"
"The engine can run as a stand-alone server controlled via network protocol,\n"
"or internal to another process (e.g. the GUI).  The GUI, or other\n"
"clients, can communicate with the engine via any supported protocol, or host the\n"
"engine in the same process.  Many clients can connect to an engine at once.\n\n"
"Examples:\n"
"  ingen -e              # Run an engine, listen for connections\n"
"  ingen -g              # Run a GUI, connect to running engine\n"
"  ingen -eg             # Run an engine and a GUI in one process\n"
"  ingen -egl foo.ttl    # Run an engine and a GUI and load a graph\n"
"  ingen -egl foo.ingen  # Run an engine and a GUI and load a graph")
	, _max_name_length(0)
{
	add("clientPort",     "client-port",    'C', "Client port", SESSION, forge.Int, Raul::Atom());
	add("connect",        "connect",        'c', "Connect to engine URI", SESSION, forge.String, forge.alloc("unix:///tmp/ingen.sock"));
	add("engine",         "engine",         'e', "Run (JACK) engine", SESSION, forge.Bool, forge.make(false));
	add("enginePort",     "engine-port",    'E', "Engine listen port", SESSION, forge.Int, forge.make(16180));
	add("socket",         "socket",         'S', "Engine socket path", SESSION, forge.String, forge.alloc("/tmp/ingen.sock"));
	add("gui",            "gui",            'g', "Launch the GTK graphical interface", SESSION, forge.Bool, forge.make(false));
	add("",               "help",           'h', "Print this help message", SESSION, forge.Bool, forge.make(false));
	add("jackName",       "jack-name",      'n', "JACK name", SESSION, forge.String, forge.alloc("ingen"));
	add("jackServer",     "jack-server",    's', "JACK server name", GLOBAL, forge.String, forge.alloc(""));
	add("uuid",           "uuid",           'u', "JACK session UUID", SESSION, forge.String, Raul::Atom());
	add("load",           "load",           'l', "Load graph", SESSION, forge.String, Raul::Atom());
	add("path",           "path",           'L', "Target path for loaded graph", SESSION, forge.String, Raul::Atom());
	add("queueSize",      "queue-size",     'q', "Event queue size", GLOBAL, forge.Int, forge.make(4096));
	add("run",            "run",            'r', "Run script", SESSION, forge.String, Raul::Atom());
	add("humanNames",     "human-names",     0,  "Show human names in GUI", GUI, forge.Bool, forge.make(true));
	add("portLabels",     "port-labels",     0,  "Show port labels in GUI", GUI, forge.Bool, forge.make(true));
	add("graphDirectory", "graph-directory", 0,  "Default directory for opening graphs", GUI, forge.String, Raul::Atom());
}

Configuration&
Configuration::add(const std::string&       key,
                   const std::string&       name,
                   char                     letter,
                   const std::string&       desc,
                   Scope                    scope,
                   const Raul::Atom::TypeID type,
                   const Raul::Atom&        value)
{
	assert(value.type() == type || value.type() == 0);
	_max_name_length = std::max(_max_name_length, name.length());
	_options.insert(make_pair(name, Option(key, name, letter, desc, scope, type, value)));
	if (!key.empty()) {
		_keys.insert(make_pair(key, name));
	}
	if (letter != '\0') {
		_short_names.insert(make_pair(letter, name));
	}
	return *this;
}

void
Configuration::print_usage(const std::string& program, std::ostream& os)
{
	os << "Usage: " << program << " [OPTION]..." << std::endl;
	os << _shortdesc << std::endl << std::endl;
	os << _desc << std::endl << std::endl;
	os << "Options:" << std::endl;
	for (Options::iterator o = _options.begin(); o != _options.end(); ++o) {
		Option& option = o->second;
			os << "  ";
		if (option.letter != '\0')
			os << "-" << option.letter << ", ";
		else
			os << "    ";
		os.width(_max_name_length + 4);
		os << std::left << (std::string("--") + o->first);
		os << option.desc << std::endl;
	}
}

int
Configuration::set_value_from_string(Configuration::Option& option,
                                     const std::string&     value)
		throw (Configuration::OptionError)
{
	if (option.type == _forge.Int) {
		char* endptr = NULL;
		int   intval = static_cast<int>(strtol(value.c_str(), &endptr, 10));
		if (endptr && *endptr == '\0') {
			option.value = _forge.make(intval);
		} else {
			throw OptionError(
				(Raul::fmt("option `%1%' has non-integer value `%2%'")
				 % option.name % value).str());
		}
	} else if (option.type == _forge.String) {
		option.value = _forge.alloc(value.c_str());
		assert(option.value.type() == _forge.String);
	} else if (option.type == _forge.Bool) {
		option.value = _forge.make(bool(!strcmp(value.c_str(), "true")));
		assert(option.value.type() == _forge.Bool);
	} else {
		throw OptionError(
			(Raul::fmt("bad option type `%1%'") % option.name).str());
	}
	return EXIT_SUCCESS;
}

/** Parse command line arguments. */
void
Configuration::parse(int argc, char** argv) throw (Configuration::OptionError)
{
	for (int i = 1; i < argc; ++i) {
		if (argv[i][0] != '-' || !strcmp(argv[i], "-")) {
			_files.push_back(argv[i]);
		} else if (argv[i][1] == '-') {
			const std::string name = std::string(argv[i]).substr(2);
			Options::iterator o = _options.find(name);
			if (o == _options.end()) {
				throw OptionError(
					(Raul::fmt("unrecognized option `%1%'") % name).str());
			}
			if (o->second.type == _forge.Bool) {
				o->second.value = _forge.make(true);
			} else {
				if (++i >= argc)
					throw OptionError(
						(Raul::fmt("missing value for `%1'") % name).str());
				set_value_from_string(o->second, argv[i]);
			}
		} else {
			const size_t len = strlen(argv[i]);
			for (size_t j = 1; j < len; ++j) {
				char letter = argv[i][j];
				ShortNames::iterator n = _short_names.find(letter);
				if (n == _short_names.end())
					throw OptionError(
						(Raul::fmt("unrecognized option `%1%'") % letter).str());
				Options::iterator o = _options.find(n->second);
				if (j < len - 1) {
					if (o->second.type != _forge.Bool)
						throw OptionError(
							(Raul::fmt("missing value for `%1%'") % letter).str());
					o->second.value = _forge.make(true);
				} else {
					if (o->second.type == _forge.Bool) {
						o->second.value = _forge.make(true);
					} else {
						if (++i >= argc)
							throw OptionError(
								(Raul::fmt("missing value for `%1%'") % letter).str());
						set_value_from_string(o->second, argv[i]);
					}
				}
			}
		}
	}
}

bool
Configuration::load(const std::string& path)
{
	if (!Glib::file_test(path, Glib::FILE_TEST_EXISTS)) {
		return false;
	}

	SerdNode node = serd_node_new_file_uri(
		(const uint8_t*)path.c_str(), NULL, NULL, true);
	const std::string uri((const char*)node.buf);

	Sord::World world;
	Sord::Model model(world, uri, SORD_SPO, false);
	SerdEnv*    env = serd_env_new(&node);
	model.load_file(env, SERD_TURTLE, uri, uri);

	Sord::Node nodemm(world, Sord::Node::URI, (const char*)node.buf);
	Sord::Node nil;
	for (Sord::Iter i = model.find(nodemm, nil, nil); !i.end(); ++i) {
		const Sord::Node& pred = i.get_predicate();
		const Sord::Node& obj  = i.get_object();
		if (pred.to_string().substr(0, sizeof(NS_INGEN) - 1) == NS_INGEN) {
			const std::string key = pred.to_string().substr(sizeof(NS_INGEN) - 1);
			const Keys::iterator k = _keys.find(key);
			if (k != _keys.end() && obj.type() == Sord::Node::LITERAL) {
				set_value_from_string(_options.find(k->second)->second,
				                      obj.to_string());
			}
		}
	}

	serd_node_free(&node);
	serd_env_free(env);
	return true;
}

std::string
Configuration::save(URIMap&            uri_map,
                    const std::string& app,
                    const std::string& filename,
                    unsigned           scopes)
		throw (FileError)
{
	// Save to file if it is absolute, otherwise save to user config dir
	std::string path = filename;
	if (!Glib::path_is_absolute(path)) {
		path = Glib::build_filename(Glib::get_user_config_dir(), app, filename);
	}

	// Create parent directories if necessary
	const std::string dir = Glib::path_get_dirname(path);
	if (g_mkdir_with_parents(dir.c_str(), 0755) < 0) {
		throw FileError((Raul::fmt("Error creating directory %1% (%2%)")
		                % dir % strerror(errno)).str());
	}

	// Attempt to open file for writing
	FILE* file = fopen(path.c_str(), "w");
	if (!file) {
		throw FileError((Raul::fmt("Failed to open file %1% (%2%)")
		                 % path % strerror(errno)).str());
	}

	// Use the file's URI as the base URI
	SerdURI  base_uri;
	SerdNode base = serd_node_new_file_uri(
		(const uint8_t*)path.c_str(), NULL, &base_uri, true);

	// Create environment with ingen prefix
	SerdEnv* env = serd_env_new(&base);
	serd_env_set_prefix_from_strings(
		env, (const uint8_t*)"ingen", (const uint8_t*)NS_INGEN);

	// Create Turtle writer
	SerdWriter* writer = serd_writer_new(
		SERD_TURTLE,
		(SerdStyle)(SERD_STYLE_RESOLVED|SERD_STYLE_ABBREVIATED),
		env,
		&base_uri,
		serd_file_sink,
		file);

	// Write a prefix directive for each prefix in the environment
	serd_env_foreach(env, (SerdPrefixSink)serd_writer_set_prefix, writer);

	// Create an atom serialiser and connect it to the Turtle writer
	Sratom* sratom = sratom_new(&uri_map.urid_map_feature()->urid_map);
	sratom_set_pretty_numbers(sratom, true);
	sratom_set_sink(sratom, (const char*)base.buf,
	                (SerdStatementSink)serd_writer_write_statement, NULL,
	                writer);

	// Write a statement for each valid option
	for (Options::iterator o = _options.begin(); o != _options.end(); ++o) {
		const Raul::Atom& value = o->second.value;
		if (!(o->second.scope & scopes) ||
			o->second.key.empty() ||
		    !value.is_valid()) {
			continue;
		}

		const std::string key(std::string("ingen:") + o->second.key);
		SerdNode pred = serd_node_from_string(
			SERD_CURIE, (const uint8_t*)key.c_str());
		sratom_write(sratom, &uri_map.urid_unmap_feature()->urid_unmap, 0,
		             &base, &pred, value.type(), value.size(), value.get_body());
	}

	sratom_free(sratom);
	serd_writer_free(writer);
	serd_env_free(env);
	serd_node_free(&base);
	fclose(file);

	return path;
}

std::list<std::string>
Configuration::load_default(const std::string& app,
                            const std::string& filename)
{
	std::list<std::string> loaded;

	const std::vector<std::string> dirs = Glib::get_system_config_dirs();
	for (std::vector<std::string>::const_iterator i = dirs.begin();
	     i != dirs.end();
	     ++i) {
		const std::string path = Glib::build_filename(*i, app, filename);
		if (load(path)) {
			loaded.push_back(path);
		}
	}

	const std::string path = Glib::build_filename(
		Glib::get_user_config_dir(), app, filename);
	if (load(path)) {
		loaded.push_back(path);
	}

	return loaded;
}

const Raul::Atom&
Configuration::option(const std::string& long_name) const
{
	static const Raul::Atom nil;
	Options::const_iterator o = _options.find(long_name);
	if (o == _options.end()) {
		return nil;
	} else {
		return o->second.value;
	}
}

bool
Configuration::set(const std::string& long_name, const Raul::Atom& value)
{
	Options::iterator o = _options.find(long_name);
	if (o != _options.end()) {
		o->second.value = value;
		return true;
	}
	return false;
}

} // namespace Ingen