// Copyright (c) 2005-2010, Niels Martin Hansen
// Copyright (c) 2005-2010, Rodrigo Braz Monteiro
// Copyright (c) 2010, Amar Takhar
// 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/
//
// $Id$

/// @file edit.cpp
/// @brief edit/ commands.
/// @ingroup command
///

#include "../config.h"

#ifndef AGI_PRE
#include <algorithm>

#include <wx/clipbrd.h>
#include <wx/tokenzr.h>
#endif

#include "command.h"

#include "../ass_dialogue.h"
#include "../ass_file.h"
#include "../ass_karaoke.h"
#include "../dialog_paste_over.h"
#include "../dialog_search_replace.h"
#include "../main.h"
#include "../include/aegisub/context.h"
#include "../subs_edit_ctrl.h"
#include "../subs_grid.h"
#include "../video_context.h"

namespace {
	using cmd::Command;
/// @defgroup cmd-edit Editing commands.
/// @{

struct validate_sel_nonempty : public Command {
	CMD_TYPE(COMMAND_VALIDATE)
	bool Validate(const agi::Context *c) {
		return c->selectionController->GetSelectedSet().size() > 0;
	}
};

struct validate_sel_multiple : public Command {
	CMD_TYPE(COMMAND_VALIDATE)
	bool Validate(const agi::Context *c) {
		return c->selectionController->GetSelectedSet().size() > 1;
	}
};

void paste_lines(agi::Context *c, bool paste_over) {
	wxString data;
	if (wxTheClipboard->Open()) {
		if (wxTheClipboard->IsSupported(wxDF_TEXT)) {
			wxTextDataObject rawdata;
			wxTheClipboard->GetData(rawdata);
			data = rawdata.GetText();
		}
		wxTheClipboard->Close();
	}
	if (!data) return;

	AssDialogue *rel_line = c->selectionController->GetActiveLine();
	entryIter pos = find(c->ass->Line.begin(), c->ass->Line.end(), rel_line);

	AssDialogue *first = 0;
	std::set<AssDialogue *> newsel;

	std::vector<bool> pasteOverOptions;
	wxStringTokenizer token (data,"\r\n",wxTOKEN_STRTOK);
	while (token.HasMoreTokens()) {
		// Convert data into an AssDialogue
		wxString curdata = token.GetNextToken();
		curdata.Trim(true);
		curdata.Trim(false);
		AssDialogue *curdiag;
		try {
			// Try to interpret the line as an ASS line
			curdiag = new AssDialogue(curdata);
		}
		catch (...) {
			// Line didn't parse correctly, assume it's plain text that
			// should be pasted in the Text field only
			curdiag = new AssDialogue();
			curdiag->Text = curdata;
			// Make sure pasted plain-text lines always are blank-timed
			curdiag->Start = 0;
			curdiag->End = 0;
		}

		if (!first)
			first = curdiag;

		if (!paste_over) {
			newsel.insert(curdiag);
			c->ass->Line.insert(pos, curdiag);
		}
		else {
			// Get list of options to paste over, if not asked yet
			if (pasteOverOptions.empty()) {
				DialogPasteOver diag(c->parent);
				if (diag.ShowModal()) {
					delete curdiag;
					return;
				}
				pasteOverOptions = OPT_GET("Tool/Paste Lines Over/Fields")->GetListBool();
			}

			AssDialogue *line = static_cast<AssDialogue *>(*pos);
			if (pasteOverOptions[0]) line->Layer = curdiag->Layer;
			if (pasteOverOptions[1]) line->Start = curdiag->Start;
			if (pasteOverOptions[2]) line->End = curdiag->End;
			if (pasteOverOptions[3]) line->Style = curdiag->Style;
			if (pasteOverOptions[4]) line->Actor = curdiag->Actor;
			if (pasteOverOptions[5]) line->Margin[0] = curdiag->Margin[0];
			if (pasteOverOptions[6]) line->Margin[1] = curdiag->Margin[1];
			if (pasteOverOptions[7]) line->Margin[2] = curdiag->Margin[2];
			if (pasteOverOptions[8]) line->Effect = curdiag->Effect;
			if (pasteOverOptions[9]) line->Text = curdiag->Text;

			delete curdiag;

			do {
				++pos;
			} while (pos != c->ass->Line.end() && !dynamic_cast<AssDialogue*>(*pos));
			if (pos == c->ass->Line.end())
				break;
		}
	}

	if (first) {
		c->ass->Commit(_("paste"), paste_over ? AssFile::COMMIT_DIAG_FULL : AssFile::COMMIT_DIAG_ADDREM);

		if (!paste_over)
			c->selectionController->SetSelectionAndActive(newsel, first);
	}
}

/// Find and replace words in subtitles.
struct edit_find_replace : public Command {
	CMD_NAME("edit/find_replace")
		STR_MENU("Find and R&eplace...")
		STR_DISP("Find and Replace")
		STR_HELP("Find and replace words in subtitles")

		void operator()(agi::Context *c) {
			c->videoController->Stop();
			Search.OpenDialog(true);
	}
};

/// Copy subtitles.
struct edit_line_copy : public validate_sel_nonempty {
	CMD_NAME("edit/line/copy")
	STR_MENU("&Copy Lines")
	STR_DISP("Copy Lines")
	STR_HELP("Copy subtitles")

	void operator()(agi::Context *c) {
		// Ideally we'd let the control's keydown handler run and only deal
		// with the events not processed by it, but that doesn't seem to be
		// possible with how wx implements key event handling - the native
		// platform processing is evoked only if the wx event is unprocessed,
		// and there's no way to do something if the native platform code leaves
		// it unprocessed

		if (wxTextEntryBase *ctrl = dynamic_cast<wxTextEntryBase*>(c->parent->FindFocus()))
			ctrl->Copy();
		else
			c->subsGrid->CopyLines(c->subsGrid->GetSelection());
	}
};


/// Cut subtitles.
struct edit_line_cut: public validate_sel_nonempty {
	CMD_NAME("edit/line/cut")
	STR_MENU("Cu&t Lines")
	STR_DISP("Cut Lines")
	STR_HELP("Cut subtitles")

	void operator()(agi::Context *c) {
		if (wxTextEntryBase *ctrl = dynamic_cast<wxTextEntryBase*>(c->parent->FindFocus()))
			ctrl->Cut();
		else
			c->subsGrid->CutLines(c->subsGrid->GetSelection());
	}
};


/// Delete currently selected lines.
struct edit_line_delete : public validate_sel_nonempty {
	CMD_NAME("edit/line/delete")
	STR_MENU("De&lete Lines")
	STR_DISP("Delete Lines")
	STR_HELP("Delete currently selected lines")

	void operator()(agi::Context *c) {
		c->subsGrid->DeleteLines(c->subsGrid->GetSelection());
	}
};

struct in_selection : public std::unary_function<AssEntry*, bool> {
	SubtitleSelectionController::Selection const& sel;
	in_selection(SubtitleSelectionController::Selection const& sel) : sel(sel) { }
	bool operator()(AssEntry *e) const {
		AssDialogue *d = dynamic_cast<AssDialogue*>(e);
		return d && sel.count(d);
	}
};

static void duplicate_lines(agi::Context *c, bool shift) {
	in_selection sel(c->selectionController->GetSelectedSet());
	SubtitleSelectionController::Selection new_sel;
	AssDialogue *new_active = 0;

	std::list<AssEntry*>::iterator start = c->ass->Line.begin();
	std::list<AssEntry*>::iterator end = c->ass->Line.end();
	while (start != end) {
		// Find the first line in the selection
		start = find_if(start, end, sel);
		if (start == end) break;

		// And the last line in this contiguous selection
		std::list<AssEntry*>::iterator insert_pos = find_if(start, end, std::not1(sel));
		std::list<AssEntry*>::iterator last = insert_pos;
		--last;

		// Duplicate each of the selected lines, inserting them in a block
		// after the selected block
		do {
			AssDialogue *new_diag = static_cast<AssDialogue*>((*start)->Clone());

			c->ass->Line.insert(insert_pos, new_diag);
			new_sel.insert(new_diag);
			if (!new_active)
				new_active = new_diag;

			if (shift) {
				int pos = c->videoController->FrameAtTime(new_diag->End, agi::vfr::END) + 1;
				new_diag->Start = c->videoController->TimeAtFrame(pos, agi::vfr::START);
				new_diag->End = c->videoController->TimeAtFrame(pos, agi::vfr::END);
			}
		} while (start++ != last);

		// Skip over the lines we just made
		start = insert_pos;
	}

	if (new_sel.empty()) return;

	c->ass->Commit(_("duplicate lines"), AssFile::COMMIT_DIAG_ADDREM);

	c->selectionController->SetSelectionAndActive(new_sel, new_active);
}

/// Duplicate the selected lines.
struct edit_line_duplicate : public validate_sel_nonempty {
	CMD_NAME("edit/line/duplicate")
	STR_MENU("&Duplicate Lines")
	STR_DISP("Duplicate Lines")
	STR_HELP("Duplicate the selected lines")

	void operator()(agi::Context *c) {
		duplicate_lines(c, false);
	}
};


/// Duplicate lines and shift by one frame.
struct edit_line_duplicate_shift : public Command {
	CMD_NAME("edit/line/duplicate/shift")
	STR_MENU("D&uplicate and Shift by 1 Frame")
	STR_DISP("Duplicate and Shift by 1 Frame")
	STR_HELP("Duplicate lines and shift by one frame")
	CMD_TYPE(COMMAND_VALIDATE)

	bool Validate(const agi::Context *c) {
		return !c->selectionController->GetSelectedSet().empty() && c->videoController->IsLoaded();
	}

	void operator()(agi::Context *c) {
		duplicate_lines(c, true);
	}
};

static void combine_lines(agi::Context *c, void (*combiner)(AssDialogue *, AssDialogue *), wxString const& message) {
	SelectionController<AssDialogue>::Selection sel = c->selectionController->GetSelectedSet();

	AssDialogue *first = 0;
	entryIter out = c->ass->Line.begin();
	for (entryIter it = c->ass->Line.begin(); it != c->ass->Line.end(); ++it) {
		AssDialogue *diag = dynamic_cast<AssDialogue*>(*it);
		if (!diag || !sel.count(diag)) {
			*out++ = *it;
			continue;
		}
		if (!first) {
			first = diag;
			*out++ = *it;
			continue;
		}

		combiner(first, diag);

		first->End = std::max(first->End, diag->End);
		delete diag;
	}

	c->ass->Line.erase(out, c->ass->Line.end());
	sel.clear();
	sel.insert(first);
	c->selectionController->SetSelectionAndActive(sel, first);
	c->ass->Commit(message, AssFile::COMMIT_DIAG_ADDREM | AssFile::COMMIT_DIAG_FULL);
}

static void combine_karaoke(AssDialogue *first, AssDialogue *second) {
	first->Text += wxString::Format("{\\k%d}%s", (second->Start - first->End) / 10, second->Text);
}

static void combine_concat(AssDialogue *first, AssDialogue *second) {
	first->Text += " " + second->Text;
}

static void combine_drop(AssDialogue *, AssDialogue *) { }

/// Joins selected lines in a single one, as karaoke.
struct edit_line_join_as_karaoke : public validate_sel_multiple {
	CMD_NAME("edit/line/join/as_karaoke")
	STR_MENU("As &Karaoke")
	STR_DISP("As Karaoke")
	STR_HELP("Joins selected lines in a single one, as karaoke")

	void operator()(agi::Context *c) {
		combine_lines(c, combine_karaoke, _("join as karaoke"));
	}
};


/// Joins selected lines in a single one, concatenating text together.
struct edit_line_join_concatenate : public validate_sel_multiple {
	CMD_NAME("edit/line/join/concatenate")
	STR_MENU("&Concatenate")
	STR_DISP("Concatenate")
	STR_HELP("Joins selected lines in a single one, concatenating text together")

	void operator()(agi::Context *c) {
		combine_lines(c, combine_concat, _("join lines"));
	}
};


/// Joins selected lines in a single one, keeping text of first and discarding remaining.
struct edit_line_join_keep_first : public validate_sel_multiple {
	CMD_NAME("edit/line/join/keep_first")
	STR_MENU("Keep &First")
	STR_DISP("Keep First")
	STR_HELP("Joins selected lines in a single one, keeping text of first and discarding remaining")

	void operator()(agi::Context *c) {
		combine_lines(c, combine_drop, _("join lines"));
	}
};


/// Paste subtitles.
struct edit_line_paste : public Command {
	CMD_NAME("edit/line/paste")
	STR_MENU("&Paste Lines")
	STR_DISP("Paste Lines")
	STR_HELP("Paste subtitles")
	CMD_TYPE(COMMAND_VALIDATE)

	bool Validate(const agi::Context *) {
		if (wxTheClipboard->Open()) {
			bool can_paste = wxTheClipboard->IsSupported(wxDF_TEXT);
			wxTheClipboard->Close();
			return can_paste;
		}
		return false;
	}

	void operator()(agi::Context *c) {
		if (wxTextEntryBase *ctrl = dynamic_cast<wxTextEntryBase*>(c->parent->FindFocus()))
			ctrl->Paste();
		else
			paste_lines(c, false);
	}
};


/// Paste subtitles over others.
struct edit_line_paste_over : public Command {
	CMD_NAME("edit/line/paste/over")
	STR_MENU("Paste Lines &Over...")
	STR_DISP("Paste Lines Over")
	STR_HELP("Paste subtitles over others")
	CMD_TYPE(COMMAND_VALIDATE)

	bool Validate(const agi::Context *c) {
		if (wxTheClipboard->Open()) {
			bool can_paste = wxTheClipboard->IsSupported(wxDF_TEXT);
			wxTheClipboard->Close();
			return can_paste && c->selectionController->GetSelectedSet().size();
		}
		return false;
	}

	void operator()(agi::Context *c) {
		paste_lines(c, true);
	}
};


/// Recombine subtitles when they have been split and merged.
struct edit_line_recombine : public validate_sel_multiple {
	CMD_NAME("edit/line/recombine")
	STR_MENU("Recom&bine Lines")
	STR_DISP("Recombine Lines")
	STR_HELP("Recombine subtitles when they have been split and merged")

	void operator()(agi::Context *c) {
		c->subsGrid->RecombineLines();
	}
};


/// Uses karaoke timing to split line into multiple smaller lines.
struct edit_line_split_by_karaoke : public validate_sel_nonempty {
	CMD_NAME("edit/line/split/by_karaoke")
	STR_MENU("Split Lines (by karaoke)")
	STR_DISP("Split Lines (by karaoke)")
	STR_HELP("Uses karaoke timing to split line into multiple smaller lines")

	void operator()(agi::Context *c) {
		AssKaraoke::SplitLines(c->selectionController->GetSelectedSet(), c);
	}
};

/// Redoes last action.
struct edit_redo : public Command {
	CMD_NAME("edit/redo")
	STR_HELP("Redoes last action")
	CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME)

	wxString StrMenu(const agi::Context *c) const {
		return c->ass->IsRedoStackEmpty() ?
			_("Nothing to &redo") :
		wxString::Format(_("&Redo %s"), c->ass->GetRedoDescription());
	}
	wxString StrDisplay(const agi::Context *c) const {
		return c->ass->IsRedoStackEmpty() ?
			_("Nothing to redo") :
		wxString::Format(_("Redo %s"), c->ass->GetRedoDescription());
	}

	bool Validate(const agi::Context *c) {
		return !c->ass->IsRedoStackEmpty();
	}

	void operator()(agi::Context *c) {
		c->ass->Redo();
	}
};

/// Undoes last action.
struct edit_undo : public Command {
	CMD_NAME("edit/undo")
	STR_HELP("Undoes last action")
	CMD_TYPE(COMMAND_VALIDATE | COMMAND_DYNAMIC_NAME)

	wxString StrMenu(const agi::Context *c) const {
		return c->ass->IsUndoStackEmpty() ?
			_("Nothing to &undo") :
			wxString::Format(_("&Undo %s"), c->ass->GetUndoDescription());
	}
	wxString StrDisplay(const agi::Context *c) const {
		return c->ass->IsUndoStackEmpty() ?
			_("Nothing to undo") :
			wxString::Format(_("Undo %s"), c->ass->GetUndoDescription());
	}

	bool Validate(const agi::Context *c) {
		return !c->ass->IsUndoStackEmpty();
	}

	void operator()(agi::Context *c) {
		c->ass->Undo();
	}
};

}
/// @}

namespace cmd {
	void init_edit() {
		reg(new edit_find_replace);
		reg(new edit_line_copy);
		reg(new edit_line_cut);
		reg(new edit_line_delete);
		reg(new edit_line_duplicate);
		reg(new edit_line_duplicate_shift);
		reg(new edit_line_join_as_karaoke);
		reg(new edit_line_join_concatenate);
		reg(new edit_line_join_keep_first);
		reg(new edit_line_paste);
		reg(new edit_line_paste_over);
		reg(new edit_line_recombine);
		reg(new edit_line_split_by_karaoke);
		reg(new edit_redo);
		reg(new edit_undo);
	}
}
