// Copyright (c) 2005, 2006, 2007, Rodrigo Braz Monteiro, Niels Martin Hansen
// 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 audio_karaoke.cpp
/// @brief Karaoke table UI in audio box (not in audio display)
/// @ingroup audio_ui
///


///////////
// Headers
#include "config.h"

#include "audio_karaoke.h"
#include "audio_display.h"
#include "audio_box.h"
#include "ass_dialogue.h"
#include "ass_override.h"
#include <wx/tokenzr.h>
#include <wx/menu.h>
#include <wx/log.h>
#include <wx/dcclient.h>
#include <wx/settings.h>
#include <algorithm>



/// @brief Empty constructor 
///
AudioKaraokeSyllable::AudioKaraokeSyllable()
: AssKaraokeSyllable()
, start_time(0), selected(false)
, display_w(0), display_x(0)
{
}


/// @brief Copy-from-base constructor 
/// @param base 
///
AudioKaraokeSyllable::AudioKaraokeSyllable(const AssKaraokeSyllable &base)
: AssKaraokeSyllable(base)
, start_time(0), selected(false)
, display_w(0), display_x(0)
{
}




/// @brief Constructor 
/// @param parent 
///
AudioKaraoke::AudioKaraoke(wxWindow *parent)
: wxWindow (parent,-1,wxDefaultPosition,wxSize(10,5),wxTAB_TRAVERSAL|wxBORDER_SUNKEN)
{
	wxLogDebug(_T("AudioKaraoke constructor"));
	enabled = false;
	splitting = false;
	split_cursor_syl = -1;
	curSyllable = 0;
	diag = 0;
	workDiag = 0;
}



/// @brief Destructor 
///
AudioKaraoke::~AudioKaraoke() {
	delete workDiag;
}



/// @brief Load from dialogue 
/// @param _diag 
/// @return 
///
bool AudioKaraoke::LoadFromDialogue(AssDialogue *_diag) {
	wxLogDebug(_T("AudioKaraoke::LoadFromDialogue(diag=%p)"), _diag);
	// Make sure we're not in splitting-mode
	if (splitting) {
		wxLogDebug(_T("AudioKaraoke::LoadFromDialogue: is splitting, discarding splits"));
		// Discard any splits and leave split-mode
		EndSplit(false);
	}

	// Set dialogue
	delete workDiag;
	diag = _diag;
	if (!diag) {
		wxLogDebug(_T("AudioKaraoke::LoadFromDialogue: no diag, refreshing and returning false"));
		Refresh(false);
		return false;
	}

	// Split
	wxLogDebug(_T("AudioKaraoke::LoadFromDialogue: split"));
	workDiag = new AssDialogue(diag->GetEntryData(), false);
	workDiag->ParseASSTags();
	must_rebuild = false;
	bool hasKar = ParseDialogue(workDiag);

	// No karaoke, autosplit
	if (!hasKar) {
		wxLogDebug(_T("AudioKaraoke::LoadFromDialogue: no existing karaoke, auto-splitting"));
		AutoSplit();
	}

	// Done
	//if (curSyllable < 0) curSyllable = syllables.size()-1;
	//if (curSyllable >= (signed) syllables.size()) curSyllable = 0;
	//SetSelection(curSyllable);
	//Refresh(false);
	wxLogDebug(_T("AudioKaraoke::LoadFromDialogue: returning %d"), hasKar?0:1);
	return !hasKar;
}



/// @brief Writes line back 
/// @return 
///
void AudioKaraoke::Commit() {
	wxLogDebug(_T("AudioKaraoke::Commit"));
	if (splitting) {
		wxLogDebug(_T("AudioKaraoke::Commit: splitting, ending split"));
		EndSplit(true);
	}
	wxString finalText = _T("");
	AudioKaraokeSyllable *syl;
	size_t n = syllables.size();
	wxLogDebug(_T("AudioKaraoke::Commit: syllables.size() = %u"), n);
	if (must_rebuild) {
		wxLogDebug(_T("AudioKaraoke::Commit: must_rebuild"));
		workDiag->ClearBlocks();
		for (size_t i=0;i<n;i++) {
			syl = &syllables.at(i);
			finalText += wxString::Format(_T("{%s%i}"), syl->type.c_str(), syl->duration) + syl->text;
		}
		workDiag->Text = finalText;
		workDiag->ParseASSTags();
		diag->Text = finalText;
	} else {
		wxLogDebug(_T("AudioKaraoke::Commit: Updating karaoke without rebuild"));
		for (size_t i = 0; i < n; i++) {
			wxLogDebug(_T("AudioKaraoke::Commit: Updating syllable %d"), i);
			syl = &syllables.at(i);
			wxLogDebug(_T("AudioKaraoke::Commit: Syllable pointer: %p; tagdata pointer: %p; length: %d"), syl, syl->tag, syl->duration);
			// Some weird people have text before the first karaoke tag on a line.
			// Check that a karaoke tag actually exists for the (non-)syllable to avoid a crash.
			if (syl->tag && syl->tag->Params.size()>0)
				syl->tag->Params[0]->SetInt(syl->duration);
			// Of course, if the user changed the duration of such a non-syllable, its timing can't be updated and will stay zero.
			// There is no way to check for that right now, and I can't bother to fix it.
		}
		wxLogDebug(_T("AudioKaraoke::Commit: Done updating syllables"));
		workDiag->UpdateText();
		workDiag->ClearBlocks();
		workDiag->ParseASSTags();
		diag->Text = workDiag->Text;
	}
	wxLogDebug(_T("AudioKaraoke::Commit: returning"));
}



/// @brief Autosplit line 
/// @return 
///
void AudioKaraoke::AutoSplit() {
	wxLogDebug(_T("AudioKaraoke::AutoSplit"));

	// Get lengths
	int timeLen = (diag->End.GetMS() - diag->Start.GetMS())/10;
	int letterlen = diag->Text.Length();
	int round = letterlen / 2;
	int curlen;
	int acumLen = 0;
	wxString newText;

	// Parse words
	wxStringTokenizer tkz(diag->Text,_T(" "),wxTOKEN_RET_DELIMS);
	wxArrayString words;
	while (tkz.HasMoreTokens()) {
		words.Add(tkz.GetNextToken());
	}

	// Process words
	for (size_t i=0;i<words.Count();i++) {
		curlen = (words[i].Length() * timeLen + round) / letterlen;
		acumLen += curlen;

		// Don't let it accumulate over total length
		if (acumLen > timeLen) {
			curlen -= acumLen - timeLen;
			acumLen = timeLen;
		}

		// Ensure that it accumulates all of it
		if (i == words.Count()-1 && acumLen < timeLen) {
			curlen += timeLen - acumLen;
			acumLen = timeLen;
		}

		newText += wxString::Format(_T("{\\k%i}"),curlen) + words[i];
	}

	// Workaround for bug #503
	// Make the line one blank syllable if it's completely blank
	if (newText == _T("")) newText = wxString::Format(_T("{\\k%d}"), timeLen);

	// Load
	must_rebuild = true;
	AssDialogue newDiag(diag->GetEntryData());
	newDiag.Text = newText;
	newDiag.ParseASSTags();
	ParseDialogue(&newDiag);

	wxLogDebug(_T("AudioKaraoke::AutoSplit: returning"));
}



/// @brief Parses text to extract karaoke 
/// @param curDiag 
/// @return 
///
bool AudioKaraoke::ParseDialogue(AssDialogue *curDiag) {
	// parse the tagdata
	AssKaraokeVector tempsyls;
	ParseAssKaraokeTags(curDiag, tempsyls);

	bool found_kara = tempsyls.size() > 1;

	// copy base syllables to real
	syllables.clear();
	syllables.reserve(tempsyls.size());
	int cur_time = 0;
	for (AssKaraokeVector::iterator base = tempsyls.begin(); base != tempsyls.end(); ++base) {
		AudioKaraokeSyllable fullsyl(*base);
		fullsyl.start_time = cur_time;
		cur_time += fullsyl.duration;
		syllables.push_back(fullsyl);
	}

	// if first syllable is empty, remove it
	if (!syllables[0].unstripped_text) {
		syllables.erase(syllables.begin());
		found_kara = syllables.size() > 0;
	}

	// if there's more than one syllable in the list, at least one karaoke tag was found
	return found_kara;
}



/// @brief Set syllable 
/// @param n 
/// @return 
///
void AudioKaraoke::SetSyllable(int n) {
	wxLogDebug(_T("AudioKaraoke::SetSyllable(n=%d)"), n);
	if (n == -1) n = syllables.size()-1;
	if (n >= (signed) syllables.size()) n = 0;
	curSyllable = n;
	startClickSyl = n;
	SetSelection(n);
	Refresh(false);
	wxLogDebug(_T("AudioKaraoke::SetSyllable: returning"));
}


///////////////
// Event table
BEGIN_EVENT_TABLE(AudioKaraoke,wxWindow)
	EVT_PAINT(AudioKaraoke::OnPaint)
	EVT_SIZE(AudioKaraoke::OnSize)
	EVT_MOUSE_EVENTS(AudioKaraoke::OnMouse)
END_EVENT_TABLE()



/// @brief Paint event 
/// @param event 
///
void AudioKaraoke::OnPaint(wxPaintEvent &event) {
	// Get dimensions
	int w,h;
	GetClientSize(&w,&h);

	// Start Paint
	wxPaintDC dc(this);

	// Draw background
	dc.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNFACE)));
	dc.SetPen(*wxTRANSPARENT_PEN);
	dc.DrawRectangle(0,0,w,h);

	// Set syllable font
	wxFont curFont(9,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_NORMAL,false,_T("Verdana"),wxFONTENCODING_SYSTEM); // FIXME, hardcoded font
	dc.SetFont(curFont);

	// Draw syllables
	if (enabled) {
		wxString temptext;
		size_t syln = syllables.size();
		int dx = 0;
		int tw,th;
		int delta;
		int dlen;
		for (size_t i=0;i<syln;i++) {
			AudioKaraokeSyllable &syl = syllables.at(i);
			// Calculate text length
			temptext = syl.text;
			// If we're splitting, every character must be drawn
			if (!splitting) {
				temptext.Trim(true);
				temptext.Trim(false);
			}
			GetTextExtent(temptext,&tw,&th,NULL,NULL,&curFont);
			delta = 0;
			if (tw < 10) delta = 10 - tw;
			dlen = tw + 8 + delta;

			// Draw border
			//dc.SetPen(wxPen(wxColour(0,0,0)));
			dc.SetPen(*wxTRANSPARENT_PEN);
			if (syl.selected && !splitting) {
				dc.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)));
				dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHTTEXT));
			}
			else {
				dc.SetBrush(wxBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNHIGHLIGHT)));
				dc.SetTextForeground(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
			}
			dc.DrawRectangle(dx,0,dlen,h);

			// Put syllable-end-marker after text
			dc.SetPen(*wxBLACK_PEN);
			//dc.DrawLine(dx, 0, dx, h);
			dc.DrawLine(dx, 0, dx+1, 0);
			dc.DrawLine(dx, h-1, dx+1, h-1);
			dc.DrawLine(dx+dlen-1, 0, dx+dlen-1, h);
			dc.DrawLine(dx+dlen-2, 0, dx+dlen-1, 0);
			dc.DrawLine(dx+dlen-2, h-1, dx+dlen-1, h-1);

			// Draw text
			if (splitting) {
				// Make sure the text position is more predictable in case of splitting
				dc.DrawText(temptext,dx+4,(h-th)/2);
			} else {
				dc.DrawText(temptext,dx+(delta/2)+4,(h-th)/2);
			}

			// Draw pending splits
			if (syl.pending_splits.size() > 0) {
				wxArrayInt widths;
				if (dc.GetPartialTextExtents(temptext, widths)) {
					for (unsigned int j = 0; j < syl.pending_splits.size(); j++) {
						dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT));
						int splitxpos = dx + 4;
						// Handle splitters placed before first char in syllable; these are represented as -1
						if (syl.pending_splits[j] >= 0) {
							splitxpos += widths[syl.pending_splits[j]];
						} else {
							splitxpos += 0;
						}
						dc.DrawLine(splitxpos, 0, splitxpos, h);
					}
				} else {
					wxLogError(_T("Karaoke syllable display: Failed to GetPartialTextExtents. This should never happen, except on severely overloaded systems."));
				}
			}

			if (splitting && split_cursor_syl == (signed)i /*&& split_cursor_x > 0*/) {
				dc.SetPen(*wxRED);
				dc.DrawLine(dx+4+split_cursor_x, 0, dx+4+split_cursor_x, h);
				dc.SetPen(wxPen(wxColour(0,0,0)));
			}

			// Set syllable data
			syl.display_x = dx;
			syl.display_w = dlen;

			// Increment dx
			dx += dlen;
		}
	}

	event.Skip();
}



/// @brief Size event 
/// @param event 
///
void AudioKaraoke::OnSize(wxSizeEvent &event) {
	Refresh(false);
}



/// @brief Mouse event 
/// @param event 
/// @return 
///
void AudioKaraoke::OnMouse(wxMouseEvent &event) {
	// Get coordinates
	int x = event.GetX();
	//int y = event.GetY();
	bool shift = event.m_shiftDown;

	// Syllable selection mode
	if (!splitting) {
		int syl = GetSylAtX(x);

		// Button pressed
		if (event.LeftDown() || event.RightDown()) {
			if (syl != -1) {
				if (shift) {
					SetSelection(syl,startClickSyl);
					Refresh(false);
				}

				else {
					SetSelection(syl);
					startClickSyl = syl;
					curSyllable = syl;
					Refresh(false);
					display->Update();
					CaptureMouse();
				}
			}
		}
		// Dragging to make a selection
		else if (event.Dragging() && (event.LeftIsDown() || event.RightIsDown())) {
			if (syl < 0) syl = 0;
			SetSelection(syl, startClickSyl);
			Refresh(false);
		}
		// Released left button
		else if (event.LeftUp() && HasCapture()) {
			ReleaseMouse();
		}
		// Released right button; make a menu for selecting \k type
		else if (event.RightUp()) {
			if (HasCapture())
				ReleaseMouse();

			AudioKaraokeTagMenu menu(this);
			PopupMenu(&menu);
		}
	}
	
	// Karaoke syllable splitting mode
	else {
		int syli = GetSylAtX(x);

		// Valid syllable
		if (syli != -1) {
			AudioKaraokeSyllable &syl = syllables.at(syli);

			// Get the widths after each character in the text
			wxClientDC dc(this);
			wxFont curFont(9,wxFONTFAMILY_DEFAULT,wxFONTSTYLE_NORMAL,wxFONTWEIGHT_NORMAL,false,_T("Verdana"),wxFONTENCODING_SYSTEM);
			dc.SetFont(curFont);
			wxArrayInt widths;
			dc.GetPartialTextExtents(syl.text, widths);

			// Find the character closest to the mouse
			int rx = x - syl.display_x - 4;
			int split_cursor_char = -2;
			split_cursor_syl = -1;
			split_cursor_x = -1;
			if (syl.text.Len() > 0) {
				int lastx = 0;
				split_cursor_syl = syli;
				for (unsigned int i = 0; i < widths.size(); i++) {
					//wxLogDebug(_T("rx=%d, lastx=%d, widths[i]=%d, i=%d, widths.size()=%d, syli=%d"), rx, lastx, widths[i], i, widths.size(), syli);
					if (lastx - rx < widths[i] - rx) {
						if (rx - lastx < widths[i] - rx) {
							//wxLogDebug(_T("Found at PREV!"));
							split_cursor_x = lastx;
							split_cursor_char = i - 1;
							break;
						} else if (rx < widths[i]) {
							//wxLogDebug(_T("Found at CURRENT!"));
							split_cursor_x = widths[i];
							split_cursor_char = i;
							break;
						}
					}
					lastx = widths[i];
				}
				// If no split-point was caught by the for-loop, it must be over the last character,
				// ie. split at next-to-last position
				if (split_cursor_x < 0) {
					//wxLogDebug(_T("Emergency picking LAST!"));
					split_cursor_x = widths[widths.size()-1];
					split_cursor_char = widths.size() - 1;
				}
			}

			// Do something if there was a click and we're at a valid position
			if (event.LeftDown() && split_cursor_char >= -1) {
				//wxLogDebug(_T("A click!"));
				int num_removed = 0;
				std::vector<int>::iterator i = syl.pending_splits.begin();
				while (i != syl.pending_splits.end()) {
					if (split_cursor_char == *i) {
						//wxLogDebug(_T("Erasing entry"));
						num_removed++;
						syl.pending_splits.erase(i);
						break;
					} else {
						i++;
					}
				}
				if (num_removed == 0) {
					syl.pending_splits.push_back(split_cursor_char);
				}
			}

		}
		
		// Invalid syllable (int syli = GetSylAtX(x); returned -1)
		else {
			split_cursor_syl = -1;
			split_cursor_x = -1;
		}

		if (split_cursor_syl >= 0) {
			Refresh(false);
		}
	}
}



/// @brief Get Syllable at position X 
/// @param x 
/// @return 
///
int AudioKaraoke::GetSylAtX(int x) {
	int dx,dw;
	size_t syln = syllables.size();
	for (size_t i=0;i<syln;i++) {
		dx = syllables.at(i).display_x; 
		dw = syllables.at(i).display_w; 
		if (x >= dx && x < dx+dw) {
			return i;
		}
	}
	return -1;
}



/// @brief Set selection 
/// @param start 
/// @param end   
///
void AudioKaraoke::SetSelection(int start,int end) {
	wxLogDebug(_T("AudioKaraoke::SetSelection(start=%d, end=%d)"), start, end);
	// Default end
	if (end == -1) end = start;

	// Get min/max range
	size_t min = start;
	size_t max = end;
	if (max < min) {
		size_t temp = max;
		max = min;
		min = temp;
	}
	wxLogDebug(_T("AudioKaraoke::SetSelection: min=%d, max=%d"), min, max);

	// Set values
	bool state;
	size_t syls = syllables.size();
	int sels = 0;
	for (size_t i=0;i<syls;i++) {
		state = (i >= min && i <= max);
		syllables.at(i).selected = state;
		if (state) sels++;
	}
	curSyllable = min;
	selectionCount = max-min+1;
	wxLogDebug(_T("AudioKaraoke::SetSelection: new curSyllable=%d, selectionCount=%d"), curSyllable, selectionCount);

	// Set box buttons
	box->SetKaraokeButtons();
}



/// @brief Join syllables 
/// @return 
///
void AudioKaraoke::Join() {
	wxLogDebug(_T("AudioKaraoke::Join"));
	// Variables
	bool gotOne = false;
	size_t syls = syllables.size();
	AudioKaraokeSyllable *curSyl;
	int first = 0;

	// Loop
	for (size_t i=0;i<syls;i++) {
		curSyl = &syllables.at(i);
		wxLogDebug(_T("AudioKaraoke::Join: syllable %d, text='%s', unstripped_text='%s', duration=%d"), i, curSyl->text.c_str(), curSyl->unstripped_text.c_str(), curSyl->duration);
		if (curSyl->selected) {
			if (!gotOne) {
				gotOne = true;
				first = i;
			}
			else {
				AudioKaraokeSyllable &work = syllables.at(first);
				wxLogDebug(_T("AudioKaraoke::Join: worksyl %d, text='%s', unstripped_text='%s', duration=%d"), i, work.text.c_str(), work.unstripped_text.c_str(), work.duration);
				work.duration += curSyl->duration;
				work.text += curSyl->text;
				work.unstripped_text += curSyl->unstripped_text;
				syllables.erase(syllables.begin()+i);
				i--;
				syls--;
			}
		}
	}

	// Set selection
	SetSelection(first);

	// Update
	must_rebuild = true;
	display->NeedCommit = true;
	display->Update();
	Refresh(false);

	wxLogDebug(_T("AudioKaraoke::Join: returning"));
}



/// @brief Enter splitting-mode 
///
void AudioKaraoke::BeginSplit() {
	wxLogDebug(_T("AudioKaraoke::BeginSplit"));
	splitting = true;
	split_cursor_syl = -1;
	split_cursor_x = -1;
	box->SetKaraokeButtons();
	Refresh(false);
}



/// @brief Leave splitting-mode, committing changes 
/// @param commit 
/// @return 
///
void AudioKaraoke::EndSplit(bool commit) {
	wxLogDebug(_T("AudioKaraoke::EndSplit(commit=%d)"), commit?1:0);
	splitting = false;
	bool hasSplit = false;
	size_t first_sel = ~0U;
	for (size_t i = 0; i < syllables.size(); i ++) {
		if (syllables[i].pending_splits.size() > 0) {
			if (commit) {
				if (syllables[i].selected && i < first_sel)
					first_sel = i;
				SplitSyl(i);
				hasSplit = true;
			} else {
				syllables[i].pending_splits.clear();
			}
		}
	}

	// Update
	if (hasSplit) {
		wxLogDebug(_T("AudioKaraoke::EndSplit: hasSplit"));
		must_rebuild = true;
		display->NeedCommit = true;
		SetSelection(first_sel);
		display->Update();
	}
	// Always redraw, since the display is different in splitting mode
	box->SetKaraokeButtons();
	Refresh(false);

	wxLogDebug(_T("AudioKaraoke::EndSplit: returning"));
}



/// @brief Split a syllable using the pending_slits data 
/// @param n 
/// @return 
///
int AudioKaraoke::SplitSyl (unsigned int n) {
	wxLogDebug(_T("AudioKaraoke::SplitSyl(n=%u)"), n);

	// Avoid multiple vector resizes (this must be first)
	syllables.reserve(syllables.size() + syllables[n].pending_splits.size());

	// The syllable we're splitting
	AudioKaraokeSyllable &basesyl = syllables[n];
	wxLogDebug(_T("AudioKaraoke::SplitSyl: basesyl. contents='%s' selected=%d"), basesyl.unstripped_text.c_str(), basesyl.selected?1:0);

	// Start by sorting the split points
	std::sort(basesyl.pending_splits.begin(), basesyl.pending_splits.end());

	wxString originalText = basesyl.text;
	int originalDuration = basesyl.duration;

	// Fixup the first syllable
	basesyl.text = originalText.Mid(0, basesyl.pending_splits[0] + 1);
	basesyl.unstripped_text = basesyl.text;
	basesyl.selected = false;
	basesyl.duration = originalDuration * basesyl.text.Length() / originalText.Length();
	int curpos = basesyl.start_time + basesyl.duration;

	// For each split, make a new syllable
	for (unsigned int i = 0; i < basesyl.pending_splits.size(); i++) {
		AudioKaraokeSyllable newsyl;
		if (i < basesyl.pending_splits.size()-1) {
			// in the middle
			newsyl.text = originalText.Mid(basesyl.pending_splits[i]+1, basesyl.pending_splits[i+1] - basesyl.pending_splits[i]);
		} else {
			// the last one (take the rest)
			newsyl.text = originalText.Mid(basesyl.pending_splits[i]+1);
		}
		newsyl.unstripped_text = newsyl.text;
		newsyl.duration = originalDuration * newsyl.text.Length() / originalText.Length();
		newsyl.start_time = curpos;
		newsyl.type = basesyl.type;
		newsyl.selected = false;//basesyl.selected;
		wxLogDebug(_T("AudioKaraoke::SplitSyl: newsyl. contents='%s' selected=%d"), newsyl.text.c_str(), newsyl.selected?1:0);
		curpos += newsyl.duration;
		syllables.insert(syllables.begin()+n+i+1, newsyl);
	}

	// The total duration of the new syllables will be equal to or less than the original duration
	// Fix this, so they'll always add up
	// Use an unfair method, just adding 1 to each syllable one after another, until it's correct
	int newDuration = 0;
	for (unsigned int j = n; j < basesyl.pending_splits.size()+n+1; j++) {
		newDuration += syllables[j].duration;
	}
	unsigned int k = n;
	while (newDuration < originalDuration) {
		syllables[k].duration++;
		k++;
		if (k >= syllables.size()) {
			k = n;
		}
		newDuration++;
	}

	// Prepare for return and clear pending splits
	int numsplits = basesyl.pending_splits.size();
	basesyl.pending_splits.clear();
	return numsplits;
}



/// @brief Apply delta length to syllable 
/// @param n     
/// @param delta 
/// @param mode  
/// @return 
///
bool AudioKaraoke::SyllableDelta(int n,int delta,int mode) {
	wxLogDebug(_T("AudioKaraoke::SyllableDelta(n=%d, delta=%d, mode=%d)"), n, delta, mode);
	// Get syllable and next
	AudioKaraokeSyllable *curSyl=NULL,*nextSyl=NULL;
	curSyl = &syllables.at(n);
	int nkar = syllables.size();
	if (n < nkar-1) {
		nextSyl = &syllables.at(n+1);
	}

	// Get variables
	int len = curSyl->duration;

	// Cap delta
	int minLen = 0;
	if (len + delta < minLen) delta = minLen-len;
	if (mode == 0 && nextSyl && (nextSyl->duration - delta) < minLen) delta = nextSyl->duration - minLen;

	wxLogDebug(_T("AudioKaraoke::SyllableDelta: nkar=%d, len=%d, minLen=%d, delta=%d"), nkar, len, minLen, delta);

	// Apply
	if (delta != 0) {
		wxLogDebug(_T("AudioKaraoke::SyllableDelta: delta != 0"));
		curSyl->duration += delta;

		// Normal mode
		if (mode == 0 && nextSyl) {
			wxLogDebug(_T("AudioKaraoke::SyllableDelta: normal mode"));
			nextSyl->duration -= delta;
			nextSyl->start_time += delta;
		}

		// Shift mode
		if (mode == 1) {
			wxLogDebug(_T("AudioKaraoke::SyllableDelta: shift mode"));
			for (int i=n+1;i<nkar;i++) {
				syllables.at(i).start_time += delta;
			}
		}

		// Flag update
		wxLogDebug(_T("AudioKaraoke::SyllableDelta: return true"));
		return true;
	}
	wxLogDebug(_T("AudioKaraoke::SyllableDelta: return false"));
	return false;
}



/// @brief Karaoke tag menu constructor 
/// @param _kara 
///
AudioKaraokeTagMenu::AudioKaraokeTagMenu(AudioKaraoke *_kara)
: wxMenu(_("Karaoke tag"))
, kara(_kara)
{
	// Create menu items
	AppendCheckItem(10001, _T("\\k"), _("Change karaoke tag to \\k"));
	AppendCheckItem(10002, _T("\\kf / \\K"), _("Change karaoke tag to \\kf"));
	AppendCheckItem(10003, _T("\\ko"), _("Change karaoke tag to \\ko"));

	// Find out what kinds of tags are in use atm
	for (size_t i = 0; i < kara->syllables.size(); i++) {
		AudioKaraokeSyllable &syl = kara->syllables[i];
		if (syl.selected) {
			if (syl.type == _T("\\k")) {
				Check(10001, true);
			} else if (syl.type == _T("\\kf") || syl.type == _T("\\K")) {
				Check(10002, true);
			} else if (syl.type == _T("\\ko")) {
				Check(10003, true);
			}
		}
	}
}



/// @brief Karaoke tag menu destructor 
///
AudioKaraokeTagMenu::~AudioKaraokeTagMenu() {
}


///////////////
// Event table
BEGIN_EVENT_TABLE(AudioKaraokeTagMenu,wxMenu)
	EVT_MENU_RANGE(10001, 10003, AudioKaraokeTagMenu::OnSelectItem)
END_EVENT_TABLE()



/// @brief Karaoke tag menu event handler 
/// @param event 
///
void AudioKaraokeTagMenu::OnSelectItem(wxCommandEvent &event) {
	// Select the new tag for the syllables
	wxString newtag;
	switch (event.GetId()) {
		case 10001: newtag = _T("\\k"); break;
		case 10002: newtag = _T("\\kf"); break;
		case 10003: newtag = _T("\\ko"); break;
		default: return;
	}

	// Apply it
	size_t firstsel = kara->syllables.size();
	int lastsel = -1;
	for (size_t i = 0; i < kara->syllables.size(); i++) {
		AudioKaraokeSyllable &syl = kara->syllables[i];
		if (syl.selected) {
			if (firstsel > i) firstsel = i;
			lastsel = i;
			syl.type = newtag;
		}
	}

	// Update display
	kara->must_rebuild = true;
	//kara->Commit();
	kara->display->NeedCommit = true;
	kara->display->CommitChanges();
	//kara->display->Update();
	kara->SetSelection(firstsel, lastsel);
}




