/*
 * See the files COPYING and README for copyright information and how to reach
 * the author.
 *
 *  $Id: chain-dvd.c,v 1.31 2006/09/16 18:33:36 lordjaxom Exp $
 */

#include "chain-dvd.h"
#include "burn.h"
#include "logger-vdr.h"
#include "jobs.h"
#include "render.h"
#include "config.h"
#include "menuburn.h"
#include "filter.h"
#include "setup.h"
#include "etsi-const.h"
#include "proctools/shellescape.h"
#include "proctools/shellprocess.h"
#include "proctools/functions.h"
#include "proctools/logger.h"
#include <string>
#include <sstream>
#include <algorithm>
#include <memory>
#include <boost/bind.hpp>
#include <boost/format.hpp>
#include <vdr/recording.h>
#include <sys/stat.h>

namespace vdr_burn
{
	using namespace std;
	using boost::bind;
	using proctools::shellescape;
	using proctools::shellprocess;
	using proctools::logger;
	using proctools::process;

	chain_dvd::chain_dvd(job& job_):
			chain_vdr( "DVD", job_ ),
			m_step( build ),
			m_currentRecording( get_recordings().begin() ),
			m_pxAudioIndex( 0 ),
			m_totalDone( 0 ),
			m_lastRecSize( 0 ),
			m_lastProgress( 0 )
	{
	}

	void chain_dvd::process_line(const string& line)
	{
		static const string px_found_id( "-> found PES-ID " );
		static const string stat_vobu( "STAT: VOBU " );
		static const string stat_fixing( "STAT: fixing VOBU at " );
		static const string burn_done( "% done, estimate finish" );

		switch (m_step) {
		case build:
			{
				if (line.substr(0, px_found_id.length()) == px_found_id) {
					istringstream parser( line.substr(px_found_id.length()) );
					int cid;
					parser >> setbase(16) >> cid;

					logger::info( str( boost::format( "px found id: 0x%02x" ) % cid ) );
					if (cid >= 0xc0 && cid <= 0xdf) { // audio stream
						ostringstream filename;
						filename << "vdrsync";
						if (m_pxAudioIndex > 0)
							filename << m_pxAudioIndex;
						filename << ".mpa";
						m_currentRecording->set_track_path(cid, filename.str());
						++m_pxAudioIndex;
					}
				}
				else if (line.substr(0, stat_vobu.length()) == stat_vobu) {
					if (!elapsed_since(m_lastProgress, 1))
						return;

					istringstream parser(line.substr(stat_vobu.length()));
					int mbDone;
					parser >> mbDone;
					parser.ignore(4); // " at "
					parser >> mbDone;

					double done = (100.f * m_totalDone
								  + 90.f * get_job().get_requant_factor() * mbDone * MEGABYTE(1))
								  / get_job().get_tracks_size() / 2.f;

					set_progress(static_cast<int>(done));
				}
				else if (line.substr(0, stat_fixing.length()) == stat_fixing) {
					if (!elapsed_since(m_lastProgress, 1))
						return;

					istringstream parser(line.substr(stat_fixing.length()));
					int mbDone;
					parser >> mbDone;

					double done = (100.f * (m_totalDone - m_lastRecSize)
								  + 90.f * m_lastRecSize
								  + 10.f * get_job().get_requant_factor() * mbDone * MEGABYTE(1))
								  / get_job().get_tracks_size() / 2.f;

					set_progress(static_cast<int>(done));
				}
			}
			break;

		case burn:
			{
				istringstream parser(line);
				double percentDone;
				parser >> percentDone;
				protected_array<char> rest( new char[ burn_done.length() + 1 ] );
				parser.get(rest.get(), burn_done.length() + 1, '\n');
				if (rest.get() == burn_done) {
					set_progress(static_cast<int>(50.f + percentDone / 2.f));
					set_burn_progress(static_cast<int>(percentDone));
				}
			}
			break;
		};
	}

	bool chain_dvd::initialize()
	{
		return prepare_job();
	}

	bool chain_dvd::finished(const process* proc)
	{
		if (proc->return_status() == process::ok) {
			// positive return
			if (proc->name() == "demux" && global_setup().DemuxType == demuxtype_projectx)
				prepare_mplex();
			else if (proc->name() == "mplex") {
				m_lastRecSize = m_currentRecording->get_tracks_size();
				m_totalDone += m_lastRecSize;
				if (m_step == build) {
					// prevent this to happen again when author quits before mplex
					++m_currentRecording;
					if (m_currentRecording != get_recordings().end())
						prepare_recording();
				}
			}
			else if (proc->name() == "author") {
				m_step = burn;
				if(get_job().get_dmh_archive_mode()) {
				  m_currentRecording = get_recordings().begin();
				  return prepare_dmh_archive();
				} else {
				  // no DMH-archive wanted, so go directly to burning
				  return prepare_burning();
				}
			}
			else if (proc->name() == "dmharchive") {
				++m_currentRecording;
				if (m_currentRecording != get_recordings().end()) {
					return prepare_dmh_archive();
				} else {
					return prepare_archive_mark();
				}
			}
			else if (proc->name() == "archivemark") {
				return prepare_burning();
			}
			return true;
		}

		// process failed
		// XXX determine if growisofs or mkisofs failed
		if (proc->name() == "burn" && prepare_device())
			return prepare_burning();

		return false;
	}

	void chain_dvd::create_files()
	{
		for_each(get_recordings().begin(), get_recordings().end(),
				 bind( &chain_dvd::create_files, this, _1 ));
	}

	void chain_dvd::create_files(const recording& rec)
	{
		// temp folders
		make_dir(rec.get_paths().temp);
		make_dir(rec.get_paths().data);

		// fifos for video track and muxed movie
		make_fifo(rec.get_video_track_path());
		make_fifo(rec.get_movie_path());
		make_fifo(rec.get_requant_path());

		// fifos for each audio track
		const_track_filter audioTracks( rec.get_tracks(), track_info::streamtype_audio, track_predicate::used );
		for_each(audioTracks.begin(), audioTracks.end(),
				 bind( &chain_dvd::make_fifo, this,
				 	   bind( &recording::get_track_path, rec, _1 )
				));

	}

	bool chain_dvd::prepare_job()
	{
		create_files();

		if (get_job().get_disk_type() == disktype_dvd_menu) {
			// render menu's
			renderer render_menus( get_job(), *this );

#warning error handling
			if ( !render_menus() ) {
				logger::error("rendering menu failed - aborting job");
				return false;
			}
		}

		dvdauthor_xml xml(get_job());
		xml.write();

		// prepare first recording
		prepare_recording();

		// start authoring
		shellprocess* author = new shellprocess( "author", shellescape( "vdrburn-dvd.sh" ) + "author" );
		make_dir(xml.get_author_path());
		author->put_environment("DVDAUTHOR_XML",  xml.get_xml_path());
		author->put_environment("DVDAUTHOR_PATH", xml.get_author_path());
		author->put_environment("ISO_FILE",       get_job().get_iso_path());
		author->put_environment("DVD_DEVICE",     BurnParameters.DvdDevice);
		add_process(author);
		return true;
	}

	void chain_dvd::prepare_recording()
	{
		prepare_demux();
		if (global_setup().DemuxType != demuxtype_projectx)
			prepare_mplex();
	}

	void chain_dvd::prepare_demux()
	{
		const char* demux_call;
		if ( global_setup().DemuxType == demuxtype_projectx )
			demux_call = "demuxpx";
		else
			demux_call = "demux";

		m_pxAudioIndex = 0;

		// processes
		shellprocess* demux = new shellprocess( "demux", shellescape( "vdrburn-dvd.sh" ) + demux_call);
		demux->put_environment("RECORDING_PATH", m_currentRecording->get_filename());
		demux->put_environment("IGNORE_TRACKS",  m_currentRecording->get_ignored_cids());
		demux->put_environment("USED_TRACKS",    m_currentRecording->get_used_cids());
		demux->put_environment("MPEG_TMP_PATH",  m_currentRecording->get_paths().temp);
		demux->put_environment("MPEG_DATA_PATH", m_currentRecording->get_paths().data);
		demux->put_environment("CONFIG_PATH",    plugin::get_config_path());
		if (get_job().get_cut_on_demux() && prepare_cutmarks())
			demux->put_environment("USE_CUTTING", "1");
		add_process(demux);
	}

	void chain_dvd::prepare_mplex()
	{
		if (get_job().get_requant_factor() > 1) {
			const char* requant_call;
			if ( global_setup().RequantType == requanttype_transcode )
				requant_call = "tcrequant";
			else
				requant_call = "requant";

			shellprocess* requant = new shellprocess( "requant", shellescape( "vdrburn-dvd.sh" ) + requant_call );
			requant->put_environment("VIDEO_FILE",     m_currentRecording->get_video_track_path());
			requant->put_environment("REQUANT_FILE",   m_currentRecording->get_requant_path());
			requant->put_environment("REQUANT_FACTOR", get_job().get_requant_factor());
			requant->put_environment("VIDEO_SIZE",     m_currentRecording->get_tracks_size( track_info::streamtype_video ));
			add_process(requant);
		}

		shellprocess* mplex = new shellprocess( "mplex", shellescape( "vdrburn-dvd.sh" ) + "mplex" );
		mplex->put_environment("MOVIE_FILE",     m_currentRecording->get_movie_path());
		mplex->put_environment("MPEG_DATA_PATH", m_currentRecording->get_paths().data);
		mplex->put_environment("MPEG_TMP_PATH",  m_currentRecording->get_paths().temp);
		mplex->put_environment("VIDEO_FILE",
				get_job().get_requant_factor() > 1
					? m_currentRecording->get_requant_path()
					: m_currentRecording->get_video_track_path());
		mplex->put_environment("AUDIO_FILES",    m_currentRecording->get_audio_paths());
		add_process(mplex);
	}

	bool chain_dvd::prepare_device()
	{
		if (get_job().get_store_mode() == storemode_create)
			return false; // no device needed for this mode

		shellprocess eject( "eject", shellescape( "eject" ) + BurnParameters.DvdDevice );
		execute(eject);
		if (eject.return_status() != process::ok)
			return false;

		Skins.QueueMessage(mtInfo, tr("Please enter the burn plugin"), 10, -1);
		bool result = menu::pagebase::wait_for_user(tr("Please insert an empty disk and press Ok"));
		Skins.QueueMessage(mtInfo, 0, 0, 0);
		return result;
		/*if (get_job().get_store_mode() == smCreateISO)
			return true;

		while (true) {
			wrappers::cdio::device cdio(BurnParameters.DvdDevice);
			if (!cdio && errno != ENOMEDIUM) {
				logger::error(format("error while accessing {0}")
							  % BurnParameters.DvdDevice);
				return false;
			}

			switch (cdio.get_media_status()) {
			case wrappers::cdio::writeable:
			case wrappers::cdio::unknown:
				return true;

			case wrappers::cdio::no_dvd:
			case wrappers::cdio::readable:
				if (!cdio.eject()) {
					logger::error(format("error while ejecting {0}")
								  % BurnParameters.DvdDevice);
					return false;
				}

			case wrappers::cdio::error:
				Skins.QueueMessage(mtInfo, tr("Please enter the burn plugin"),
								   10, -1);
				if (!menu::pagebase::wait_for_user(tr("Please insert an empty "
						 						      "disk and press Ok")))
					return false;
				break;

			case wrappers::cdio::unknown:
				logger::error(format("unknown condition while accessing {0}")
							  % BurnParameters.DvdDevice);
				return false; XXX Double Layer
			}
		}
		return true;*/
	}

	bool chain_dvd::prepare_burning()
	{
		switch (get_job().get_store_mode()) {
		case storemode_create:
			{
				shellprocess* burn = new shellprocess( "burn", shellescape( "vdrburn-dvd.sh" ) + "mkiso" );
				burn->put_environment("DVDAUTHOR_PATH",
								  dvdauthor_xml::get_author_path(get_job()));
				burn->put_environment("ISO_FILE", get_job().get_iso_path());
				burn->put_environment("DISC_ID", get_job().get_volume_id());
				add_process(burn);
			}
			break;

		case storemode_burn:
			{
				const char* burn_call;
				if (get_job().get_disk_size() == disksize_cdr)
					burn_call = "burndircd";
				else
					burn_call = "burndir";

				shellprocess* burn = new shellprocess( "burn", shellescape( "vdrburn-dvd.sh" ) + burn_call );
				burn->put_environment("DVDAUTHOR_PATH", dvdauthor_xml::get_author_path(get_job()));
				burn->put_environment("DVD_DEVICE", BurnParameters.DvdDevice);
				burn->put_environment("BURN_SPEED", global_setup().BurnSpeed);
				burn->put_environment("DISC_ID", get_job().get_volume_id());
				add_process(burn);
			}
			break;

		case storemode_createburn:
			{
				boost::format fifofmt( "%s/burnfifo" );
				fifofmt % get_paths().temp;
				make_fifo( fifofmt.str() );

				shellprocess* pipe = new shellprocess( "pipe", shellescape( "vdrburn-dvd.sh" ) + "pipeiso" );
				pipe->put_environment("DVDAUTHOR_PATH", dvdauthor_xml::get_author_path(get_job()));
				pipe->put_environment("ISO_FILE",       get_job().get_iso_path());
				pipe->put_environment("ISO_PIPE",       fifofmt.str());
				pipe->put_environment("DISC_ID",        get_job().get_volume_id());
				add_process(pipe);

 				const char* burn_call;
 				if (get_job().get_disk_size() == disksize_cdr)
 					burn_call = "burnisocd";
 				else
 					burn_call = "burniso";

 				shellprocess* burn = new shellprocess( "burn", shellescape( "vdrburn-dvd.sh" ) + burn_call );
				burn->put_environment("DVD_DEVICE", BurnParameters.DvdDevice);
				burn->put_environment("ISO_PIPE",   fifofmt.str());
				burn->put_environment("BURN_SPEED", global_setup().BurnSpeed);
				add_process(burn);
			}
			break;
		}
		set_is_burning();
		return true;
	}

	bool chain_dvd::prepare_cutmarks()
	{
		if (global_setup().DemuxType == demuxtype_vdrsync)
			return true;

		cMarks marks;
		if (!marks.Load(m_currentRecording->get_filename().c_str()) || !marks.Count())
			return false;

		// convert marks to bytepos
		cIndexFile ifile( m_currentRecording->get_filename().c_str(), false );
		ofstream cutfile( str( boost::format("%s/px.cut") % m_currentRecording->get_paths().data ).c_str() );

		for (cMark *mark = marks.First(); mark != 0; mark = marks.Next(mark)) {
			int pos = mark->position;

			uchar filenumber;
			int offset;
			ifile.Get(pos, &filenumber, &offset, 0, 0);

			uint64_t bytepos = offset;
			boost::format filefmt( "%s/%03d.vdr" );
			for (int i = 1; i < filenumber; ++i) {
				boost::format vdrfile( filefmt );
				vdrfile % m_currentRecording->get_filename() % i;

				struct stat filebuf;
				if ( stat( vdrfile.str().c_str(), &filebuf ) < 0 ) {
					logger::error( str( boost::format("error determining size of \"%s\"") % vdrfile ) );
					return false; // so px won't complain about missing marks
				}
				bytepos += filebuf.st_size;
			}
			logger::info( str( boost::format( "mark at %s, bytepos %llu" ) % *IndexToHMSF( pos, true ) % bytepos ) );
			cutfile << bytepos << endl;
		}
		cutfile.close();
		return true;
	}

	bool chain_dvd::prepare_dmh_archive()
	{
		shellprocess* dmharchive = new shellprocess( "dmharchive", shellescape( "vdrburn-dvd.sh" ) + "dmharchive" );
		dmharchive->put_environment("DVDAUTHOR_PATH", dvdauthor_xml::get_author_path(get_job()));
		dmharchive->put_environment("RECORDING_PATH", m_currentRecording->get_filename());
		dmharchive->put_environment("TEMP_PATH",  m_currentRecording->get_paths().temp);
		dmharchive->put_environment("CONFIG_PATH", plugin::get_config_path());

		string track = str( boost::format( "%02d" ) % ( std::distance( get_recordings().begin(), m_currentRecording ) + 1 ) );
		dmharchive->put_environment("TRACK_ON_DVD", track);

		add_process(dmharchive);
		return true;
	}

	bool chain_dvd::prepare_archive_mark()
	{
		shellprocess* archivemark = new shellprocess( "archivemark", shellescape( "vdrburn-dvd.sh" ) + "archivemark" );
		archivemark->put_environment("DVDAUTHOR_PATH", dvdauthor_xml::get_author_path(get_job()));
		archivemark->put_environment("CONFIG_PATH",    plugin::get_config_path());

		add_process(archivemark);
		return true;
	}
}
