/////////////////////////////////////////////////////////////////////////////
// Name:        Menu.cpp
// Purpose:     The class to store a DVD Menu
// Author:      Alex Thuering
// Created:	04.11.2003
// RCS-ID:      $Id: Menu.cpp,v 1.83 2010/05/03 06:03:20 ntalex Exp $
// Copyright:   (c) Alex Thuering
// Licence:     GPL
/////////////////////////////////////////////////////////////////////////////

#include "Menu.h"
#include "MenuPalettes.h"
#include "Palette3D.h"
#include "DVD.h"
#include <wxVillaLib/SConv.h>
#include <wxVillaLib/utils.h>
#include <wxSVG/svg.h>
#include <wxSVG/SVGCanvas.h>
#include <wxSVGXML/svgxmlhelpr.h>
#include <wx/mstream.h>
#include <wx/filename.h>
#include <wx/sstream.h>

#define OBJECTS_DIR wxFindDataDirectory(_T("objects"))
#define BACKGROUNDS_DIR wxFindDataDirectory(_T("backgrounds"))

#define BUTTON_MAX_COLORS 4

#if !wxCHECK_VERSION(2,9,0)
#define wxBITMAP_TYPE_ANY -1
#endif

Menu::Menu(DVD* dvd, int tsi, VideoFormat videoFormat, AspectRatio aspectRatio) {
	m_dvd = dvd;
	m_tsi = tsi;
	m_videoFormat = videoFormat;
	m_aspectRatio = aspectRatio;
	m_startTime = _T("00:00:00.00");
	m_transpColour = wxColour(8, 8, 8);
	m_svg = CreateSVG(GetResolution().GetWidth(), GetResolution().GetHeight());
	SetBackgroundColour(wxColour(0, 0, 0));
}

Menu::~Menu() {
	delete m_svg;
	WX_CLEAR_ARRAY(m_objects)
}

wxSVGDocument* Menu::CreateSVG(int width, int height) {
	wxSVGDocument* svg = new wxSVGDocument;
	wxSVGSVGElement* svgElement = new wxSVGSVGElement;
	svgElement->SetWidth(width);
	svgElement->SetHeight(height);
	svg->AppendChild(svgElement);
	wxSVGDefsElement* defsElement = new wxSVGDefsElement;
	defsElement->SetId(wxT("defs"));
	svgElement->AppendChild(defsElement);
	wxSVGGElement* gElem = new wxSVGGElement;
	gElem->SetId(wxT("objects"));
	svgElement->AppendChild(gElem);
	gElem = new wxSVGGElement;
	gElem->SetId(wxT("buttons"));
	svgElement->AppendChild(gElem);
	return svg;
}

void Menu::SetVideoFormat(VideoFormat format) {
	VideoFormat oldFormat = m_videoFormat;
	m_videoFormat = format;
	// svg root element
	m_svg->GetRootElement()->SetWidth(GetResolution().GetWidth());
	m_svg->GetRootElement()->SetHeight(GetResolution().GetHeight());
	// background
	wxSVGElement* bgElement = m_svg->GetElementById(wxT("background"));
	if (bgElement) {
		if (bgElement->GetDtd() == wxSVG_RECT_ELEMENT) {
			((wxSVGRectElement*) bgElement)->SetWidth(GetResolution().GetWidth());
			((wxSVGRectElement*) bgElement)->SetHeight(GetResolution().GetHeight());
		} else if (bgElement->GetDtd() == wxSVG_IMAGE_ELEMENT) {
			((wxSVGImageElement*) bgElement)->SetWidth(GetResolution().GetWidth());
			((wxSVGImageElement*) bgElement)->SetHeight(GetResolution().GetHeight());
		}
	}
	// change object coordinates
	if (oldFormat != m_videoFormat) {
		double f = m_videoFormat == vfNTSC ? (double)480/576 : (double)576/480;
		for (unsigned int obji = 0; obji < m_objects.GetCount(); obji++) {
			MenuObject* obj = m_objects[obji];
			obj->SetY((int)(f*obj->GetY()));
			if (!obj->IsDefaultSize()) {
				obj->SetHeight((int)(f*obj->GetHeight()));
			}
		}
	}
	FixButtonCoordinates();
}

wxSize Menu::GetResolution() {
	if (m_videoFormat == vfNTSC)
		return wxSize(720, 480);
	return wxSize(720, 576); // vfPAL
}

void Menu::SetBackground(wxString fileName) {
	SetBackground(m_svg->GetRootElement(), fileName);
}
wxString Menu::GetBackground() {
	return GetBackground(m_svg->GetRootElement());
}

void Menu::SetBackground(wxSVGSVGElement* root, wxString fileName) {
	bool image = wxImage::FindHandler(fileName.AfterLast(wxT('.')).Lower(), wxBITMAP_TYPE_ANY) != NULL;
	// test if element exists
	wxSVGElement* bgElement = (wxSVGElement*) root->GetElementById(wxT("background"));
	if (!bgElement || (image && bgElement->GetDtd() != wxSVG_IMAGE_ELEMENT)
			|| (!image && bgElement->GetDtd() != wxSVG_VIDEO_ELEMENT)) {
		if (bgElement) // remove old
			bgElement->GetParent()->RemoveChild(bgElement);
		if (image) { // create image element
			bgElement = new wxSVGImageElement;
			((wxSVGImageElement*) bgElement)->SetWidth(GetResolution().GetWidth());
			((wxSVGImageElement*) bgElement)->SetHeight(GetResolution().GetHeight());
		} else { // create video element
			bgElement = new wxSVGVideoElement;
			((wxSVGVideoElement*) bgElement)->SetWidth(GetResolution().GetWidth());
			((wxSVGVideoElement*) bgElement)->SetHeight(GetResolution().GetHeight());
		}
		bgElement->SetId(wxT("background"));
		root->InsertChild(bgElement, root->GetChildren());
	}
	// set href
	if (image)
		((wxSVGImageElement*) bgElement)->SetHref(fileName);
	else
		((wxSVGVideoElement*) bgElement)->SetHref(fileName);
}

wxString Menu::GetBackground(wxSVGSVGElement* root) {
	wxSVGElement* bgElement = (wxSVGElement*) root->GetElementById(wxT("background"));
	if (bgElement) {
		if (bgElement->GetDtd() == wxSVG_IMAGE_ELEMENT)
			return ((wxSVGImageElement*) bgElement)->GetHref();
		else if (bgElement->GetDtd() == wxSVG_VIDEO_ELEMENT)
			return ((wxSVGVideoElement*) bgElement)->GetHref();
	}
	return wxT("");
}

void Menu::SetBackgroundColour(wxColour colour) {
	wxSVGElement* bgElement = m_svg->GetElementById(wxT("background"));
	wxSVGRectElement* bgRect = (wxSVGRectElement*) bgElement;
	if (!bgElement || bgElement->GetDtd() != wxSVG_RECT_ELEMENT) {
		if (bgElement)
			bgElement->GetParent()->RemoveChild(bgElement);
		bgRect = new wxSVGRectElement;
		bgRect->SetId(wxT("background"));
		bgRect->SetWidth(GetResolution().GetWidth());
		bgRect->SetHeight(GetResolution().GetHeight());
		m_svg->GetRootElement()->InsertChild(bgRect,
				m_svg->GetRootElement()->GetChildren());
	}
	bgRect->SetFill(wxSVGPaint(colour.Red(), colour.Green(), colour.Blue()));
}

wxColour Menu::GetBackgroundColour() {
	wxSVGRectElement* bgRect = (wxSVGRectElement*) m_svg->GetElementById(wxT("background"));
	if (bgRect && bgRect->GetDtd() == wxSVG_RECT_ELEMENT)
		return bgRect->GetFill().GetRGBColor();
	return wxColour(0, 0, 0);
}

bool Menu::HasVideoBackground() {
	wxSVGElement* bgElement = m_svg->GetElementById(wxT("background"));
	return bgElement && bgElement->GetDtd() == wxSVG_VIDEO_ELEMENT;
}

void Menu::SetTransparentColor() {
	m_transpColour = wxColour(0, 0, 0);
	wxImageHistogram h1, h2, h3;
	wxImage* images = GetImages();
	images[1].ComputeHistogram(h1);
	images[2].ComputeHistogram(h2);
	images[3].ComputeHistogram(h3);
	for (int i = 1; i < 30; i++) {
		m_transpColour = wxColour(i * 8, i * 8, i * 8);
		unsigned long colour = (i * 8 << 16) + (i * 8 << 8) + i * 8;
		if (h1.find(colour) == h1.end() && h2.find(colour) == h2.end() && h3.find(colour) == h3.end())
			break;
	}
}

bool Menu::IsDefElement(wxSVGElement* element) {
	if (!element || element->GetDtd() == wxSVG_SVG_ELEMENT)
		return false;
	if (element->GetDtd() == wxSVG_DEFS_ELEMENT)
		return true;
	return IsDefElement((wxSVGElement*) element->GetParent());
}

void Menu::RemoveChangeable(wxSVGElement* element, MenuObject* obj) {
	wxSVGElement* child = (wxSVGElement*) element->GetChildren();
	while (child) {
		wxSVGElement* elem = child;
		child = (wxSVGElement*) child->GetNext();
		if (elem->GetType() != wxSVGXML_ELEMENT_NODE)
			continue;
		// don't remove def elements
		if (IsDefElement(elem))
			continue;
		// check if elem changeable
		if (elem->GetId().length()) {
			// check if element has changeable attributes
			bool isChangeable = false;
			for (int i = 0; i < obj->GetObjectParamsCount(); i++) {
				MenuObjectParam* param = obj->GetObjectParam(i);
				if (param->element == elem->GetId() && param->changeable) {
					isChangeable = true;
					break;
				}
			}
			if (isChangeable) {
				// todo: don't remove if it has not changeable attributes, but
				// remove all changeable attributes: SetFill(none),SetStroke(none)
				elem->GetParent()->RemoveChild(elem);
				continue;
			}
		}
		// remove changeable children
		RemoveChangeable(elem, obj);
	}
}

bool Menu::RemoveNotChangeable(wxSVGElement* element, MenuObject* obj) {
	wxSVGElement* child = (wxSVGElement*) element->GetChildren();
	while (child) {
		wxSVGElement* elem = child;
		child = (wxSVGElement*) child->GetNext();
		if (elem->GetType() != wxSVGXML_ELEMENT_NODE) {
			elem->GetParent()->RemoveChild(elem);
			continue;
		}
		// don't remove def elements
		if (IsDefElement(elem))
			continue;
		// check if child changeable
		if (elem->GetId().length()) {
			// check if element has changeable attributes
			bool isChangeable = false;
			for (int i = 0; i < obj->GetObjectParamsCount(); i++) {
				MenuObjectParam* param = obj->GetObjectParam(i);
				if (param->element == elem->GetId() && param->changeable) {
					isChangeable = true;
					break;
				}
			}
			if (isChangeable) {
				// todo: remove not changeable attributes: SetFill(none),SetStroke(none)
				continue;
			}
		}
		// check if it has changeable children
		if (RemoveNotChangeable(elem, obj))
			elem->GetParent()->RemoveChild(elem);
	}
	return element->GetChildren() == NULL; // return if has no children
}

void CopyCachedImages(wxSVGDocument* doc, wxSVGElement* parent1,
		wxSVGElement* parent2) {
	wxSVGElement* elem1 = (wxSVGElement*) parent1->GetChildren();
	wxSVGElement* elem2 = (wxSVGElement*) parent2->GetChildren();
	while (elem1 && elem2) {
		if (elem1->GetType() == wxSVGXML_ELEMENT_NODE && elem1->GetDtd() == wxSVG_IMAGE_ELEMENT) {
			wxSVGImageElement* img1 = (wxSVGImageElement*) elem1;
			wxSVGImageElement* img2 = (wxSVGImageElement*) elem2;
			if (img1->GetHref().GetAnimVal().length() && img1->GetCanvasItem() != NULL)
				img2->SetCanvasItem(doc->GetCanvas()->CreateItem(img1));
		} else if (elem1->GetChildren())
			CopyCachedImages(doc, elem1, elem2);
		elem1 = (wxSVGElement*) elem1->GetNext();
		elem2 = (wxSVGElement*) elem2->GetNext();
	}
}

wxSVGSVGElement* Menu::GetSVGCopy(MenuDrawType drawType) {
	wxSVGSVGElement* svgNode = (wxSVGSVGElement*) m_svg->GetRoot()->CloneNode();
	CopyCachedImages(m_svg, (wxSVGElement*) m_svg->GetRoot(), svgNode);
	// remove selection rectangle
	wxSvgXmlElement* elem = svgNode->GetElementById(wxT("selection"));
	if (elem)
		elem->GetParent()->RemoveChild(elem);
	// remove safeTV rectangle
	elem = svgNode->GetElementById(wxT("safeTV"));
	if (elem)
		elem->GetParent()->RemoveChild(elem);
	// remove grid
	elem = svgNode->GetElementById(wxT("grid"));
	if (elem)
		elem->GetParent()->RemoveChild(elem);

	if (drawType == mdBACKGROUND) {
		// remove changeable elements of objects
		for (unsigned int i = 0; i < GetObjectsCount(); i++) {
			MenuObject* obj = GetObject(i);
			if (obj->IsButton()) {
				if (obj->GetAction().IsValid(m_dvd, m_tsi, 0, true)) {
					// remove dynamic part of button
					wxSVGElement* elem = (wxSVGElement*) svgNode->GetElementById(wxT("s_") + obj->GetId());
					if (elem)
						RemoveChangeable(elem, obj);
				} else {
					// remove buttons with invalid actions
					wxSVGElement* elem = (wxSVGElement*) svgNode->GetElementById(obj->GetId());
					if (elem && elem->GetParent())
						elem->GetParent()->RemoveChild(elem);
					elem = (wxSVGElement*) svgNode->GetElementById(wxT("s_") + obj->GetId());
					if (elem && elem->GetParent())
						elem->GetParent()->RemoveChild(elem);
				}
			}
		}
	} else if (drawType == mdBUTTONS_NORMAL || drawType == mdBUTTONS_HIGHLIGHTED || drawType == mdBUTTONS_SELECTED) {
		// remove background and static elements of objects
		wxSVGElement* bgElement = (wxSVGElement*) svgNode->GetElementById(wxT("background"));
		wxSVGRectElement* bgRect = (wxSVGRectElement*) bgElement;
		if (!bgElement || bgElement->GetDtd() != wxSVG_RECT_ELEMENT) {
			if (bgElement)
				bgElement->GetParent()->RemoveChild(bgElement);
			bgRect = new wxSVGRectElement;
			bgRect->SetId(wxT("background"));
			bgRect->SetWidth(GetResolution().GetWidth());
			bgRect->SetHeight(GetResolution().GetHeight());
			svgNode->InsertChild(bgRect, svgNode->GetChildren());
		}
		bgRect->SetFill(wxSVGPaint(m_transpColour.Red(),
				m_transpColour.Green(), m_transpColour.Blue()));
		for (unsigned int i = 0; i < GetObjectsCount(); i++) {
			MenuObject* obj = GetObject(i);
			if (obj->IsButton() && obj->GetAction().IsValid(m_dvd, m_tsi, 0, true)) {
				wxSVGSVGElement* symbol = (wxSVGSVGElement*)svgNode->GetElementById(wxT("s_") + obj->GetId());
				if (symbol)
					RemoveNotChangeable(symbol, obj);
				for (int i = 0; i < obj->GetObjectParamsCount(); i++) {
					MenuObjectParam* param = obj->GetObjectParam(i);
					if (param->changeable && drawType != mdBUTTONS_NORMAL) {
						wxSVGElement* elem = (wxSVGElement*) symbol->GetElementById(param->element);
						if (elem && param->attribute.length()) {
							wxSVGPaint paint(drawType == mdBUTTONS_SELECTED ? param->selectedColour
									: param->highlightedColour);
							elem->SetAttribute(param->attribute, paint.GetCSSText());
						}
					}
				}
			} else {
				wxSVGElement* elem = (wxSVGElement*) svgNode->GetElementById(obj->GetId());
				if (elem && elem->GetParent())
					elem->GetParent()->RemoveChild(elem);
				elem = (wxSVGElement*) svgNode->GetElementById(wxT("s_") + obj->GetId());
				if (elem && elem->GetParent())
					elem->GetParent()->RemoveChild(elem);
			}
		}
	}

	return svgNode;
}

////////////////////////////// Render ////////////////////////////////////////

wxImage Menu::RenderImage(MenuDrawType drawType, int width, int height) {
	wxSVGDocument svg;
	svg.AppendChild(GetSVGCopy(drawType));
	return svg.Render(width, height);
}

wxImage Menu::GetImage(int width, int height) {
	return RenderImage(mdALL, width, height);
}

wxSVGDocument* Menu::GetBackgroundSVG() {
	wxSVGDocument* svg = new wxSVGDocument;
	svg->AppendChild(GetSVGCopy(mdBACKGROUND));
	return svg;
}

bool Menu::ReduceColours() {
	// create 3d palette
	Palette3D palette;
	for (unsigned int i = 0; i < GetObjectsCount(); i++) {
		MenuObject& obj = *GetObject(i);
		if (!obj.IsButton())
			continue;
		for (int j = 0; j < obj.GetObjectParamsCount(); j++) {
			MenuObjectParam* param = obj.GetObjectParam(j);
			if (param->changeable) {
				palette.Add(param->normalColour, param->highlightedColour, param->selectedColour);
			}
		}
	}
	palette.Add(wxColour(), wxColour(), wxColour());
	// reduce the number of colours
	if (palette.GetColoursCount() <= BUTTON_MAX_COLORS)
		return false;
	palette.ReduceColours(BUTTON_MAX_COLORS);
	// apply palette
	for (unsigned int i = 0; i < GetObjectsCount(); i++) {
		MenuObject& obj = *GetObject(i);
		if (!obj.IsButton())
			continue;
		for (int i = 0; i < obj.GetObjectParamsCount(); i++) {
			MenuObjectParam* param = obj.GetObjectParam(i);
			if (param->changeable) {
				if (palette.Apply(param->normalColour, param->highlightedColour, param->selectedColour)) {
					obj.SetParamColour(param->name, param->normalColour, mbsNORMAL);
					obj.SetParamColour(param->name, param->highlightedColour, mbsHIGHLIGHTED);
					obj.SetParamColour(param->name, param->selectedColour, mbsSELECTED);
				}
			}
		}
	}
	return true;
}

wxImage* Menu::GetImages() {
	ReduceColours();

	// render images
	wxImage* images = new wxImage[4];
	images[0] = RenderImage(mdBACKGROUND, -1, -1);
	images[1] = RenderImage(mdBUTTONS_NORMAL, -1, -1);
	images[2] = RenderImage(mdBUTTONS_HIGHLIGHTED, -1, -1);
	images[3] = RenderImage(mdBUTTONS_SELECTED, -1, -1);

	// make aliasing for buttons
	for (unsigned int i = 0; i < GetObjectsCount(); i++) {
		MenuObject& obj = *GetObject(i);
		if (!obj.IsButton())
			continue;
		// make palette
		MenuPalettes objPalette(obj, m_transpColour);

		// apply palette
		unsigned char* img1 = images[1].GetData() + obj.GetY() * images[1].GetWidth() * 3 + obj.GetX() * 3;
		unsigned char* img2 = images[2].GetData() + obj.GetY() * images[2].GetWidth() * 3 + obj.GetX() * 3;
		unsigned char* img3 = images[3].GetData() + obj.GetY() * images[3].GetWidth() * 3 + obj.GetX() * 3;
		for (int y = 0; y < obj.GetHeight(); y++) {
			for (int x = 0; x < obj.GetWidth(); x++) {
				objPalette.Apply(img1, img1);
				objPalette.Apply(img1, img2, img2);
				objPalette.Apply(img1, img2, img3, img3);
				img1 += 3;
				img2 += 3;
				img3 += 3;
			}
			img1 += (images[1].GetWidth() - obj.GetWidth()) * 3;
			img2 += (images[2].GetWidth() - obj.GetWidth()) * 3;
			img3 += (images[3].GetWidth() - obj.GetWidth()) * 3;
		}
	}

	// all pixels that don't belong to objects must be transparent
	unsigned char* img1 = images[1].GetData();
	unsigned char* img2 = images[2].GetData();
	unsigned char* img3 = images[3].GetData();
	for (int y = 0; y < images[1].GetHeight(); y++) {
		for (int x = 0; x < images[1].GetWidth(); x++) {
			bool found = false;
			for (unsigned int i = 0; i < GetObjectsCount(); i++) {
				MenuObject& obj = *GetObject(i);
				if (!obj.IsButton())
					continue;
				if (x >= obj.GetX() && x < obj.GetX() + obj.GetWidth()
						&& y >= obj.GetY() && y < obj.GetY() + obj.GetHeight()) {
					found = true;
					break;
				}
			}
			if (!found) {
				img1[0] = m_transpColour.Red();
				img1[1] = m_transpColour.Green();
				img1[2] = m_transpColour.Blue();
				img2[0] = m_transpColour.Red();
				img2[1] = m_transpColour.Green();
				img2[2] = m_transpColour.Blue();
				img3[0] = m_transpColour.Red();
				img3[1] = m_transpColour.Green();
				img3[2] = m_transpColour.Blue();
			}
			img1 += 3;
			img2 += 3;
			img3 += 3;
		}
	}

	return images;
}

unsigned int Menu::GetButtonsCount() {
	int count = 0;
	for (int i=0; i<(int)m_objects.Count(); i++)
		if (m_objects[i]->IsButton())
			count++;
	return count;
}

MenuObject* Menu::GetObject(wxString id) {
	for (int i=0; i<(int)m_objects.Count(); i++)
		if (m_objects[i]->GetId() == id)
			return m_objects[i];
	return NULL;
}

void Menu::RemoveObject(wxString id) {
	MenuObject* obj = GetObject(id);
	if (obj) {
		m_objects.Remove(obj);
		delete obj;
	}
}


/** Returns true if button with given id is the first button */
bool Menu::IsFirstButton(wxString id) {
	for (int i=0; i<(int)m_objects.Count(); i++)
		if (m_objects[i]->IsButton())
			return m_objects[i]->GetId() == id;
	return false;
}

/** Makes button with given id the first button */
void Menu::SetFirstButton(wxString id) {
	MenuObject* obj = GetObject(id);
	m_objects.Remove(obj);
	m_objects.Insert(obj, 0);
}

wxString Menu::AddImage(wxString fileName, int x, int y) {
	return AddObject(OBJECTS_DIR + wxT("/image.xml"), fileName, x, y);
}

wxString Menu::AddText(wxString text, int x, int y) {
	return AddObject(OBJECTS_DIR + wxT("/text.xml"), text, x, y);
}

wxString Menu::AddObject(wxString fileName, wxString param, int x, int y) {
	MenuObject* obj = new MenuObject(this, m_tsi == -1, fileName, x, y, param);
	m_objects.Add(obj);
	return obj->GetId();
}

/** Updates image in buttons with given jump action */
void Menu::UpdateButtonImageFor(int actionTsi, int actionPgci, DVD* dvd) {
	for (unsigned int obji = 0; obji < m_objects.GetCount(); obji++) {
		MenuObject* obj = m_objects[obji];
		if (obj->IsButton() && !obj->GetAction().IsCustom()
				&& obj->GetAction().GetTsi() == actionTsi && obj->GetAction().GetPgci() == actionPgci) {
			for (int i = 0; i < obj->GetObjectParamsCount(); i++) {
				MenuObjectParam* param = obj->GetObjectParam(i);
				if (param->type == _T("image")) {
					PgcArray& pgcs = dvd->GetPgcArray(actionTsi == -2 ? m_tsi : actionTsi, false);
					wxString uri;
					if (actionPgci < (int)pgcs.size() && pgcs[actionPgci]->GetVobs().size() > 0)
						uri = pgcs[actionPgci]->GetVobs()[0]->GetVideoFrameUri(obj->GetAction().GetChapter());
					obj->SetParam(param->name, uri);
				}
			}
		}
	}
}

/** Fix coordinates of buttons if they are out of range */
void Menu::FixButtonCoordinates() {
	for (unsigned int obji = 0; obji < m_objects.GetCount(); obji++) {
		MenuObject* obj = m_objects[obji];
		if (obj->IsButton()) {
			if (obj->GetX() < 0)
				obj->SetX(0);
			if (obj->GetY() < 0)
				obj->SetY(0);
			if (obj->GetX() + obj->GetWidth() >= GetResolution().GetWidth())
				obj->SetX(GetResolution().GetWidth() - obj->GetWidth() - 1);
			if (obj->GetY() + obj->GetHeight() >= GetResolution().GetHeight())
				obj->SetY(GetResolution().GetHeight() - obj->GetHeight() - 1);
		}
	}
}

bool Menu::SaveSpumux(wxString fileName, wxString btFile, wxString hlFile, wxString selFile) {
	wxSvgXmlDocument xml;
	wxSvgXmlNode* root = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, _T("subpictures"));
	wxSvgXmlNode* streamNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, _T("stream"));
	wxSvgXmlNode* spuNode = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, _T("spu"));
	if (GetStartTime().Length())
		spuNode->AddProperty(_T("start"), GetStartTime());
	if (GetEndTime().Length())
		spuNode->AddProperty(_T("end"), GetEndTime());
	spuNode->AddProperty(_T("image"), btFile);
	spuNode->AddProperty(_T("highlight"), hlFile);
	spuNode->AddProperty(_T("select"), selFile);
	spuNode->AddProperty(_T("transparent"), SConv::ToString(m_transpColour, false));
	spuNode->AddProperty(_T("force"), _T("yes"));
	// buttons
	for (int i=0; i<(int)GetObjectsCount(); i++)
	{
		MenuObject* obj = GetObject(i);
		if (obj->IsButton() && obj->GetAction().IsValid(m_dvd, m_tsi, 0, true, obj->GetId(), false))
			spuNode->AddChild(obj->GetXML(SPUMUX_XML, false));
	}
	// actions
	for (unsigned int i=0; i<m_actions.Count(); i++)
		spuNode->AddChild(m_actions[i]->GetXML(SPUMUX_XML, false));
	streamNode->AddChild(spuNode);
	root->AddChild(streamNode);
	xml.SetRoot(root);
	return xml.Save(fileName);
}

wxSvgXmlNode* Menu::GetXML(DVDFileType type, wxSvgXmlNode* node) {
	if (node == NULL)
		node = new wxSvgXmlNode(wxSVGXML_ELEMENT_NODE, _T("menu"));
  
	if (type == DVDSTYLER_XML) {
		node->AddProperty(wxT("videoFormat"), (m_videoFormat == vfPAL) ? _T("PAL") : _T("NTSC"));
		node->AddProperty(wxT("aspectRatio"), wxString::Format(wxT("%d"), m_aspectRatio));
		if (GetStartTime().Length() && GetStartTime() != wxT("00:00:00.00"))
			node->AddProperty(_T("startTime"), GetStartTime());
		if (GetEndTime().Length())
			node->AddProperty(_T("endTime"), GetEndTime());
		
		// add svg
		if (m_svg && m_svg->GetRoot()) {
			wxSVGSVGElement* svg = GetSVGCopy();
			// fix path
			wxString bgFile = GetBackground(svg);
			if (bgFile.length() > 0) {
				wxString dir = BACKGROUNDS_DIR;
				if (bgFile.StartsWith(dir))
	 				SetBackground(svg, bgFile.substr(dir.length()));
			}
			// set relative path
			wxSVGElement* elem = svg;
			while (elem != NULL) {
				if (elem->GetDtd() == wxSVG_IMAGE_ELEMENT) {
					wxSVGImageElement* imgElem = (wxSVGImageElement*)elem;
					wxString href = imgElem->GetHref();
					wxString frameStr;
					if (href.Find(wxT('#')) > 0) {
						frameStr = wxT('#') + href.AfterLast(wxT('#'));
						href = href.BeforeLast(wxT('#'));
					}
					wxFileName fname(href);
					if (fname.GetPath() == m_dvd->GetPath(false))
						imgElem->SetHref(fname.GetFullName() + frameStr);
					else if (m_dvd->GetPath(false).length() > 0 && fname.GetPath().StartsWith(m_dvd->GetPath(false)))
						imgElem->SetHref(fname.GetPath().substr(m_dvd->GetPath(false).length() + 1)
								+ wxFILE_SEP_PATH + fname.GetFullName() + frameStr);
				}
				if (elem->GetChildren() != NULL && (elem->GetDtd() == wxSVG_SVG_ELEMENT
						|| elem->GetDtd() == wxSVG_DEFS_ELEMENT || elem->GetDtd() == wxSVG_G_ELEMENT)) {
					elem = (wxSVGElement*)elem->GetChildren();
				} else {
					while (elem != NULL && elem->GetNextSibling() == NULL)
						elem = (wxSVGElement*) elem->GetParent();
					if (elem != NULL)
						elem = (wxSVGElement*) elem->GetNextSibling();
				}
			}
			// append node
			node->AppendChild(svg);
		}
	}
	
	// add buttons info (action, etc.)
	for (unsigned int i=0; i<GetObjectsCount(); i++) {
		MenuObject* obj = GetObject(i);
		if (type == DVDSTYLER_XML || (obj->IsButton() && obj->GetAction().IsValid(m_dvd, m_tsi, 0, true)))
			node->AddChild(obj->GetXML(type, m_dvd, false, m_dvd->GetPlayAllRegister()));
	}
	
	// actions
	for (unsigned int i=0; i<m_actions.Count(); i++)
		node->AddChild(m_actions[i]->GetXML(type, m_dvd));
	
	return node;
}

bool Menu::PutXML(wxSvgXmlNode* node) {
	if (node->GetName() == _T("spumux"))
		node = node->GetChildren();
	if (node != NULL && node->GetName() == _T("menu")) {
		wxString val;
		long lval;
		m_videoFormat = vfPAL;
		if (node->GetPropVal(wxT("videoFormat"), &val) && val == wxT("NTSC"))
			m_videoFormat = vfNTSC;
		if (node->GetPropVal(wxT("aspectRatio"), &val) && val.ToLong(&lval))
		    m_aspectRatio = AspectRatio(lval);
		node->GetPropVal(wxT("startTime"), &m_startTime);
		node->GetPropVal(wxT("endTime"), &m_endTime);
		
		wxSvgXmlNode* svgNode = XmlFindNode(node, wxT("svg"));
		if (svgNode) {
			wxSvgXmlDocument xml;
			xml.SetRoot(svgNode->CloneNode());
			wxMemoryOutputStream output;
			xml.Save(output);
			wxMemoryInputStream input(output);
			m_svg->Load(input);
			// fix path
			if (GetBackground().length() > 0) {
				wxFileName fn(GetBackground());
				if (fn.IsRelative()) {
					if (wxFileExists(m_dvd->GetPath() + fn.GetFullPath()))
						SetBackground(m_dvd->GetPath() + fn.GetFullPath());
					else if (wxFileExists(m_dvd->GetPath() + fn.GetFullName()))
						SetBackground(m_dvd->GetPath() + fn.GetFullName());
					else if (wxFileExists(BACKGROUNDS_DIR + fn.GetFullName()))
						SetBackground(BACKGROUNDS_DIR + fn.GetFullName());
					else
						SetBackground(m_dvd->GetPath() + fn.GetFullPath());
				}
			}
			// fix relative path
			wxSVGElement* elem = m_svg->GetRootElement();
			while (elem != NULL) {
				if (elem->GetDtd() == wxSVG_IMAGE_ELEMENT) {
					wxSVGImageElement* imgElem = (wxSVGImageElement*)elem;
					wxString href = imgElem->GetHref();
					if (href.length()) {
						wxString frameStr;
						if (href.Find(wxT('#')) > 0) {
							frameStr = wxT('#') + href.AfterLast(wxT('#'));
							href = href.BeforeLast(wxT('#'));
						}
						wxFileName fname(href);
						if (fname.IsRelative())
							imgElem->SetHref(m_dvd->GetPath() + fname.GetFullPath() + frameStr);
						else if (!wxFileExists(href) && wxFileExists(m_dvd->GetPath() + fname.GetFullName()))
							imgElem->SetHref(m_dvd->GetPath() + fname.GetFullName() + frameStr);
					}
				}
				if (elem->GetChildren() != NULL && (elem->GetDtd() == wxSVG_SVG_ELEMENT
						|| elem->GetDtd() == wxSVG_DEFS_ELEMENT || elem->GetDtd() == wxSVG_G_ELEMENT)) {
					elem = (wxSVGElement*)elem->GetChildren();
				} else {
					while (elem != NULL && elem->GetNextSibling() == NULL)
						elem = (wxSVGElement*) elem->GetParent();
					if (elem != NULL)
						elem = (wxSVGElement*) elem->GetNextSibling();
				}
			}
	    } else { // deprecated
	    	if (node->GetPropVal(wxT("bgFile"), &val))
	    		SetBackground(val);
	    	else if (node->GetPropVal(wxT("bgColour"), &val))
	    		SetBackgroundColour(SConv::ToColour(val));
		}
		
		for (wxSvgXmlNode* child = node->GetChildren(); child != NULL; child = child->GetNext())
			if (child->GetType() == wxSVGXML_ELEMENT_NODE)
				AddObject(child);
		// fix for old version (<=1.5b5)
		if (m_svg->GetElementById(wxT("objects")) == NULL) {
		  	wxSVGGElement* objectsElem = new wxSVGGElement;
		  	objectsElem->SetId(wxT("objects"));
	  		m_svg->GetRoot()->AppendChild(objectsElem);
	  		wxSVGGElement* buttonsElem = new wxSVGGElement;
	  		buttonsElem->SetId(wxT("buttons"));
	  		m_svg->GetRoot()->AppendChild(buttonsElem);
	  		m_svg->GetRootElement()->GetChildren();
	  		for (unsigned int i = 0; i < m_objects.GetCount(); i++) {
	  			MenuObject& obj = *m_objects.Item(i);
	  			wxSVGElement* useElem = m_svg->GetElementById(obj.GetId());
	  			useElem->GetParent()->RemoveChild(useElem);
	  			if (obj.IsButton())
		  			buttonsElem->AppendChild(useElem);
		  		else
		  			objectsElem->AppendChild(useElem);
	  		}
		}
	}
	return true;
}

/** Stores object data to string */
wxString Menu::Serialize() {
	wxSvgXmlDocument xml;
	xml.SetRoot(GetXML(DVDSTYLER_XML));
	wxStringOutputStream stream;
	xml.Save(stream);
	return stream.GetString();
}

/** Restores object from data */
void Menu::Deserialize(const wxString& data) {
	wxStringInputStream stream(data);
	wxSvgXmlDocument xml;
	xml.Load(stream);
	PutXML(xml.GetRoot());
}

wxString Menu::AddObject(wxSvgXmlNode* node, bool fixPosition) {
    if (node->GetName() == wxT("action")) {
        DVDAction* action = new DVDAction(m_tsi == -1, MENU_ACTION);
        if (!action->PutXML(node)) {
            delete action;
            return wxT("");
        }
        m_actions.Add(action);
        return action->GetId();
    } else if (node->GetName() == wxT("button") || node->GetName() == wxT("object")) {
    	if (node->GetName() == wxT("button") && GetButtonsCount() > 33) {
			wxLogError(_("DVD menu can contain maximal 34 buttons"));
			return wxT("");
		}
    		
        MenuObject* obj = new MenuObject(this, m_tsi == -1);
        if (!obj->PutXML(node)) {
            delete obj;
            return wxT("");
        }
        if (fixPosition) // for copy & paste
            FixPosition(obj);
        m_objects.Add(obj);
        return obj->GetId();
    }
    return wxT("");
}

void Menu::FixPosition(MenuObject* obj) {
	if (obj->GetX() == -1)
		obj->SetX(GetResolution().x / 4);
	if (obj->GetY() == -1)
		obj->SetY(GetResolution().y / 4);
	while (obj->GetX() < GetResolution().x - 48 && obj->GetY()
			< GetResolution().y - 48) {
		bool found = false;
		for (int i = 0; i < (int) GetObjectsCount(); i++) {
			MenuObject* obj1 = GetObject(i);
			if (obj1->GetX() == obj->GetX() && obj1->GetY() == obj->GetY()) {
				found = true;
				break;
			}
		}
		if (!found)
			break;
		obj->SetX(obj->GetX() + 16);
		obj->SetY(obj->GetY() + 16);
	}
}
