// Copyright 2006 Ben Hutchings <ben@decadent.org.uk>.
// See the file "COPYING" for licence details.

#include <cerrno>
#include <cstring>
#include <sstream>

#include <unistd.h>
#include <fcntl.h>

#include <expat.h>

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

#include "auto_fd.hpp"
#include "auto_handle.hpp"
#include "vob_list.hpp"

namespace
{
    struct xml_parser_closer
    {
	void operator()(XML_Parser parser) const
	    {
		if (parser)
		    XML_ParserFree(parser);
	    }
    };

    struct xml_parser_factory
    {
	XML_Parser operator()() const
	    {
		return NULL;
	    }
    };

    typedef auto_handle<XML_Parser, xml_parser_closer, xml_parser_factory>
    auto_xml_parser;

    struct parse_context
    {
	parse_context(XML_Parser parser,
		      const std::string & list_path,
		      vob_list & list)
		: parser(parser),
		  list_path(list_path),
		  base_path(Glib::path_get_dirname(list_path)),
		  list(list),
		  level(0)
	    {}
	XML_Parser parser;
	const std::string & list_path;
	std::string base_path;
	vob_list & list;
	std::auto_ptr<xml_error> parse_error;
	unsigned level;
    };

    void start_element_handler(void * user_data,
			       const char * name,
			       const char ** attributes)
    {
	parse_context & context = *static_cast<parse_context *>(user_data);

	if (context.level == 0 && std::strcmp(name, "vob-list") == 0)
	{
	    // We don't expect (and will ignore) any attributes.
	}
	else if (context.level == 1 && std::strcmp(name, "vob") == 0)
	{
	    vob_ref ref;

	    while (attributes[0] != NULL)
	    {
		if (std::strcmp(attributes[0], "file") == 0)
		{
		    ref.file = attributes[1];
		    if (!Glib::path_is_absolute(ref.file))
			ref.file = Glib::build_filename(context.base_path,
							ref.file);
		}
		else if (std::strcmp(attributes[0], "chapters") == 0)
		{
		    ref.chapters = attributes[1];
		}
		else if (std::strcmp(attributes[0], "pause") == 0)
		{
		    ref.pause = attributes[1];
		}

		attributes += 2;
	    }

	    if (ref.file.empty())
	    {
		context.parse_error.reset(
		    new xml_error(
			context.list_path,
			XML_GetCurrentLineNumber(context.parser),
			"<vob> element missing file attribute"));
	    }
	    else
	    {
		context.list.push_back(ref);
	    }
	}
	else // not root <vob-list> or child <vob>
	{
	    context.parse_error.reset(
		new xml_error(
		    context.list_path,
		    XML_GetCurrentLineNumber(context.parser),
		    std::string("unexpected element: <").append(name)
		    .append(">")));
	}

	++context.level;
    }

    void end_element_handler(void * user_data,
			     const char * /*name*/)
    {
	parse_context & context = *static_cast<parse_context *>(user_data);
	--context.level;
    }
}

vob_list read_vob_list(const std::string & path)
{
    vob_list result;

    auto_fd fd(open(path.c_str(), O_RDONLY, 0));
    if (fd.get() < 0)
	// FIXME: look up proper error code
	throw Glib::FileError(Glib::FileError::FAILED,
			      std::strerror(errno));

    auto_xml_parser parser(XML_ParserCreate(NULL));
    if (parser.get() == NULL)
	throw std::bad_alloc(); // any other reason?

    parse_context context(parser.get(), path, result);
    XML_SetUserData(parser.get(), &context);
    XML_SetStartElementHandler(parser.get(), start_element_handler);
    XML_SetEndElementHandler(parser.get(), end_element_handler);

    for (;;)
    {
	static const int buffer_size = 1024;
	void * buffer = XML_GetBuffer(parser.get(), buffer_size);
	if (buffer == NULL)
	    throw std::bad_alloc();
	int read_size = read(fd.get(), buffer, buffer_size);
	if (read_size < 0)
	    // FIXME: look up proper error code
	    throw Glib::FileError(Glib::FileError::FAILED,
				  std::strerror(errno));
	bool is_final = read_size < buffer_size;
	if (XML_ParseBuffer(parser.get(), read_size, is_final)
	    == XML_STATUS_ERROR)
	    throw xml_error(path,
			    XML_GetCurrentLineNumber(parser.get()),
			    XML_ErrorString(XML_GetErrorCode(parser.get())));
	if (context.parse_error.get())
	    throw *context.parse_error;
	if (is_final)
	    break;
    }

    return result;
}

namespace
{
    std::string make_xml_error_message(const std::string & path, int line,
				       const std::string & message)
    {
	std::ostringstream os;
	os << path << ":" << line << ": " << message;
	return os.str();
    }
}

xml_error::xml_error(const std::string & path, int line,
		     const std::string & message)
	: std::runtime_error(make_xml_error_message(path, line, message))
{}
