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

#include <fstream>
#include <stdexcept>

#include <glibmm/spawn.h>

#include "dvd.hpp"
#include "generate_dvd.hpp"
#include "xml_utils.hpp"

dvd_contents::menu::menu()
	: vob_temp(new temp_file("videolink-vob-"))
{
    vob_temp->close();
}

void generate_dvd(const dvd_contents & contents,
		  const std::string & output_dir)
{
    temp_file temp("videolink-dvdauthor-");
    temp.close();
    std::ofstream file(temp.get_name().c_str());

    // We generate code that uses registers in the following way:
    //
    // g0:     scratch
    // g1:     current location
    // g2:     location that last jumped to a title
    // g3:     target chapter number
    //
    // All locations are divided into two bitfields: the least
    // significant 10 bits are a page/menu number and the most
    // significant 6 bits are a link/button number, and numbering
    // starts at 1, not 0.  This is done for compatibility with
    // the encoding of the s8 (button) register.
    //
    static const int button_mult = dvd::reg_s8_button_mult;
    static const int menu_mask = button_mult - 1;
    static const int button_mask = (1 << dvd::reg_bits) - button_mult;

    file <<
	"<dvdauthor>\n"
	"  <vmgm>\n"
	"    <menus>\n";
	    
    for (unsigned menu_index = 0;
	 menu_index != contents.menus.size();
	 ++menu_index)
    {
	const dvd_contents::menu & menu = contents.menus[menu_index];

	if (menu_index == 0)
	{
	    // This is the first (title) menu, displayed when the
	    // disc is first played.
	    file <<
		"      <pgc entry='title' pause='inf'>\n"
		"        <pre>\n"
		// Initialise the current location if it is not set
		// (all general registers are initially 0).
		"          if (g1 eq 0)\n"
		"            g1 = " << 1 + button_mult << ";\n";
	}
	else
	{
	    file <<
		"      <pgc pause='inf'>\n"
		"        <pre>\n";
	}

	// When a title finishes or the user presses the menu
	// button, this always jumps to the titleset's root menu.
	// We want to return the user to the last menu they used.
	// So we arrange for each titleset's root menu to return
	// to the vmgm title menu and then dispatch from there to
	// whatever the correct menu is.  We determine the correct
	// menu by looking at the menu part of g1.

	file << "          g0 = g1 &amp; " << menu_mask << ";\n";

	// There is a limit of 128 VM instructions in each PGC.
	// Therefore in each menu's <pre> section we generate
	// jumps to menus with numbers greater by 512, 256, 128,
	// ..., 1 where (a) such a menu exists, (b) this menu
	// number is divisible by twice that increment and (c) the
	// correct menu is that or a later menu.  Thus each menu
	// has at most 10 such conditional jumps and is reachable
	// by at most 10 jumps from the title menu.  This chain of
	// jumps might take too long on some players; this has yet
	// to be investigated.
	    
	for (std::size_t menu_incr = (menu_mask + 1) / 2;
	     menu_incr != 0;
	     menu_incr /= 2)
	{
	    if (menu_index + menu_incr < contents.menus.size()
		&& (menu_index & (menu_incr * 2 - 1)) == 0)
	    {
		file <<
		    "          if (g0 ge " << 1 + menu_index + menu_incr
					   << ")\n"
		    "            jump menu " << 1 + menu_index + menu_incr
					   << ";\n";
	    }
	}

	file <<
	    // Highlight the appropriate button.
	    "          s8 = g1 &amp; " << button_mask << ";\n"
	    "        </pre>\n"
	    "        <vob file='" << menu.vob_temp->get_name() << "'/>\n";

	for (unsigned button_index = 0;
	     button_index != menu.entries.size();
	     ++button_index)
	{
	    const dvd_contents::pgc_ref & target =
		menu.entries[button_index];

	    file << "        <button> ";

	    if (target.type == dvd_contents::menu_pgc)
	    {
		unsigned target_button_num;

		if (target.sub_index)
		{
		    target_button_num = target.sub_index;
		}
		else
		{
		    // Look for a button on the new menu that links
		    // back to this one.  If there is one, set that to
		    // be the highlighted button; otherwise, use the
		    // first button.
		    const std::vector<dvd_contents::pgc_ref> &
			target_menu_entries =
			contents.menus[target.index].entries;
		    dvd_contents::pgc_ref this_pgc(dvd_contents::menu_pgc,
						   menu_index);
		    target_button_num = target_menu_entries.size();
		    while (target_button_num != 0
			   && (target_menu_entries[target_button_num - 1]
			       != this_pgc))
			--target_button_num;
		}
			 
		file <<
		    // Set new menu location.
		    "g1 = " << (1 + target.index
				+ target_button_num * button_mult) << "; "
		    // Jump to the target menu.
		    "jump menu " << 1 + target.index << "; ";
	    }
	    else
	    {
		assert(target.type == dvd_contents::title_pgc);

		file <<
		    // Record current menu location (g1 specifies this
		    // menu but not necessarily this button).
		    "g1 = " << (1 + menu_index
				+ (1 + button_index) * button_mult) << "; "
		    // Set target chapter number.
		    "g3 = " << target.sub_index << "; "
		    // Jump to the target title.
		    "jump title " << 1 + target.index << "; ";
	    }

	    file <<  "</button>\n";
	}

	file <<
	    // Some DVD players don't seem to obey pause='inf' so make
	    // them loop.
	    "        <post>\n"
	    "          jump cell 1;\n"
	    "        </post>\n"
	    "      </pgc>\n";
    }

    file <<
	"    </menus>\n"
	"  </vmgm>\n";

    // Generate a titleset for each title.  This appears to make
    // jumping to titles a whole lot simpler (but limits us to 99
    // titles).
    for (unsigned title_index = 0;
	 title_index != contents.titles.size();
	 ++title_index)
    {
	file <<
	    "  <titleset>\n"
	    // Generate a dummy menu so that the menu button on the
	    // remote control will work.
	    "    <menus>\n"
	    "      <pgc entry='root'>\n"
	    "        <pre> jump vmgm menu; </pre>\n"
	    "      </pgc>\n"
	    "    </menus>\n"
	    "    <titles>\n"
	    "      <pgc>\n"
	    "        <pre>\n"
	    // Record calling location.
	    "          g2 = g1;\n";

	// Count chapters in the title.
	unsigned n_chapters = 0;
	for (vob_list::const_iterator
		 it = contents.titles[title_index].begin(),
		 end = contents.titles[title_index].end();
	     it != end;
	     ++it)
	{
	    // Chapter start times may be specified in the "chapters"
	    // attribute as a comma-separated list.  If this is not
	    // specified then the beginning of each file starts a new
	    // chapter.  Thus the number of chapters in each file is
	    // the number of commas in the chapter attribute, plus 1.
	    ++n_chapters;
	    std::size_t pos = 0;
	    while ((pos = it->chapters.find(',', pos)) != std::string::npos)
	    {
		++n_chapters;
		++pos;
	    }
	}

	// Generate jump "table" for chapters.
	for (unsigned chapter_num = 1;
	     chapter_num <= n_chapters;
	     ++chapter_num)
	    file <<
		"          if (g3 == " << chapter_num << ")\n"
		"            jump chapter " << chapter_num << ";\n";

	file <<
	    "        </pre>\n";

	for (vob_list::const_iterator
		 it = contents.titles[title_index].begin(),
		 end = contents.titles[title_index].end();
	     it != end;
	     ++it)
	{
	    file << "        <vob file='" << xml_escape(it->file) << "'";
	    if (!it->chapters.empty())
		file << " chapters='" << xml_escape(it->chapters) << "'";
	    if (!it->pause.empty())
		file << " pause='" << xml_escape(it->pause) << "'";
	    file << "/>\n";
	}

	file <<
	    "        <post>\n"
	    // If the menu location has not been changed during
	    // the title, set the location to be the following
	    // button in the menu.
	    "          if (g1 eq g2)\n"
	    "            g1 = g1 + " << button_mult << ";\n"
	    // In any case, return to some menu.
	    "          call menu;\n"
	    "        </post>\n"
	    "      </pgc>\n"
	    "    </titles>\n"
	    "  </titleset>\n";
    }

    file <<
	"</dvdauthor>\n";

    file.close();

    {
	const char * argv[] = {
	    "dvdauthor",
	    "-o", output_dir.c_str(),
	    "-x", temp.get_name().c_str(),
	    0
	};
	int command_result;
	Glib::spawn_sync(".",
			 Glib::ArrayHandle<std::string>(
			     argv, sizeof(argv)/sizeof(argv[0]),
			     Glib::OWNERSHIP_NONE),
			 Glib::SPAWN_SEARCH_PATH
			 | Glib::SPAWN_STDOUT_TO_DEV_NULL,
			 SigC::Slot0<void>(),
			 0, 0,
			 &command_result);
	if (command_result != 0)
	    throw std::runtime_error("dvdauthor failed");
    }
}
