/*
 * See the files COPYING and README for copyright information and how to reach
 * the author.
 *
 *  $Id: menuburn.c,v 1.20 2006/04/25 17:24:34 lordjaxom Exp $
 */

#include "common.h"
#include "menuburn.h"
#include "menuitems.h"
#include "i18n.h"
#include "setup.h"
#include "skins.h"
#include "jobs.h"
#include "proctools/logger.h"
#include "proctools/format.h"
#include "boost/bind.hpp"
#include <typeinfo>
#include <utility>
#include <iterator>
#include <algorithm>
#include <sstream>
#include <vdr/interface.h>
#include <vdr/videodir.h>

namespace vdr_burn
{

	using namespace std;
	using proctools::format;
	using proctools::logger;
	using proctools::process;
	using boost::bind;

	namespace menu
	{

		// --- track_editor ---------------------------------------------------

		track_editor::track_editor(recording& recording_):
				m_recording(recording_)
		{
			track_info_list::iterator it;
			for (it = m_recording.get_video_tracks().begin();
					it != m_recording.get_video_tracks().end();
					++it) {
				m_data.push_back(make_pair(it, it->used));
			}
			for (it = m_recording.get_audio_tracks().begin();
					it != m_recording.get_audio_tracks().end();
					++it) {
				m_data.push_back(make_pair(it, it->used));
			}
			refresh();
		}

		void track_editor::refresh(void)
		{
			int current = Current();
			Clear();
			for_each(m_data.begin(), m_data.end(), 
					 bind( &track_editor::add_track, this, _1 ));
			set_help_keys();
			SetCurrent(Get(current));
			Display();
		}

		bool track_editor::can_move_down(void)
		{
			track_info_list::size_type current( Current() );

			if (current < m_recording.get_video_tracks().size() - 1)
				return true;

			if (current >= m_recording.get_video_tracks().size() &&
					current < m_recording.get_video_tracks().size() +
							  m_recording.get_audio_tracks().size() - 1)
				return true;

			return false;
		}

		void track_editor::set_help_keys(void)
		{
			const char* green = 0;
			if (can_move_down())
				green = tr("Move down");
			SetHelp(0, green);
		}

		void track_editor::add_track(data_pair& track)
		{
			Add(new menu::bool_item(track.first->get_type_description(), 
									track.second));
		}

		eOSState track_editor::dispatch_key(eKeys key)
		{
			switch(key)
			{
			case kGreen:
				move_down();
				break;

			default:
				break;
			}

			return osContinue;
		}

		void track_editor::move_down(void)
		{
			if (!can_move_down())
				return;

			data_iterator current = m_data.begin();
			advance(current, Current());
			data_iterator next = current;
			++next;
			std::swap(*current, *next);
			refresh();
		}

		void track_editor::Store(void)
		{
			data_iterator it;
			track_info_list videoTracks;
			for (it = m_data.begin(); it != m_data.end() && 
					it->first->type == video; ++it) {
				it->first->used = it->second;
				videoTracks.push_back(*it->first);
			}

			track_info_list audioTracks;
			for (; it != m_data.end(); ++it) {
				it->first->used = it->second;
				audioTracks.push_back(*it->first);
			}

			m_recording.get_video_tracks().swap(videoTracks);
			m_recording.get_audio_tracks().swap(audioTracks);
			m_recording.freeze();
		}

		eOSState track_editor::ProcessKey(eKeys key)
		{
			eOSState state = cMenuSetupPage::ProcessKey(key);

			if (state == osUnknown)
				return dispatch_key(key);

			set_help_keys();
			return state;
		}
		
		// --- starter --------------------------------------------------------

		const string starter::strings[starter::count] = 
				{ string(), "Recordings", "List", "Status" };
		starter* starter::m_instance = 0;
		
		starter::starter(void):
			cOsdMenu(""),
			m_display(none)
		{
			m_instance = this;
			if (manager::get_is_busy())
				m_nextDisplay = status;
			else if (pagebase::m_pending != 0 && 
					pagebase::m_pending->get_recordings().size() > 0)
				m_nextDisplay = job;
			else
				m_nextDisplay = recordings;
			show_menu();
		}

		starter::~starter()
		{
			m_instance = 0;
		}

		string starter::get_next_display(void)
		{
			display next = display( (m_instance->m_display + 1) % count );
			if (next == none)
				next = recordings;
			return tr(strings[next].c_str());
		}

		eOSState starter::show_menu(void)
		{
			m_display = m_nextDisplay;
			m_nextDisplay = none;

			switch (m_display)
			{
			case recordings:
				return AddSubMenu(new menu::recordings);

			case job:
				return AddSubMenu(new menu::job_editor);
				
			case status:
				return AddSubMenu(new menu::status);

			default:
				return osBack;
			}
		}

		eOSState starter::ProcessKey(eKeys key)
		{
			bool hadSubMenu = HasSubMenu();
			eOSState state = cOsdMenu::ProcessKey(key);

			if (hadSubMenu && !HasSubMenu())
				return show_menu();
			return state;
		}

		// --- pagebase -------------------------------------------------------
		
		job* pagebase::m_pending = 0;
		pagebase::user_wait pagebase::m_waitInfo;

		pagebase::pagebase(const string& title, int tab0, int tab1):
				cOsdMenu(title.c_str(), tab0, tab1)
		{
			if (m_pending == 0)
				m_pending = new job;
		}

		pagebase::~pagebase()
		{
		}

		void pagebase::set_help(const string& red, const string& green, 
								const string& yellow)
		{
			// <rant> must be constants, which is mentioned inside the impl,
			// not any documentation or so... well we assume, since we are in
			// the foreground thread, that the constants are not used anymore 
			// so it is safe to replace the strings here before giving the new
			// constants to the core
			m_red = red;
			m_green = green;
			m_yellow = yellow;
			m_blue = starter::get_next_display();

			SetHelp(m_red.empty() ? 0 : m_red.c_str(), 
					m_green.empty() ? 0 : m_green.c_str(), 
					m_yellow.empty() ? 0 : m_yellow.c_str(),
					m_blue.c_str());
		}

		void pagebase::set_current(int current)
		{
			if (current == -1 || Get(current) == 0) {
				current = 0;
				while (Get(current) != 0 && !Get(current)->Selectable())
					++current;
			}
			if (Get(current) != 0)
				SetCurrent(Get(current));
		}

		void pagebase::display(void)
		{
			set_help_keys();
			Display();
		}

		eOSState pagebase::dispatch_key(eKeys key)
		{
			switch (key) {
			case kOk:     return ok_pressed();
			case kRed:    return red_pressed();
			case kGreen:  return green_pressed();
			case kYellow: return yellow_pressed();
			case kNone:   check_waiting_user(); return menu_update();
			default:      break;

			case kBlue:
				starter::cycle_display();
				return osBack;
			}
			return osUnknown;
		}

		eOSState pagebase::ProcessKey(eKeys key)
		{
			bool hadSubMenu = HasSubMenu();
			if (!hadSubMenu && key == kBack && !menu_closing())
				return osContinue;
				
			int current = Current();
			eOSState state = cOsdMenu::ProcessKey(key);

			if (hadSubMenu && HasSubMenu())
				return state;
			
			if (state == osUnknown)
				state = dispatch_key(key);
			else if (state == osContinue && current != Current())
				set_help_keys();
			return state;
		}

		bool pagebase::wait_for_user(const std::string& message)
		{
			logger::debug("wait_for_user");
			cMutexLock lock(&m_waitInfo.lock);
			
			m_waitInfo.waiting = true;
			m_waitInfo.message = message;
			m_waitInfo.condition.Wait(m_waitInfo.lock);
			logger::debug("finished waiting");
			m_waitInfo.waiting = false;

			return m_waitInfo.result;
		}

		void pagebase::check_waiting_user(void)
		{
			cMutexLock lock(&m_waitInfo.lock);

			if (m_waitInfo.waiting) {
				m_waitInfo.result = 
						Interface->Confirm(m_waitInfo.message.c_str(), 3600);
				m_waitInfo.condition.Broadcast();
			}
		}

		// --- recordings -----------------------------------------------------

		recordings::recordings(void):
				pagebase(tr("Write DVDs - Recordings"), 10, 6),
				m_pathLevel(0),
				m_pathChanged(false)
		{
			display();
		}

		void recordings::display(void)
		{
			cThreadLock RecordingsLock(&Recordings);
			Recordings.Sort();

			int current = Current();
			Clear();

			ostringstream title;
			title << tr("Recordings stored on harddisk");
			if (!m_basePath.empty())
				title << "(" << m_basePath << ")";
			Add(new menu::text_item(title.str()));

			if (m_pending->get_recordings().size() > 0)
				Add(new menu::text_item(
						format(tr("Recordings: {0}, total size: {1} MB"))
						% m_pending->get_recordings().size()
						% format::prec(m_pending->get_size_mb(), 1)));
			else
				Add(new menu::text_item(
						tr("No recordings marked for burning")));

			Add(new menu::text_item(
					progress_bar(m_pending->get_size_mb(),
								 m_pending->get_disk_size_mb(), 100)));
			Add(new menu::text_item(""));

			recording_item *lastItem = 0;
			string lastText;
			for (cRecording *rec = Recordings.First(); rec != NULL;
				 rec = Recordings.Next(rec)) {

				if (m_pending->get_by_filename(rec->FileName())
						!= m_pending->get_recordings().end())
					continue;

				if (!m_basePath.empty() &&
						(string( rec->Name() ).find(m_basePath) != 0 ||
						 rec->Name()[m_basePath.length()] != '~')) 
					continue;

				menu::recording_item* item = 
						new menu::recording_item(rec, m_pathLevel);
				if (string( item->Text() ).length() != 0 && (lastItem == 0 ||
						item->Text() != lastText)) {
					Add(item);
					lastText = item->Text();
					lastItem = item;
				} else
					delete item;

				if (lastItem != 0 && lastItem->is_directory())
					lastItem->add_entry(rec->IsNew());
			}

			if (m_pathChanged) {
				current = -1;
				m_pathChanged = false;
			}
			set_current(current);
			pagebase::display();
		}

		void recordings::set_help_keys(void)
		{
			cOsdItem* current = Get(Current());

			string red;
			if (current != 0) {
				if (!dynamic_cast<menu::recording_item*>( current )
						->is_directory())
					red = tr("Choose");
			}
			set_help(red);
		}

		bool recordings::menu_closing(void)
		{
			if (m_pathLevel == 0)
				return true;

			string newPath;
			if (m_pathLevel > 1) {
				string::size_type pos;
				if ((pos = m_basePath.rfind('~')) != string::npos)
					newPath = m_basePath.substr(0, pos);
			}
			m_basePath = newPath;
			m_pathChanged = true;
			--m_pathLevel;
			display();
			return false;
		}

		eOSState recordings::ok_pressed(void)
		{
			recording_item* item = 
					dynamic_cast<recording_item*>( Get(Current()) );

			if (item == 0 || !item->is_directory())
				return osUnknown;

			++m_pathLevel;
			ostringstream newPath;
			if (!m_basePath.empty())
				newPath << m_basePath << "~";
			newPath << item->get_name();
			m_basePath = newPath.str();
			m_pathChanged = true;
			display();
			return osContinue;
		}

		eOSState recordings::red_pressed(void)
		{
			recording_item* item = 
					dynamic_cast<recording_item*>( Get(Current()) );

			if (item == 0 || item->is_directory())
				return osUnknown;

			Skins.Message(mtStatus, tr("Analyzing recording..."));
			m_pending->append_recording(item->get_recording());
			display();
			return osContinue;
		}

		// --- job ------------------------------------------------------------

		job_editor::job_editor(void):
				pagebase(tr("Write DVDs - Job"), 16),
				m_offset(0)
		{
			display();
		}

		void job_editor::display(void)
		{
			int current = Current();
			Clear();

			Add(new menu::text_item(tr("Recordings marked for burning")));
			Add(new menu::text_item(""));
			m_offset = 2;

			if (m_pending->get_recordings().size() > 0) {
				recording_list::iterator rec;
				for (rec = m_pending->get_edit_recordings().begin();
					 rec != m_pending->get_edit_recordings().end(); ++rec) {

					Add(new recording_edit_item(rec));
				}
			} else {
				Add(new menu::text_item(tr("No recordings marked"), true));
				pagebase::display();
				return;
			}

			Add(new menu::text_item(""));
			Add(new menu::text_item(
					format(tr("Total size: {0} MB"))
					% format::prec(m_pending->get_size_mb(), 1)));

			Add(new menu::text_item(
					progress_bar(m_pending->get_size_mb(),
								 m_pending->get_disk_size_mb(), 100)));
			//XXX Width of menu

			int diskTypeCount = disk_type::count;
			if (m_pending->get_size_mb() >= 
					0.9 * m_pending->get_disk_size_mb()) {
				Add(new menu::text_item(
						tr("Recording(s) could be shrinked!")));
				diskTypeCount = disk_type::count_requant;
			}

			Add(new menu::text_item(""));
			Add(new cMenuEditStrItem(
					tr("DVD title"), m_pending->m_title, JOBNAMELEN, 
					TitleChars));
			Add(new cMenuEditStraItem(
					tr("Disk type"), &m_pending->m_diskType, diskTypeCount,
					i18n::get_disk_types()));

			if (m_pending->get_disk_type() == disk_type::dvd_menu &&
					skin_list::get().size() > 1)
				Add(new cMenuEditStraItem(
						tr("Skin"), &m_pending->m_skinIndex,
						skin_list::get().size(),
						skin_list::get().get_strings()));

			if (BurnSetup.OfferChapters && 
					m_pending->get_disk_type() != disk_type::archive)
				Add(new cMenuEditStraItem(
						tr("Chapters"), &m_pending->m_chaptersMode, cm_Count, 
						i18n::ChaptersModes()));

			if (BurnSetup.OfferDiskSize)
				Add(new cMenuEditStraItem(
						tr("Disk size"), &m_pending->m_diskSize, 
						disk_size::count, i18n::get_disk_sizes()));

			if (!BurnParameters.DvdDevice.empty() &&
				!BurnParameters.IsoPath.empty() && BurnSetup.OfferStoreMode)
				Add(new cMenuEditStraItem(
						tr("Target"), &m_pending->m_storeMode, sm_Count, 
						i18n::StoreModes()));

			set_current(current);
			pagebase::display();
		}

		void job_editor::set_help_keys(void)
		{
			string red, green, yellow;
			if (m_pending->get_recordings().size() > 0) {
				red = tr("Burn");

				recording_list::size_type current( Current() );
				if (current >= m_offset && current - m_offset 
						< m_pending->get_recordings().size()) {
					yellow = tr("Remove");
					if (current - m_offset 
							< m_pending->get_recordings().size() - 1)
						green = tr("Move down");
				}
			}
			set_help(red, green, yellow);
		}

		eOSState job_editor::ok_pressed(void)
		{
			recording_edit_item* item =
					dynamic_cast<recording_edit_item*>( Get(Current()) );

			if (item != 0)
				return AddSubMenu(new menu::track_editor(
						*item->get_recording()));
			return osUnknown;
		}

		eOSState job_editor::red_pressed(void)
		{
			if (m_pending->get_recordings().size() == 0)
				return osUnknown;

			recording_list::const_iterator rec;
			for (rec = m_pending->get_recordings().begin();
				 rec != m_pending->get_recordings().end(); ++rec) {
				
				if (find_if(rec->get_used_video_tracks().begin(),
							rec->get_used_video_tracks().end(),
							!bind( &track_info::used, _1 )) 
						!= rec->get_used_video_tracks().end()) {
						
					Skins.Message(mtError, tr("No usable video track found"));
					// XXX Should be moved to job control to determine whether
					// a video track is necessary (audio-dvd or so)
					return osContinue;
				}

				if (rec->get_used_audio_tracks().size() == 0) {
					Skins.Message(mtError, tr("No audio track selected"));
					return osContinue;
				}
			}

			if (m_pending->get_size_mb() 
					>= FreeDiskSpaceMB(BurnParameters.DataPath.c_str())) {
				Skins.Message(mtError, tr("Not enough free disk space!"));
				return osContinue;
			}

			manager::add_job(m_pending);
			m_pending = new job();
			starter::cycle_display();
			return osBack;
		}

		eOSState job_editor::green_pressed(void)
		{
			recording_edit_item *item
					= dynamic_cast<recording_edit_item*>( Get(Current()) );

			if (item == 0)
				return osUnknown;

			recording_list::size_type current( Current() );
			if (current >= m_offset && current - m_offset
					< m_pending->get_recordings().size() - 1) {
				recording_list::iterator rec = item->get_recording();
				recording_list::iterator next = rec;
				++next;

				std::swap(rec->get_edit_index(), next->get_edit_index());
				m_pending->get_edit_recordings().sort();
				display();
			}
			return osContinue;
		}

		eOSState job_editor::yellow_pressed(void)
		{
			recording_edit_item *item
					= dynamic_cast<recording_edit_item*>( Get(Current()) );

			if (item == 0)
				return osUnknown;

			if (!Interface->Confirm(tr("Remove recording from list?")))
				return osContinue;

			m_pending->get_edit_recordings().erase(item->get_recording());
			if (m_pending->get_recordings().size() == 0)
				*m_pending = job();
			display();
			return osContinue;
		}

		// --- recordings -----------------------------------------------------

		status::status(void):
				pagebase(tr("Write DVDs - Status"), 16)
		{
			display();
		}

		void status::display(void)
		{
			int current = Current();
			Clear();

			Add(new menu::text_item(tr("Queued jobs")));
			Add(new menu::text_item());

			manager::lock man_lock;

			int number = 1;
			if (manager::get_active() != 0) {
				Add(new menu::job_item(manager::get_active(), number++));

				int percent;
				if (manager::get_active()->get_is_burning(percent))
					Add(new menu::text_item(
							format(tr("Job active (Burning: {0}%%)"))
							% percent));
				else
					Add(new menu::text_item(tr("Job active (Converting)")));

				Add(new menu::text_item(
						progress_bar(manager::get_active()->get_progress(), 
									 100, 100)));
				//XXX Width of menu
			} else
				Add(new menu::text_item(tr("No active or waiting jobs present"),
										  true));

			for (job_queue::const_iterator job = manager::get_queued().begin();
				 job != manager::get_queued().end(); ++job)
				Add(new menu::job_item(*job, number++));

			if (manager::get_canceled().size() > 0) {
				Add(new menu::text_item());
				Add(new menu::text_item(tr("Canceled jobs")));
				Add(new menu::text_item());

				number = 1;
				job_queue::const_iterator job;
				for (job = manager::get_canceled().begin();
					 job != manager::get_canceled().end(); ++job)
					Add(new menu::job_item(*job, number++, true));
			}

			if (manager::get_erroneous().size() > 0) {
				Add(new menu::text_item());
				Add(new menu::text_item(tr("Erroneous jobs")));
				Add(new menu::text_item());

				number = 1;
				job_queue::const_iterator job;
				for (job = manager::get_erroneous().begin();
					 job != manager::get_erroneous().end(); ++job) {
					Add(new menu::job_item(*job, number++, true));
				}
			}
			set_current(current);
			pagebase::display();
		}

		void status::set_help_keys(void)
		{
			job_item* item = 
					dynamic_cast<job_item*>( Get(Current()) );

			manager::lock man_lock;

			string green, yellow;
			if (item != 0) {
				if (item->get_job()->get_is_canceled())
					green = tr("Restart");
				else if (item->get_job()->get_return_status() == process::error)
					green = tr("Retry");
				
				if (item->get_job() == manager::get_active())
					yellow = tr("Cancel");
				else
					yellow = tr("Remove");
			}
			set_help(string(), green, yellow);
		}

		eOSState status::menu_update(void)
		{
			display();
			return osContinue;
		}

		eOSState status::green_pressed(void)
		{
			job_item* item = 
					dynamic_cast<job_item*>( Get(Current()) );

			if (item == 0)
				return osUnknown;

			manager::lock man_lock;

			if (item->get_job() == manager::get_active())
				return osUnknown;

			if (m_pending->get_recordings().size() > 0
					&& !Interface->Confirm(tr("Replace current job?")))
				return osContinue;

			delete m_pending;
			m_pending = item->get_job();
			manager::del_job(m_pending);
			m_pending->reset();
			starter::cycle_display();
			starter::cycle_display();
			return osBack;
		}

		eOSState status::yellow_pressed(void)
		{
			job_item* item = 
					dynamic_cast<job_item*>( Get(Current()) );

			if (item == 0)
				return osUnknown;

			manager::lock man_lock;

			if (item->get_job() == manager::get_active()) {
				if (Interface->Confirm(tr("Job still active - really cancel?")))
					item->get_job()->stop();
			} 
			else if (Interface->Confirm("Remove job from list?")) {
				manager::del_job(item->get_job());
				delete item->get_job();
			}
			display();
			return osContinue;
		}
	}

}
