// Copyright (c) 2007, Rodrigo Braz Monteiro
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/

/// @file subtitle_format_ttxt.cpp
/// @brief Reading/writing MPEG-4 Timed Text subtitles in TTXT XML format
/// @ingroup subtitle_io
///

#include "subtitle_format_ttxt.h"

#include "ass_dialogue.h"
#include "ass_file.h"
#include "compat.h"
#include "options.h"

#include <libaegisub/ass/time.h>

#include <wx/xml/xml.h>

DEFINE_EXCEPTION(TTXTParseError, SubtitleFormatParseError);

TTXTSubtitleFormat::TTXTSubtitleFormat()
: SubtitleFormat("MPEG-4 Streaming Text")
{
}

std::vector<std::string> TTXTSubtitleFormat::GetReadWildcards() const {
	return {"ttxt"};
}

std::vector<std::string> TTXTSubtitleFormat::GetWriteWildcards() const {
	return GetReadWildcards();
}

void TTXTSubtitleFormat::ReadFile(AssFile *target, agi::fs::path const& filename, agi::vfr::Framerate const& fps, const char *encoding) const {
	target->LoadDefault(false, OPT_GET("Subtitle Format/TTXT/Default Style Catalog")->GetString());

	// Load XML document
	wxXmlDocument doc;
	if (!doc.Load(filename.wstring())) throw TTXTParseError("Failed loading TTXT XML file.");

	// Check root node name
	if (doc.GetRoot()->GetName() != "TextStream") throw TTXTParseError("Invalid TTXT file.");

	// Check version
	wxString verStr = doc.GetRoot()->GetAttribute("version", "");
	int version = -1;
	if (verStr == "1.0")
		version = 0;
	else if (verStr == "1.1")
		version = 1;
	else
		throw TTXTParseError("Unknown TTXT version: " + from_wx(verStr));

	// Get children
	AssDialogue *diag = nullptr;
	int lines = 0;
	for (wxXmlNode *child = doc.GetRoot()->GetChildren(); child; child = child->GetNext()) {
		// Line
		if (child->GetName() == "TextSample") {
			if ((diag = ProcessLine(child, diag, version))) {
				lines++;
				target->Events.push_back(*diag);
			}
		}
		// Header
		else if (child->GetName() == "TextStreamHeader") {
			ProcessHeader(child);
		}
	}

	// No lines?
	if (lines == 0)
		target->Events.push_back(*new AssDialogue);
}

AssDialogue *TTXTSubtitleFormat::ProcessLine(wxXmlNode *node, AssDialogue *prev, int version) const {
	// Get time
	wxString sampleTime = node->GetAttribute("sampleTime", "00:00:00.000");
	agi::Time time(from_wx(sampleTime));

	// Set end time of last line
	if (prev)
		prev->End = time;

	// Get text
	wxString text;
	if (version == 0)
		text = node->GetAttribute("text", "");
	else
		text = node->GetNodeContent();

	// Create line
	if (text.empty()) return nullptr;

	// Create dialogue
	auto diag = new AssDialogue;
	diag->Start = time;
	diag->End = 36000000-10;

	// Process text for 1.0
	if (version == 0) {
		wxString finalText;
		finalText.reserve(text.size());
		bool in = false;
		bool first = true;
		for (auto chr : text) {
			if (chr == '\'') {
				if (!in && !first) finalText += "\\N";
				first = false;
				in = !in;
			}
			else if (in) finalText += chr;
		}
		diag->Text = from_wx(finalText);
	}

	// Process text for 1.1
	else {
		text.Replace("\r", "");
		text.Replace("\n", "\\N");
		diag->Text = from_wx(text);
	}

	return diag;
}

void TTXTSubtitleFormat::ProcessHeader(wxXmlNode *node) const {
	// TODO
}

void TTXTSubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filename, agi::vfr::Framerate const& fps, const char *encoding) const {
	// Convert to TTXT
	AssFile copy(*src);
	ConvertToTTXT(copy);

	// Create XML structure
	wxXmlDocument doc;
	wxXmlNode *root = new wxXmlNode(nullptr, wxXML_ELEMENT_NODE, "TextStream");
	root->AddAttribute("version", "1.1");
	doc.SetRoot(root);

	// Create header
	WriteHeader(root);

	// Create lines
	const AssDialogue *prev = nullptr;
	for (auto const& current : copy.Events) {
		WriteLine(root, prev, &current);
		prev = &current;
	}

	// Save XML
	doc.Save(filename.wstring());
}

void TTXTSubtitleFormat::WriteHeader(wxXmlNode *root) const {
	// Write stream header
	wxXmlNode *node = new wxXmlNode(wxXML_ELEMENT_NODE, "TextStreamHeader");
	node->AddAttribute("width", "400");
	node->AddAttribute("height", "60");
	node->AddAttribute("layer", "0");
	node->AddAttribute("translation_x", "0");
	node->AddAttribute("translation_y", "0");
	root->AddChild(node);
	root = node;

	// Write sample description
	node = new wxXmlNode(wxXML_ELEMENT_NODE, "TextSampleDescription");
	node->AddAttribute("horizontalJustification", "center");
	node->AddAttribute("verticalJustification", "bottom");
	node->AddAttribute("backColor", "0 0 0 0");
	node->AddAttribute("verticalText", "no");
	node->AddAttribute("fillTextRegion", "no");
	node->AddAttribute("continuousKaraoke", "no");
	node->AddAttribute("scroll", "None");
	root->AddChild(node);
	root = node;

	// Write font table

	node = new wxXmlNode(wxXML_ELEMENT_NODE, "FontTable");
	root->AddChild(node);

	wxXmlNode *subNode = new wxXmlNode(wxXML_ELEMENT_NODE, "FontTableEntry");
	subNode->AddAttribute("fontName", "Sans");
	subNode->AddAttribute("fontID", "1");
	node->AddChild(subNode);

	// Write text box
	node = new wxXmlNode(wxXML_ELEMENT_NODE, "TextBox");
	node->AddAttribute("top", "0");
	node->AddAttribute("left", "0");
	node->AddAttribute("bottom", "60");
	node->AddAttribute("right", "400");
	root->AddChild(node);

	// Write style
	node = new wxXmlNode(wxXML_ELEMENT_NODE, "Style");
	node->AddAttribute("styles", "Normal");
	node->AddAttribute("fontID", "1");
	node->AddAttribute("fontSize", "18");
	node->AddAttribute("color", "ff ff ff ff");
	root->AddChild(node);
}

void TTXTSubtitleFormat::WriteLine(wxXmlNode *root, const AssDialogue *prev, const AssDialogue *line) const {
	// If it doesn't start at the end of previous, add blank
	if (prev && prev->End != line->Start) {
		wxXmlNode *node = new wxXmlNode(wxXML_ELEMENT_NODE, "TextSample");
		node->AddAttribute("sampleTime", to_wx("0" + prev->End.GetAssFormatted(true)));
		node->AddAttribute("xml:space", "preserve");
		root->AddChild(node);
		node->AddChild(new wxXmlNode(wxXML_TEXT_NODE, "", ""));
	}

	// Generate and insert node
	wxXmlNode *node = new wxXmlNode(wxXML_ELEMENT_NODE, "TextSample");
	node->AddAttribute("sampleTime", to_wx("0" + line->Start.GetAssFormatted(true)));
	node->AddAttribute("xml:space", "preserve");
	root->AddChild(node);
	node->AddChild(new wxXmlNode(wxXML_TEXT_NODE, "", to_wx(line->Text)));
}

void TTXTSubtitleFormat::ConvertToTTXT(AssFile &file) const {
	file.Sort();
	StripComments(file);
	RecombineOverlaps(file);
	MergeIdentical(file);
	StripTags(file);
	ConvertNewlines(file, "\r\n");

	// Find last line
	agi::Time lastTime;
	if (!file.Events.empty())
		lastTime = file.Events.back().End;

	// Insert blank line at the end
	auto diag = new AssDialogue;
	diag->Start = lastTime;
	diag->End = lastTime+OPT_GET("Timing/Default Duration")->GetInt();
	file.Events.push_back(*diag);
}
