/////////////////////////////////////////////////////////////////////////////
// Name:        Pgc.cpp
// Purpose:     The class to store a PGC
// Author:      Alex Thuering
// Created:     29.01.2003
// RCS-ID:      $Id: Pgc.cpp,v 1.14 2011/11/23 19:18:49 ntalex Exp $
// Copyright:   (c) Alex Thuering
// Licence:     GPL
/////////////////////////////////////////////////////////////////////////////

#include "Pgc.h"
#include "Menu.h"
#include "Cache.h"
#include "Config.h"
#include "Utils.h"
#include <wx/file.h>
#include <wx/filename.h>
#include <wx/tokenzr.h>
#include <wx/sstream.h>
#include <wxSVGXML/svgxmlhelpr.h>
#include <wxVillaLib/utils.h>
#include <wxSVG/mediadec_ffmpeg.h>

#define DATA_FILE(fname) wxFindDataFile(_T("data") + wxString(wxFILE_SEP_PATH) + fname)

//////////////////////////////// Cell ////////////////////////////////////
/** Constructor */
Cell::Cell() {
	m_start = -1;
	m_end = -1;
	m_chapter = false;
	m_program = false;
	m_pause = 0;
}

/** Constructor */
Cell::Cell(long start, bool chapter) {
	m_start = start;
	m_end = -1;
	m_chapter = chapter;
	m_program = false;
	m_pause = 0;
}

/** Returns the start time as string */
wxString Cell::GetStartStr() const {
	return Time2String(m_start);
}

/** Sets the start time */
void Cell::SetStart(const wxString& startStr) {
	m_start = String2Time(startStr);
}

/** Returns the end time as string */
wxString Cell::GetEndStr() const {
	return Time2String(m_end);
}

/** Sets the end time */
void Cell::SetEnd(const wxString& endStr) {
	m_end = String2Time(endStr);
}

wxSvgXmlNode* Cell::GetXML(DVDFileType type) {
	wxSvgXmlNode* node = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("cell"));
	if (m_start >= 0)
		node->AddProperty(wxT("start"), GetStartStr());
	if (m_end >= 0)
		node->AddProperty(wxT("end"), GetEndStr());
	if (m_chapter)
		node->AddProperty(wxT("chapter"), wxT("1"));
	if (m_program)
		node->AddProperty(wxT("program"), wxT("1"));
	if (m_pause != 0)
		node->AddProperty(wxT("pause"), m_pause == -1 ? wxT("inf") : wxString::Format(wxT("%d"), m_pause));
	if (m_commands.length())
		node->AddChild(new wxSvgXmlNode(wxSVGXML_TEXT_NODE, wxEmptyString, m_commands));
	return node;
}

bool Cell::PutXML(wxSvgXmlNode* node) {
	if (node == NULL || node->GetName() != wxT("cell"))
		return false;
	long lval;
	wxString val;
	if (node->GetPropVal(wxT("start"), &val))
		SetStart(val);
	if (node->GetPropVal(wxT("end"), &val))
		SetEnd(val);
	if (node->GetPropVal(wxT("chapter"), &val))
		SetChapter(val == wxT("1") || val == wxT("on") || val == wxT("yes"));
	if (node->GetPropVal(wxT("program"), &val))
		SetProgram(val == wxT("1") || val == wxT("on") || val == wxT("yes"));
	if (node->GetPropVal(wxT("pause"), &val) && val == wxT("inf"))
		SetPause(-1);
	else if (node->GetPropVal(wxT("pause"), &val) && val.ToLong(&lval))
		SetPause(lval);
	if (node->GetChildren() && (node->GetChildren()->GetType() == wxSVGXML_TEXT_NODE
				|| node->GetChildren()->GetType() == wxSVGXML_CDATA_SECTION_NODE))
		SetCommands(node->GetChildren()->GetContent().Strip(wxString::both));
	return true;
}

//////////////////////////////// Stream /////////////////////////////////////

Stream::Stream(StreamType type, wxString codecName) {
	m_type = type;
	m_sourceCodecName = codecName;
	m_sourceChannelNumber = -1;
	m_sourceAspectRatio = -1;
	m_sourceFps = -1;
	m_destinationFormat = 1; // vfCOPY/afCOPY/sfCOPY
	m_audioVolume = 256;
}

VideoFormat Stream::GetSourceVideoFormat() {
	if (m_sourceVideoSize.GetWidth() == 720 && m_sourceVideoSize.GetHeight() == 576)
		return vfPAL;
	else if (m_sourceVideoSize.GetWidth() == 720 && m_sourceVideoSize.GetHeight() == 480)
		return vfNTSC;
	return vfNONE;
}

AudioFormat Stream::GetSourceAudioFormat() {
	if (m_sourceCodecName == wxT("mp2"))
		return afMP2;
	else if (m_sourceCodecName == wxT("liba52") || m_sourceCodecName == wxT("ac3")
			|| m_sourceCodecName == wxT("ac-3"))
		return afAC3;
	return afNONE;
}

wxString Stream::GetSourceFormat() {
	wxString result = m_sourceCodecName;
	if (result == wxT("liba52") || m_sourceCodecName == wxT("ac-3"))
		result = wxT("ac3");
	else if (result.StartsWith(wxT("mpeg2video")))
		result = wxT("mpeg2");
	switch (m_type) {
	case stVIDEO:
		if (m_sourceVideoSize.IsFullySpecified()) {
			result += wxString::Format(wxT(", %dx%d"),
					m_sourceVideoSize.GetWidth(), m_sourceVideoSize.GetHeight());
			if (GetSourceVideoFormat() == vfPAL)
				result += wxT(" (PAL)");
			else if (GetSourceVideoFormat() == vfNTSC)
				result += wxT(" (NTSC)");
			if (round(GetSourceAspectRatio()*100) == 133 || round(GetSourceAspectRatio()*100) == 136)
				result += wxT(", 4:3");
			else if (round(GetSourceAspectRatio()*100) == 178)
				result += wxT(", 16:9");
			else
				result += wxString::Format(wxT(", 1:%0.2f"), GetSourceAspectRatio());
		}
		break;
	case stAUDIO:
		if (m_sourceChannelNumber > 0) {
			result += wxT(", ");
			if (m_sourceChannelNumber == 1)
				result += _("mono");
			else if (m_sourceChannelNumber == 2)
				result += _("stereo");
			else if (m_sourceChannelNumber == 6)
				result += wxT("5:1");
			else
				result += wxString::Format(_("%d channels"), m_sourceChannelNumber);
		}
		if (m_sourceSampleRate)
			result += wxT(", ") + wxString::Format(_("%d Hz"), m_sourceSampleRate);
		break;
	default:
		break;
	}
	return result;
}

////////////////////////////////// Vob //////////////////////////////////////
Vob::Vob() {
	Init();
}

Vob::Vob(const wxString& filename) {
	Init();
	SetFilename(filename);
}

Vob::Vob(Menu* menu) {
	Init(menu);
}

Vob::Vob(Slideshow* slideshow) {
	Init(NULL, slideshow);
}

void Vob::Init(Menu* menu, Slideshow* slideshow) {
	m_pause = 0;
	m_startTime = 0;
	m_recordingTime = -1;
	m_menu = menu;
	m_slideshow = slideshow;
	m_doNotTranscode = false;
	m_interlaced = false;
	m_firstField = ffAUTO;
	m_keepAspectRatio = true;
	for (int i = 0; i < 4; i++) {
		m_pad.push_back(0);
		m_crop.push_back(0);
	}
	m_fadeIn = 0;
	m_fadeOut = 0;
}

Vob::Vob(const Vob& vob): m_pad(vob.m_pad), m_crop(vob.m_crop) {
	m_filename = vob.m_filename;
	m_audioFilenames = vob.m_audioFilenames;
	for (unsigned int i = 0; i<vob.m_subtitles.size(); i++)
		m_subtitles.Add(new TextSub(*vob.m_subtitles[i]));
	m_tmpFilename = vob.m_tmpFilename;
	m_pause = vob.m_pause;
	m_menu = vob.m_menu;
	m_slideshow = vob.m_slideshow != NULL ? new Slideshow(*vob.m_slideshow) : NULL;
	m_duration = vob.m_duration;
	m_startTime = vob.m_startTime;
	m_recordingTime = vob.m_recordingTime;
	for (unsigned int i = 0; i<vob.m_streams.size(); i++)
		m_streams.Add(new Stream(*vob.m_streams[i]));
	VECTOR_COPY(vob.m_cells, m_cells, Cell);
	m_interlaced = vob.m_interlaced;
	m_firstField = vob.m_firstField;
	m_keepAspectRatio = vob.m_keepAspectRatio;
	m_doNotTranscode = vob.m_doNotTranscode;
	m_fadeIn = vob.m_fadeIn;
	m_fadeOut = vob.m_fadeOut;
	m_videoFilters = vob.m_videoFilters;
}

Vob::~Vob() {
	if (m_menu)
		delete m_menu;
	if (m_slideshow)
		delete m_slideshow;
	WX_CLEAR_ARRAY(m_subtitles)
	WX_CLEAR_ARRAY(m_streams)
	VECTOR_CLEAR(m_cells, Cell)
}

bool Vob::SetFilename(const wxString& value) {
	m_filename = value;
	wxFfmpegMediaDecoder ffmpeg;
	bool ok = m_filename.length() && ffmpeg.Load(m_filename);
	m_duration = ok && ffmpeg.GetDuration() > 0 ? ffmpeg.GetDuration() : 0;
	if (m_streams.GetCount() - m_audioFilenames.GetCount() > 0)
		m_streams.RemoveAt(0, m_streams.GetCount() - m_audioFilenames.GetCount());
	for (unsigned int stIdx = 0; stIdx < ffmpeg.GetStreamCount(); stIdx++) {
		Stream* stream = new Stream(ffmpeg.GetStreamType(stIdx), ffmpeg.GetCodecName(stIdx));
		if (stream->GetType() == stAUDIO) {
			stream->SetSourceChannelNumber(ffmpeg.GetChannelNumber(stIdx));
			stream->SetSourceSampleRate(ffmpeg.GetSampleRate(stIdx));
			stream->SetSourceBitrate(ffmpeg.GetBitrate(stIdx));
		} else if (stream->GetType() == stVIDEO) {
			stream->SetSourceVideoSize(ffmpeg.GetVideoSize());
			stream->SetSourceBitrate(ffmpeg.GetBitrate(stIdx));
			stream->SetSourceAspectRatio(ffmpeg.GetFrameAspectRatio());
			stream->SetSourceFps(ffmpeg.GetFps());
		}
		m_streams.Insert(stream, stIdx);
	}
	return ok;
}

bool Vob::HasAudio() {
	if (GetAudioFilenames().GetCount() > 0)
		return true;
	for (int i = 0; i < (int)GetStreams().GetCount(); i++) {
		if (GetStreams()[i]->GetType() == stAUDIO)
			return true;
	}
	return false;
}

/** Returns count of audio streams */
unsigned int Vob::GetAudioStreamCount() {
	unsigned int cnt = 0;
	for (StreamArray::iterator it = m_streams.begin(); it != m_streams.end(); it++)
		if ((*it)->GetType() == stAUDIO)
			cnt++;
	return cnt;
}

/** Returns count of subtitle streams */
unsigned int Vob::GetSubtitleStreamsCount() {
	unsigned int cnt = 0;
	if (!m_menu) {
		for (StreamArray::iterator it = m_streams.begin(); it != m_streams.end(); it++)
			if ((*it)->GetType() == stSUBTITLE)
				cnt++;
		cnt += GetSubtitles().GetCount();
	} else
		cnt = 1;
	return cnt;
}

/** Adds the given audio file to the vob */
bool Vob::AddAudioFile(wxString filename) {
	wxFfmpegMediaDecoder ffmpeg;
	if (!ffmpeg.Load(filename))
		return false;
	m_audioFilenames.Add(filename);
	if (ffmpeg.GetStreamCount() > 0) {
		for (unsigned int i = 0; i < ffmpeg.GetStreamCount(); i++) {
			if (ffmpeg.GetStreamType(i) == stAUDIO) {
				Stream* stream = new Stream(ffmpeg.GetStreamType(i), ffmpeg.GetCodecName(i));
				stream->SetSourceChannelNumber(ffmpeg.GetChannelNumber(i));
				stream->SetSourceSampleRate(ffmpeg.GetSampleRate(i));
				m_streams.Add(stream);
				break;
			}
		}
	} else
		m_streams.Add(new Stream(stAUDIO, wxT("unknown")));
	if (GetFilename().length() == 0) // menu or slideshow
		m_duration = ffmpeg.GetDuration();
	return true;
}

/** Removes audio file with given index from the vob */
void Vob::RemoveAudioFile(int index) {
	m_streams.RemoveAt(m_streams.GetCount() - m_audioFilenames.GetCount() + index);
	m_audioFilenames.RemoveAt(index);
}

/** Sets chapter list */
void Vob::SetChapters(const wxString& value) {
	VECTOR_CLEAR(m_cells, Cell);
	wxStringTokenizer tkz(value, _T(","));
	while (tkz.HasMoreTokens()) {
		wxString token = tkz.GetNextToken().Strip(wxString::both);
		m_cells.push_back(new Cell(String2Time(token)));
	}
}

/** Returns chapter list */
wxString Vob::GetChapters() {
	wxString result;
	for (vector<Cell*>::iterator it = m_cells.begin(); it != m_cells.end(); it++)
		if ((*it)->IsChapter()) {
			if (result.length())
				result += wxT(",");
			result += (*it)->GetStartStr();
		}
	return result;
}

/** Updates pad values to keep aspect ratio*/
void Vob::UpdatePad(AspectRatio aspectRatio) {
	int padx = 0;
	int pady = 0;
	Stream* videoSt = GetVideoStream();
	VideoFormat videoFormat = videoSt != NULL ? videoSt->GetVideoFormat() : vfCOPY;
	if (GetKeepAspectRatio() &&  CalcPad(padx, pady, videoFormat, aspectRatio, m_crop)) {
		m_pad[0] = m_pad[1] = padx;
		m_pad[2] = m_pad[3] = pady;
	}
}

/** Calculates pad values to keep aspect ratio*/
bool Vob::CalcPad(int& padx, int& pady, VideoFormat videoFormat, AspectRatio aspectRatio, const vector<int>& crop) {
	Stream* videoSt = GetVideoStream();
	if (videoSt != NULL && videoSt->GetSourceAspectRatio() <= 0)
		return false;
	padx = 0;
	pady = 0;
	if (videoFormat != vfCOPY && aspectRatio != arAUTO) {
		float oldAspect = videoSt->GetSourceAspectRatio();
		wxSize oldFrameSize = videoSt->GetSourceVideoSize();
		int cropX = crop[0] + crop[1];
		int cropY = crop[2] + crop[3];
		if (cropX + cropY > 0 && oldFrameSize.x > cropX && oldFrameSize.y > cropY)
			oldAspect *= ((float) oldFrameSize.y)/oldFrameSize.x*(oldFrameSize.x - cropX)/(oldFrameSize.y - cropY);
		float aspect = GetFrameAspectRatio(videoFormat, aspectRatio);
		wxSize frameSize = GetFrameSize(videoFormat);
		if (aspect > oldAspect)
			padx = lround(((double)frameSize.x)*(1.0 - oldAspect/aspect)/2);
		else if (aspect < oldAspect)
			pady = lround(((double)frameSize.x)*(1.0 - aspect/oldAspect)/2);
	}
	return true;
}

/** Returns all video filters (incl. crop, pad, fade-in and fade-out) */
wxString Vob::GetAllVideoFilters() {
	wxString result;
	
	// add crop & pad filters
	VideoFormat videoFormat = vfCOPY;
	for (unsigned int stIdx = 0; stIdx < GetStreams().GetCount(); stIdx++) {
		Stream* stream = GetStreams()[stIdx];
		if (stream->GetType() == stVIDEO && stream->GetVideoFormat() != vfNONE) {
			videoFormat = stream->GetVideoFormat();
			break;
		}
	}
	if (videoFormat != vfCOPY) {
		wxSize frameSize = GetFrameSize(videoFormat);
		bool doCrop = m_crop.size() == 4 && m_crop[0] + m_crop[1] + m_crop[2] + m_crop[3] > 0;
		bool doPad = m_pad.size() == 4 && m_pad[0] + m_pad[1] + m_pad[2] + m_pad[3] > 0;
		if (doCrop) {
			if (result.length())
				result += wxT(',');
			result += wxString::Format(wxT("crop=iw-%d:ih-%d:%d:%d"),
					m_crop[0] + m_crop[1], m_crop[2] + m_crop[3], m_crop[0], m_crop[2]);
		}
		if (doCrop || doPad) {
			if (result.length())
				result += wxT(',');
			result += wxString::Format(wxT("scale=%d:%d"),
					frameSize.GetWidth() - m_pad[0] - m_pad[1], frameSize.GetHeight() - m_pad[2] - m_pad[3]);
		}
		if (doPad) {
			result += wxString::Format(wxT(",pad=%d:%d:%d:%d"),
					frameSize.GetWidth(), frameSize.GetHeight(), m_pad[0], m_pad[2]);
		}
	}
	
	// add fade-in and fade-out filters
	if (GetFadeIn() > 0) {
		if (result.length())
			result += wxT(',');
		double fps = isNTSC(videoFormat) ? 30000/1001 : 25;
		long startFrame = GetStartTime() * fps;
		result += wxString::Format(wxT("fade=in:%d:%d"), startFrame, (int) (GetFadeIn() * fps));
	}
	if (GetFadeOut() > 0) {
		if (result.length())
			result += wxT(',');
		double fps = isNTSC(videoFormat) ? 30000/1001 : 25;
		double endTime = GetRecordingTime() >= 0 ? GetRecordingTime() : GetDuration();
		long startFrame = (endTime - GetFadeOut()) * fps;
		result += wxString::Format(wxT("fade=out:%d:%d"), startFrame, (int) (GetFadeOut() * fps));
	}
	
	// add custom filters 
	if (GetVideoFilters().length()) {
		if (result.length())
			result += wxT(',');
		result += GetVideoFilters();
	}
	return result;
}

/** Returns video stream parameters */
Stream* Vob::GetVideoStream() {
	for (int i = 0; i < (int)GetStreams().GetCount(); i++)
		if (GetStreams()[i]->GetType() == stVIDEO)
			return GetStreams()[i];
	return NULL;
}

void ReplaceCallLastMenu(wxString& commands, DVD* dvd) {
	if (dvd->GetLastMenuRegister() == -1)
		return;
	wxString cmd = wxT("call menu;");
	int stPos = commands.Index(cmd);
	if (stPos >= 0)
		commands = commands.Mid(0, stPos) + wxString::Format(wxT("g%d = 0;"), dvd->GetLastMenuRegister())
			+ commands.Mid(stPos);
	cmd = wxT("call last menu");
	stPos = commands.Find(cmd);
	if (stPos >= 0)
		commands = commands.Mid(0, stPos) + wxT("call menu") + commands.Mid(stPos + cmd.length());
}

wxSvgXmlNode* Vob::GetXML(DVDFileType type, DVD* dvd) {
	wxSvgXmlNode* node = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("vob"));
	wxString fname = GetFilename();
	if (type == DVDAUTHOR_XML) {
		if (GetTmpFilename().length())
			fname = GetTmpFilename();
	} else {
		wxFileName filename(fname);
		if (filename.GetPath() == dvd->GetPath(false))
			fname = filename.GetFullName();
		else if (dvd->GetPath(false).length() > 0 && filename.GetPath().StartsWith(dvd->GetPath(false)))
			fname = filename.GetPath().substr(dvd->GetPath(false).length() + 1) + wxFILE_SEP_PATH
					+  filename.GetFullName();
	}
	if (fname.length())
		node->AddProperty(wxT("file"), fname);
	
	if (type == DVDSTYLER_XML) {
		int stIdx = 0;
		for (; stIdx < (int)(GetStreams().GetCount() - GetAudioFilenames().GetCount()); stIdx++) {
			Stream* stream = GetStreams()[stIdx];
			wxString streamName;
			switch (stream->GetType()) {
			case stVIDEO:
				streamName = wxT("video");
				break;
			case stAUDIO:
				streamName = wxT("audio");
				break;
			case stSUBTITLE:
				streamName = wxT("subtitle");
				break;
			default:
				break;
			}
			if (streamName.length() == 0)
				continue;
			wxSvgXmlNode* streamNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, streamName);
			streamNode->AddProperty(wxT("format"), wxString::Format(wxT("%d"), stream->GetDestinationFormat()));
			if (stream->GetType() == stVIDEO) {
				if (GetInterlaced()) {
					streamNode->AddProperty(wxT("interlaced"), wxT("1"));
					streamNode->AddProperty(wxT("firstField"), wxString::Format(wxT("%d"), GetFirstField()));
				}
				if (!GetKeepAspectRatio() && stream->GetVideoFormat() > vfCOPY)
					streamNode->AddProperty(wxT("keepAspect"), wxT("0"));
			} else if (stream->GetType() == stAUDIO) {
				if (stream->GetAudioVolume() != 256)
					streamNode->AddProperty(wxT("volume"), wxString::Format(wxT("%d"), stream->GetAudioVolume()));
			}
			node->AddChild(streamNode);
		}
		for (int i = 0; i < (int)GetAudioFilenames().GetCount(); i++) {
			Stream* stream = GetStreams()[stIdx++];
			wxSvgXmlNode* audioNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("audio"));
			audioNode->AddChild(new wxSvgXmlNode(wxSVGXML_TEXT_NODE, wxEmptyString, GetAudioFilenames()[i]));
			audioNode->AddProperty(wxT("format"), wxString::Format(wxT("%d"), stream->GetDestinationFormat()));
			if (stream->GetAudioVolume() != 256)
				audioNode->AddProperty(wxT("volume"), wxString::Format(wxT("%d"), stream->GetAudioVolume()));
			node->AddChild(audioNode);
		}
		for (int i = 0;  i < (int) GetSubtitles().GetCount(); i++) {
			node->AddChild(GetSubtitles()[i]->GetXML(type));
		}
		if (GetDoNotTranscode())
			node->AddProperty(wxT("doNotTranscode"), wxT("1"));
		if (!m_keepAspectRatio && m_pad[0] + m_pad[1] + m_pad[2] + m_pad[3] > 0) {
			wxSvgXmlNode* padNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("pad"));
			if (m_pad[0] > 0)
				padNode->AddProperty(wxT("left"), wxString::Format(wxT("%d"), m_pad[0]));
			if (m_pad[1] > 0)
				padNode->AddProperty(wxT("right"), wxString::Format(wxT("%d"), m_pad[1]));
			if (m_pad[2] > 0)
				padNode->AddProperty(wxT("top"), wxString::Format(wxT("%d"), m_pad[2]));
			if (m_pad[3] > 0)
				padNode->AddProperty(wxT("bottom"), wxString::Format(wxT("%d"), m_pad[3]));
			node->AddChild(padNode);
		}
		if (m_crop[0] + m_crop[1] + m_crop[2] + m_crop[3] > 0) {
			wxSvgXmlNode* cropNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("crop"));
			if (m_crop[0] > 0)
				cropNode->AddProperty(wxT("left"), wxString::Format(wxT("%d"), m_crop[0]));
			if (m_crop[1] > 0)
				cropNode->AddProperty(wxT("right"), wxString::Format(wxT("%d"), m_crop[1]));
			if (m_crop[2] > 0)
				cropNode->AddProperty(wxT("top"), wxString::Format(wxT("%d"), m_crop[2]));
			if (m_crop[3] > 0)
				cropNode->AddProperty(wxT("bottom"), wxString::Format(wxT("%d"), m_crop[3]));
			node->AddChild(cropNode);
		}
		// start & recording time
		if (GetStartTime() > 0)
			node->AddProperty(wxT("startTime"), wxString::Format(wxT("%f"), GetStartTime()));
		if (GetRecordingTime() > 0)
			node->AddProperty(wxT("recordingTime"), wxString::Format(wxT("%f"), GetRecordingTime()));
		// video filters
		if (GetFadeIn() > 0)
			node->AddProperty(wxT("fadeIn"), wxString::Format(wxT("%d"), GetFadeIn()));
		if (GetFadeOut() > 0)
			node->AddProperty(wxT("fadeOut"), wxString::Format(wxT("%d"), GetFadeOut()));
		if (GetVideoFilters().length() > 0)
			node->AddProperty(wxT("videoFilters"), GetVideoFilters());
	}
	
	// chapters/cells
	bool cells = false;
	for (vector<Cell*>::iterator it = m_cells.begin(); it != m_cells.end(); it++) {
		if (!(*it)->IsChapter() || (*it)->GetEnd() >= 0 || (*it)->GetPause() != 0
				|| (*it)->GetCommands().length() > 0) {
			cells = true;
			break;
		}
	}
	if (cells) {
		for (vector<Cell*>::iterator it = m_cells.begin(); it != m_cells.end(); it++) {
			Cell* cell = *it;
			if (type == DVDAUTHOR_XML && cell->GetCommands().length() > 0) {
				wxString cmd = cell->GetCommands();
				ReplaceCallLastMenu(cmd, dvd);
				wxStringTokenizer tkz(cmd, wxT(";"));
				while (tkz.HasMoreTokens()) {
					wxString token = tkz.GetNextToken().Strip(wxString::both) + wxT(";");
					if (tkz.HasMoreTokens()) {
						Cell tmpCell(cell->GetStart(), false);
						tmpCell.SetCommands(token);
						node->AddChild(tmpCell.GetXML(type));
					} else {
						Cell tmpCell(*cell);
						tmpCell.SetCommands(token);
						node->AddChild(tmpCell.GetXML(type));
					}
				}
			} else
				node->AddChild(cell->GetXML(type));
		}
	} else if (m_cells.size() > 0) {
		node->AddProperty(wxT("chapters"), GetChapters());
		if (GetPause() != 0)
			node->AddProperty(wxT("pause"), GetPause() > 0 ? wxString::Format(wxT("%d"), GetPause()) : wxT("inf"));
	}
	
	if (GetMenu() && type == DVDSTYLER_XML)
		node->AddChild(GetMenu()->GetXML(type));
	
	if (GetSlideshow()) {
		if (type == DVDSTYLER_XML)
			node->AddChild(GetSlideshow()->GetXML(type));
		else if (type == DVDAUTHOR_XML && m_cells.size() == 0) {
			wxString chapters;
			int t = 1;
			for (unsigned i=1; i<GetSlideshow()->GetCount()/5; i++) {
				t += GetSlideshow()->GetDuration()*5;
				int h = t/3600;
				int m = (t%3600)/60;
				int s = t%60;
				if (chapters.length())
					chapters += wxT(",");
				chapters += wxString::Format(wxT("%d:%2d:%2d.1"), h, m, s);
			}
			node->AddProperty(wxT("chapters"), chapters);
		}
	}
	
	return node;
}

bool Vob::PutXML(wxSvgXmlNode* node, DVD* dvd, int tsi, int pgci, bool menu) {
	wxString val;
	long lval;
	double dval;

	node->GetPropVal(wxT("file"), &val);
	if (val.length() > 0) {
		wxFileName fname(val);
		if (fname.IsRelative())
			val = dvd->GetPath() + fname.GetFullPath();
		else if (!wxFileExists(val) && wxFileExists(dvd->GetPath() + fname.GetFullName()))
			val = dvd->GetPath() + fname.GetFullName();
		SetFilename(val);
	}
	int stIdx = 0;
	wxSvgXmlNode* child = node->GetChildren();
	while (child) {
		if (child->GetName() == wxT("video")) {
			if (child->GetChildren() != NULL && child->GetChildren()->GetContent().length() > 0) {
				val = child->GetChildren()->GetContent();
				wxFileName fname(val);
				if (fname.IsRelative())
					val = dvd->GetPath() + fname.GetFullPath();
				else if (!wxFileExists(val) && wxFileExists(dvd->GetPath() + fname.GetFullName()))
					val = dvd->GetPath() + fname.GetFullName();
				SetFilename(val);
			}
			
			if (child->GetPropVal(wxT("format"), &val) && val.length() > 0 && val.ToLong(&lval)) {
				if ((int)m_streams.GetCount() <= stIdx || m_streams[stIdx]->GetType() != stVIDEO)
					stIdx = 0;
				while ((int)m_streams.GetCount() > stIdx) {
					if (m_streams[stIdx]->GetType() == stVIDEO) {
						m_streams[stIdx++]->SetDestinationFormat(lval);
						break;
					}
					stIdx++;
				}
			}
			if (child->GetPropVal(wxT("interlaced"), &val) && val == wxT("1")) {
				m_interlaced = true;
				if (child->GetPropVal(wxT("firstField"), &val) && val.length() > 0 && val.ToLong(&lval))
					m_firstField = (FirstField) lval;
			}
			if (child->GetPropVal(wxT("keepAspect"), &val) && val == wxT("0"))
				m_keepAspectRatio = false;
		} else if (child->GetName() == wxT("audio")) {
			if (child->GetChildren() != NULL && child->GetChildren()->GetContent().length() > 0) {
				val = child->GetChildren()->GetContent();
				wxFileName fname(val);
				if (fname.IsRelative())
					val = dvd->GetPath() + fname.GetFullPath();
				else if (!wxFileExists(val) && wxFileExists(dvd->GetPath() + fname.GetFullName()))
					val = dvd->GetPath() + fname.GetFullName();
				if (AddAudioFile(val)) {
					stIdx = m_streams.GetCount() - 1;
					if (child->GetPropVal(wxT("format"), &val) && val.length() > 0 && val.ToLong(&lval))
						m_streams[stIdx]->SetDestinationFormat(lval);
					if (child->GetPropVal(wxT("volume"), &val) && val.length() > 0 && val.ToLong(&lval))
						m_streams[stIdx]->SetAudioVolume(lval);
					stIdx++;
				}
			} else if (child->GetPropVal(wxT("format"), &val) && val.length() > 0 && val.ToLong(&lval)) {
				if ((int)m_streams.GetCount() <= stIdx || m_streams[stIdx]->GetType() != stAUDIO)
					stIdx = 0;
				while ((int)m_streams.GetCount() > stIdx) {
					if (m_streams[stIdx]->GetType() == stAUDIO) {
						m_streams[stIdx]->SetDestinationFormat(lval);
						if (child->GetPropVal(wxT("volume"), &val) && val.length() > 0 && val.ToLong(&lval))
							m_streams[stIdx]->SetAudioVolume(lval);
						stIdx++;
						break;
					}
					stIdx++;
				}
			}
		} else if (child->GetName() == wxT("cell")) {
			Cell* cell = new Cell;
			cell->PutXML(child);
			m_cells.push_back(cell);
		} else if (child->GetName() == wxT("pad")) {
			m_pad.clear();
			m_pad.push_back(child->GetPropVal(wxT("left"), &val) && val.ToLong(&lval) ? lval : 0);
			m_pad.push_back(child->GetPropVal(wxT("right"), &val) && val.ToLong(&lval) ? lval : 0);
			m_pad.push_back(child->GetPropVal(wxT("top"), &val) && val.ToLong(&lval) ? lval : 0);
			m_pad.push_back(child->GetPropVal(wxT("bottom"), &val) && val.ToLong(&lval) ? lval : 0);
		} else if (child->GetName() == wxT("crop")) {
			m_crop.clear();
			m_crop.push_back(child->GetPropVal(wxT("left"), &val) && val.ToLong(&lval) ? lval : 0);
			m_crop.push_back(child->GetPropVal(wxT("right"), &val) && val.ToLong(&lval) ? lval : 0);
			m_crop.push_back(child->GetPropVal(wxT("top"), &val) && val.ToLong(&lval) ? lval : 0);
			m_crop.push_back(child->GetPropVal(wxT("bottom"), &val) && val.ToLong(&lval) ? lval : 0);
		} else if (child->GetName() == wxT("textsub")) {
			TextSub* textsub = new TextSub;
			textsub->PutXML(child);
			GetSubtitles().Add(textsub);
		} else if (child->GetName() == wxT("menu")) {
			m_menu = new Menu(dvd, tsi, pgci);
			if (!m_menu->PutXML(child))
				return false;
		} else if (child->GetName() == wxT("slideshow"))
			m_slideshow = new Slideshow(child);
		child = child->GetNext();
	}
	if (node->GetPropVal(wxT("pause"), &val)) {
		if (val == wxT("inf"))
			m_pause = -1;
		else if (val.ToLong(&lval))
			m_pause = int(lval);
	}
	m_startTime = node->GetPropVal(wxT("startTime"), &val) && val.ToDouble(&dval) ? dval : 0;
	m_recordingTime = node->GetPropVal(wxT("recordingTime"), &val) && val.ToDouble(&dval) ? dval : -1;
	m_fadeIn = node->GetPropVal(wxT("fadeIn"), &val) && val.ToLong(&lval) ? lval : 0;
	m_fadeOut = node->GetPropVal(wxT("fadeOut"), &val) && val.ToLong(&lval) ? lval : 0;
	m_videoFilters = node->GetPropVal(wxT("videoFilters"), wxT(""));
		
	if (node->GetPropVal(wxT("doNotTranscode"), &val) && val == wxT("1"))
		SetDoNotTranscode(true);
	if (node->GetPropVal(wxT("chapters"), &val))
		SetChapters(val);
	
	return true;
}

unsigned int Vob::GetFileSize(wxString filename) {
	return wxFile(filename).Length()/1024;
}

int Vob::GetSize(DVD* dvd, bool generated) {
	if (generated) {
		if (GetTmpFilename().length())
			return GetFileSize(GetTmpFilename());
		else if (GetFilename().length())
			return GetFileSize(GetFilename());
		return 0;
	}

	long size = 0;
	int stIdx = 0;
	if (GetMenu()) {
		double duration = m_duration;
		if (duration < 1)
			duration = s_config.GetMenuFrameCount()/(GetMenu()->GetVideoFormat() == vfPAL ? 25 : 30);
		size += (long)(duration*s_config.GetMenuVideoBitrate()/8/3);
	} else if (GetSlideshow()) {
		size += GetSlideshow()->GetDuration()*GetSlideshow()->GetCount()*s_config.GetSlideshowVideoBitrate()/8/3;
	} else {
		if (m_doNotTranscode) {
			size += GetFileSize(GetFilename());
		} else {
			if (GetFilename().length()) {
				int streamsCount = (int)m_streams.GetCount() - m_audioFilenames.GetCount();
				bool copyVideo = false;
				long audioSize = 0;
				for (; stIdx < streamsCount; stIdx++) {
					Stream* stream = m_streams[stIdx];
					switch (stream->GetType()) {
					case stVIDEO:
						if (stream->GetVideoFormat() == vfCOPY)
							copyVideo = true;
						else
							size += (long) (GetDuration()*dvd->GetVideoBitrate()/8);
						break;
					case stAUDIO: {
						int srcBitrate = stream->GetSourceBitrate() > 0 ? stream->GetSourceBitrate() : 64000;
						audioSize += (long) (GetDuration()*srcBitrate/8/1024);
						if (stream->GetAudioFormat() == afCOPY)
							size += (long) (GetDuration()*srcBitrate/8/1024);
						else if (stream->GetAudioFormat() != afNONE)
							size += (long) (GetDuration()*s_config.GetAudioBitrate()/8);
						break;
					}
					case stSUBTITLE:
					case stUNKNOWN:
						break;
					}
				}
				if (copyVideo)
					size += GetFileSize(GetFilename()) - audioSize;
			}
		}
	}
	for (unsigned int i=0; i<GetAudioFilenames().Count(); i++) {
		Stream* stream = m_streams[stIdx++];
		size += GetFileSize(GetAudioFilenames()[i]);
		if (stream->GetAudioFormat() == afCOPY)
			size += GetFileSize(GetAudioFilenames()[i]);
		else
			size += (long) (GetDuration()*s_config.GetAudioBitrate()/8);
	}
	for (unsigned int i=0; i<GetSubtitles().Count(); i++)
		size += GetFileSize(GetSubtitles()[i]->GetFilename());
	return size;
}

int Vob::GetRequiredSize(DVD* dvd, Cache* cache) {
	int size = GetSize(dvd);
	if (GetMenu() || GetSlideshow() || GetSubtitles().Count() > 0
			 || ((!GetDoNotTranscode() || GetAudioFilenames().Count() > 0)
					 && cache->Find(this, dvd, false).length() == 0))
		size = size * 2;
	return size;
}

Vob* Vob::CreateEmptyVob(VideoFormat videoFormat, AudioFormat audioFormat) {
	wxString filename= wxT("empty_pal_mp2.mpg");
	if (videoFormat == vfNTSC) {
		filename = audioFormat == afMP2 ? wxT("empty_ntsc_mp2.mpg") : wxT("empty_ntsc_ac3.mpg");
	} else if (audioFormat != afMP2) {
		filename = wxT("empty_pal_ac3.mpg");
	}
	return new Vob(DATA_FILE(filename));
}

////////////////////////////////// Pgc //////////////////////////////////////

wxString Pgc::GetEntriesAsString() {
	wxString result;
	for (StringSet::const_iterator entry = m_entries.begin(); entry != m_entries.end(); entry++) {
		if (result.length())
			result += wxT(",");
		result += *entry;
	}
	return result;
}

void Pgc::RemoveEntries(const StringSet& entries) {
	for (StringSet::const_iterator entry = entries.begin(); entry != entries.end(); entry++) {
		m_entries.erase(*entry);
	}
}

Menu* Pgc::GetMenu() {
	for (int i = 0; i < (int) GetVobs().GetCount(); i++)
		if (GetVobs()[i]->GetMenu())
			return GetVobs()[i]->GetMenu();
	return NULL;
}

Slideshow* Pgc::GetSlideshow() {
	for (int i = 0; i < (int) GetVobs().GetCount(); i++)
		if (GetVobs()[i]->GetSlideshow())
			return GetVobs()[i]->GetSlideshow();
	return NULL;
}

unsigned int Pgc::GetChapterCount() {
	int result = 1;
	for (int vi = 0; vi < (int) GetVobs().Count(); vi++) {
		Vob& vob = *GetVobs()[vi];
		if (vob.GetSlideshow()) {
			result += vob.GetSlideshow()->GetCount() / 5;
		} else {
			wxStringTokenizer tkz(vob.GetChapters(), _T(","));
			while (tkz.HasMoreTokens()) {
				wxString token = tkz.GetNextToken();
				if (token != _T("0") || vi > 0)
					result++;
			}
		}
	}
	return result;
}

/** Returns URI of video frame */
wxString Pgc::GetVideoFrameUri(int chapter, long position) {
	if (GetVobs().size() == 0)
		return wxT("");
	wxString uri;
	if (position == -1 && chapter > 0) {
		for (unsigned int vobi = 0; vobi < m_vobs.size() && chapter > 0; vobi++) {
			Vob* vob = GetVobs()[vobi];
			uri = vob->GetFilename();
			for (vector<Cell*>::iterator it = vob->GetCells().begin();
					it != vob->GetCells().end() && chapter > 0; it++) {
				if ((*it)->IsChapter()) {
					position = lround(vob->GetStartTime() * 1000) + (*it)->GetStart();
					chapter--;
				}
			}
		}
	} else {
		Vob* vob = GetVobs()[0];
		uri = vob->GetFilename();
		if (position == -1 && chapter == 0) {
			double dpos = vob->GetDuration() < 3600 ? vob->GetDuration() * 0.05 : 180;
			position = lround((vob->GetStartTime() + dpos) * 1000);
		}
	}
	if (position >= 0 && uri.length() > 0)
		uri += wxT('#') + wxString::Format(wxT("%d"), position);
	return uri;
}

wxSvgXmlNode* Pgc::GetXML(DVDFileType type, DVD* dvd, PgcArray& pgcs, wxString nextTitle) {
	wxSvgXmlNode* node = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, wxT("pgc"));
	if (m_entries.size() > 0)
		node->AddProperty(wxT("entry"), GetEntriesAsString());
	
	if (m_palette.length())
		node->AddProperty(wxT("palette"), m_palette);
	
	if (GetMenu() && type == DVDAUTHOR_XML)
		GetMenu()->GetXML(type, node);
	
	if (GetSlideshow() && type == DVDAUTHOR_XML)
		GetSlideshow()->GetXML(type, node);
	
	for (int i = 0; i < (int) GetVobs().GetCount(); i++)
		node->AddChild(GetVobs()[i]->GetXML(type, dvd));
	
	wxString preCommands = GetPreCommands();
	if (type == DVDAUTHOR_XML) {
		if (GetMenu()) {
			unsigned int pgci = 0;
			for (unsigned int i = 0; i < pgcs.size(); i++) {
				if (this == pgcs[i]) {
					pgci = i;
					break;
				}
			}
			int lastMenuReg = dvd->GetLastMenuRegister();
			if (lastMenuReg != -1) {
				bool vmgm = &pgcs == &dvd->GetVmgm();
				preCommands = wxString::Format(vmgm ? wxT("g%d=1%02d;") : wxT("g%d=%d;"), lastMenuReg, pgci + 1)
						+ preCommands;
				if (GetEntries().find(wxT("root")) != GetEntries().end() && dvd->HasCallLastMenu()) {
					wxString s;
					for (unsigned int i = 0; i < dvd->GetVmgm().size(); i++)
						s += wxString::Format(wxT("if (g%d==1%02d) jump vmgm menu %d;"), lastMenuReg, i + 1, i + 1);
					for (unsigned int i = 0; i < pgcs.size(); i++)
						if (i != pgci)
							s += wxString::Format(wxT("if (g%d==%d) jump menu %d;"), lastMenuReg, i + 1, i + 1);
					preCommands = s + preCommands;
				}
			}
			if (GetMenu()->GetRememberLastButton()) {
				int lastBtReg = GetMenu()->GetRememberLastButtonRegister();
				bool checkLastMenu = false;
				if (lastBtReg == -1) {
					lastBtReg = dvd->GetRememberLastButtonRegister();
					checkLastMenu = true;
				} else {
					for (int tsi = -1; tsi < (int) dvd->GetTitlesets().Count(); tsi++) {
						PgcArray& pgcs = dvd->GetPgcArray(tsi, true);
						for (int pgci = 0; pgci<(int)pgcs.Count(); pgci++) {
							Menu* menu2 = pgcs[pgci]->GetMenu();
							if (menu2 && menu2 != GetMenu() && menu2->GetRememberLastButtonRegister() == lastBtReg) {
								checkLastMenu = true;
								break;
							}
						}
						if (checkLastMenu)
							break;
					}
				}
				wxString expr1 = checkLastMenu ? wxString::Format(wxT("g%d==%d and "), lastMenuReg, pgci + 1) : wxT("");
				preCommands = wxString::Format(wxT("if (%sg%d>0) button = g%d; else button = 1024;%s"),
						expr1.c_str(), lastBtReg, lastBtReg, preCommands.c_str());
			}
		} else
			ReplaceCallLastMenu(preCommands, dvd);
	}
	if (preCommands.length())
		XmlWriteValue(node, wxT("pre"), preCommands);
	
	wxString postCommands = GetPostCommands();
	if (type == DVDAUTHOR_XML) {
		ReplaceCallLastMenu(postCommands, dvd);
		if (dvd->GetPlayAllRegister() != -1 && nextTitle.length()) {
			wxString g = wxT("g") + wxString::Format(wxT("%d"), dvd->GetPlayAllRegister());
			postCommands = wxT("if (") + g + wxT("==1) jump ") + nextTitle + wxT(";") + postCommands;
		}
	}
	if (postCommands.length())
		XmlWriteValue(node, wxT("post"), postCommands);
	
	return node;
}

bool Pgc::PutXML(wxSvgXmlNode* node, DVD* dvd, int tsi, int pgci, bool menu) {
	WX_CLEAR_ARRAY(GetVobs());

	wxString val;
	m_entries.clear();
	if (node->GetPropVal(wxT("entry"), &val)) {
		wxArrayString entryArray = wxStringTokenize(val, wxT(","));
		for (unsigned int entryIdx = 0; entryIdx < entryArray.Count(); entryIdx++)
			m_entries.insert(entryArray[entryIdx]);
	}

	node->GetPropVal(wxT("palette"), &m_palette);

	wxSvgXmlNode* child = node->GetChildren();
	Menu* menuObj = NULL; // old

	while (child) {
		if (child->GetName() == wxT("spumux")) {
			// get spunode (old)
			menuObj = new Menu(dvd, tsi, pgci);
			menuObj->PutXML(child);
		} else if (child->GetName() == wxT("vob")) {
			// get vob
			Vob* vob;
			if (menuObj)
				vob = new Vob(menuObj);
			else
				vob = new Vob(wxT(""));
			if (vob->PutXML(child, dvd, tsi, pgci, menu))
				GetVobs().Add(vob);
			else {
				delete vob;
				wxLogError(_("Can't load vob"));
				return false;
			}
		} else if (child->GetName() == wxT("pre")) // get pre commands
			SetPreCommands(child->GetChildren()->GetContent());
		else if (child->GetName() == wxT("post")) // get post commands
			SetPostCommands(child->GetChildren()->GetContent());
		child = child->GetNext();
	}
	return true;
}
