/* Resp: A programming language
 * Copyright (C) 2008-2009 David Robillard <http://drobilla.net>
 *
 * Resp 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 (at your
 * option) any later version.
 *
 * Resp 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 more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Resp.  If not, see <http://www.gnu.org/licenses/>.
 */

/** @file
 * @brief Parsing (build an AST from text)
 */

#include <stdio.h>

#include <stack>
#include <string>

#include "resp.hpp"

using namespace std;

static inline int
read_char(Cursor& cur, istream& in)
{
	switch (in.peek()) {
	case '\n': ++cur.line; cur.col = 0; break;
	default:   ++cur.col;
	}
	return in.get();
}

static inline void
skip_space(Cursor& cur, istream& in)
{
	while (isspace(in.peek()))
		read_char(cur, in);
}

static inline void
eat_char(Cursor& cur, istream& in, const char character)
{
	const char c = read_char(cur, in);
	THROW_IF(c != character, cur, (format("expected `%1%'") % character).str());
}

static const AST*
read_string(Cursor& cur, istream& in)
{
	Cursor loc = cur;
	string str;
	char   c;
	eat_char(cur, in, '"');
	while ((c = read_char(cur, in)) != '"') {
		switch (c) {
		case '\\':
			switch (c = read_char(cur, in)) {
			case '"':
				str.push_back('"');
				break;
			case '\\':
				str.push_back('\\');
				break;
			default:
				cin.putback(c);
				throw Error(cur, string("unknown string escape `\\") + (char)c + "'");
			}
		default:
			str.push_back(c);
		}
	}
	return new AString(loc, str);
}

static void
read_line_comment(Cursor& cur, istream& in)
{
	while (read_char(cur, in) != '\n') {}
}

static const AST*
read_list(PEnv& penv, Cursor& cur, istream& in)
{
	List list;

	eat_char(cur, in, '(');
	while (true) {
		skip_space(cur, in);
		if (in.peek() == ')') {
			eat_char(cur, in, ')');
			return list.head;
		} else {
			list.push_back(penv.parse(cur, in));
		}
	}
	assert(false);
}

static const AST*
read_special(Cursor& cur, istream& in)
{
	eat_char(cur, in, '#');
	switch (in.peek()) {
	case '|':
		while (!(read_char(cur, in) == '|' && read_char(cur, in) == '#')) {}
		return NULL;
	case 't':
		eat_char(cur, in, 't');
		return new ALiteral<bool>(T_BOOL, true, cur);
	case 'f':
		eat_char(cur, in, 'f');
		return new ALiteral<bool>(T_BOOL, false, cur);
	default:
		throw Error(cur, (format("unknown special lexeme `%1%'") % in.peek()).str());
	}
	assert(false);
}

static const AST*
read_number(Cursor& cur, istream& in)
{
	Cursor loc = cur;
	string str;
	char   c;
	while ((c = in.peek()) != EOF) {
		if (isdigit(c) || c == '.')
			str += read_char(cur, in);
		else
			break;
	}

	if (str.find('.') == string::npos)
		return new ALiteral<int32_t>(T_INT32, strtol(str.c_str(), NULL, 10), loc);
	else
		return new ALiteral<float>(T_FLOAT, strtod(str.c_str(), NULL), loc);
}

static const AST*
read_symbol(PEnv& penv, Cursor& cur, istream& in)
{
	string str;
	char   c;
	while ((c = in.peek()) != EOF) {
		if (!isspace(c) && c != ')' && c != '(' && c != EOF && c != -1)
			str += read_char(cur, in);
		else
			break;
	}

	return penv.sym(str, cur);
}

/// Read an expression from @a in
const AST*
PEnv::parse(Cursor& cur, istream& in) throw(Error)
{
	while (!cin.eof()) {
		skip_space(cur, in);
		const char c = in.peek();
		switch (c) {
		case EOF:
			return NULL;
		case ';':
			read_line_comment(cur, in);
			break;
		case '"':
			return read_string(cur, in);
		case '(':
			return read_list(*this, cur, in);
		case ')':
			throw Error(cur, "unexpected `)'");
		case '#':
		{
			const AST* ret = read_special(cur, in);
			if (ret)
				return ret;
			break;
		}
		case '-':
		case '+':
			read_char(cur, in);
			if (isdigit(in.peek())) {
				in.putback(c);
				return read_number(cur, in);
			} else {
				in.putback(c);
				return read_symbol(*this, cur, in);
			}
		default:
			if (isdigit(c))
				return read_number(cur, in);
			else
				return read_symbol(*this, cur, in);
		}
	}
	return NULL;
}