/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** 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, 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, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/

#include "mu-command-parser.hh"
#include "mu-error.hh"
#include "mu-utils.hh"

#include <iostream>
#include <algorithm>

using namespace Mu;
using namespace Command;

void
Command::invoke(const Command::CommandMap& cmap, const Sexp& call)
{
	if (!call.is_call()) {
		throw Mu::Error{Error::Code::Command,
				"expected call-sexpr but got %s",
				call.to_sexp_string().c_str()};
	}

	const auto& params{call.list()};
	const auto  cmd_it = cmap.find(params.at(0).value());
	if (cmd_it == cmap.end())
		throw Mu::Error{Error::Code::Command,
				"unknown command in call %s",
				call.to_sexp_string().c_str()};

	const auto& cinfo{cmd_it->second};

	// all required parameters must be present
	for (auto&& arg : cinfo.args) {
		const auto& argname{arg.first};
		const auto& arginfo{arg.second};

		// calls used keyword-parameters, e.g.
		//    (my-function :bar 1 :cuux "fnorb")
		// so, we're looking for the odd-numbered parameters.
		const auto param_it = [&]() -> Sexp::Seq::const_iterator {
			for (size_t i = 1; i < params.size(); i += 2)
				if (params.at(i).is_symbol() && params.at(i).value() == argname)
					return params.begin() + i + 1;

			return params.end();
		}();

		// it's an error when a required parameter is missing.
		if (param_it == params.end()) {
			if (arginfo.required)
				throw Mu::Error{Error::Code::Command,
						"missing required parameter %s in call %s",
						argname.c_str(),
						call.to_sexp_string().c_str()};
			continue; // not required
		}

		// the types must match, but the 'nil' symbol is acceptable as
		// "no value"
		if (param_it->type() != arginfo.type && !(param_it->is_nil()))
			throw Mu::Error{Error::Code::Command,
					"parameter %s expects type %s, but got %s in call %s",
					argname.c_str(),
					to_string(arginfo.type).c_str(),
					to_string(param_it->type()).c_str(),
					call.to_sexp_string().c_str()};
	}

	// all passed parameters must be known
	for (size_t i = 1; i < params.size(); i += 2) {
		if (std::none_of(cinfo.args.begin(), cinfo.args.end(), [&](auto&& arg) {
			    return params.at(i).value() == arg.first;
		    }))
			throw Mu::Error{Error::Code::Command,
					"unknown parameter %s in call %s",
					params.at(i).value().c_str(),
					call.to_sexp_string().c_str()};
	}

	if (cinfo.handler)
		cinfo.handler(params);
}

static Sexp::Seq::const_iterator
find_param_node(const Parameters& params, const std::string& argname)
{
	if (params.empty())
		throw Error(Error::Code::InvalidArgument, "params must not be empty");

	if (argname.empty() || argname.at(0) != ':')
		throw Error(Error::Code::InvalidArgument,
			    "property key must start with ':' but got '%s')",
			    argname.c_str());

	for (size_t i = 1; i < params.size(); i += 2) {
		if (i + 1 != params.size() && params.at(i).is_symbol() &&
		    params.at(i).value() == argname)
			return params.begin() + i + 1;
	}

	return params.end();
}

static Error
wrong_type(Sexp::Type expected, Sexp::Type got)
{
	return Error(Error::Code::InvalidArgument,
		     "expected <%s> but got <%s>",
		     to_string(expected).c_str(),
		     to_string(got).c_str());
}

Option<std::string>
Command::get_string(const Parameters& params, const std::string& argname)
{
	const auto it = find_param_node(params, argname);
	if (it == params.end() || it->is_nil())
		return Nothing;
	else if (!it->is_string())
		throw wrong_type(Sexp::Type::String, it->type());
	else
		return it->value();
}

Option<std::string>
Command::get_symbol(const Parameters& params, const std::string& argname)
{
	const auto it = find_param_node(params, argname);
	if (it == params.end() || it->is_nil())
		return Nothing;
	else if (!it->is_symbol())
		throw wrong_type(Sexp::Type::Symbol, it->type());
	else
		return it->value();
}

Option<int>
Command::get_int(const Parameters& params, const std::string& argname)
{
	const auto it = find_param_node(params, argname);
	if (it == params.end() || it->is_nil())
		return Nothing;
	else if (!it->is_number())
		throw wrong_type(Sexp::Type::Number, it->type());
	else
		return ::atoi(it->value().c_str());
}

Option<unsigned>
Command::get_unsigned(const Parameters& params, const std::string& argname)
{
	if (auto val = get_int(params, argname); val && *val >= 0)
		return val;
	else
		return Nothing;
}


Option<bool>
Command::get_bool(const Parameters& params, const std::string& argname)
{
	const auto it = find_param_node(params, argname);
	if (it == params.end())
		return Nothing;
	else if (!it->is_symbol())
		throw wrong_type(Sexp::Type::Symbol, it->type());
	else
		return it->is_nil() ? false : true;
}

std::vector<std::string>
Command::get_string_vec(const Parameters& params, const std::string& argname)
{
	const auto it = find_param_node(params, argname);
	if (it == params.end() || it->is_nil())
		return {};
	else if (!it->is_list())
		throw wrong_type(Sexp::Type::List, it->type());

	std::vector<std::string> vec;
	for (const auto& n : it->list()) {
		if (!n.is_string())
			throw wrong_type(Sexp::Type::String, n.type());
		vec.emplace_back(n.value());
	}

	return vec;
}
