/**
 * Copyright (C) 2007-2009 Sergio Pistone <sergio_pistone@yahoo.com.ar>
 * Copyright (C) 2010-2015 Mladen Milinkovic <max@smoothware.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "subtitleline.h"
#include "subtitlelineactions.h"
#include "subtitle.h"

#include <QRegExp>

#include <KLocalizedString>

#include <math.h>

using namespace SubtitleComposer;

/// "magic" code taken from http://tekpool.wordpress.com/category/bit-count/
int
SubtitleLine::bitsCount(unsigned int u)
{
	unsigned int uCount;
	uCount = u - ((u >> 1) & 033333333333) - ((u >> 2) & 011111111111);
	return ((uCount + (uCount >> 3)) & 030707070707) % 63;
}

SubtitleLine::ErrorFlag
SubtitleLine::errorFlag(SubtitleLine::ErrorID id)
{
	if(id < 0 || id >= ErrorSIZE)
		return (ErrorFlag)0;

	return (ErrorFlag)(0x1 << id);
}

SubtitleLine::ErrorID
SubtitleLine::errorID(SubtitleLine::ErrorFlag flag)
{
	if(flag < 1)
		return ErrorUNKNOWN;

	int id = log2(static_cast<int>(flag));
	return id < ErrorSIZE ? (ErrorID)id : ErrorUNKNOWN;
}

/// ERRORS DESCRIPTIONS
/// ===================

const QString &
SubtitleLine::simpleErrorText(SubtitleLine::ErrorFlag errorFlag)
{
	return simpleErrorText(errorID(errorFlag));
}

const QString &
SubtitleLine::simpleErrorText(SubtitleLine::ErrorID errorID)
{
	static const QString empty;
	static QString texts[ErrorSIZE];

	if(texts[EmptyPrimaryTextID].isEmpty()) {
		texts[EmptyPrimaryTextID] = i18n("Empty text");
		texts[EmptySecondaryTextID] = texts[EmptySecondaryTextID - 1];
		texts[MaxPrimaryCharsID] = i18n("Too many characters");
		texts[MaxSecondaryCharsID] = texts[MaxSecondaryCharsID - 1];
		texts[MaxPrimaryLinesID] = i18n("Too many lines");
		texts[MaxSecondaryLinesID] = texts[MaxSecondaryLinesID - 1];
		texts[PrimaryUnneededSpacesID] = i18n("Unnecessary white space");
		texts[SecondaryUnneededSpacesID] = texts[SecondaryUnneededSpacesID - 1];
		texts[PrimaryUnneededDashID] = i18n("Unnecessary dash");
		texts[SecondaryUnneededDashID] = texts[SecondaryUnneededDashID - 1];
		texts[PrimaryCapitalAfterEllipsisID] = i18n("Capital letter after continuation ellipsis");
		texts[SecondaryCapitalAfterEllipsisID] = texts[SecondaryCapitalAfterEllipsisID - 1];
		texts[MaxDurationPerPrimaryCharID] = i18n("Too long duration per character");
		texts[MaxDurationPerSecondaryCharID] = texts[MaxDurationPerSecondaryCharID - 1];
		texts[MinDurationPerPrimaryCharID] = i18n("Too short duration per character");
		texts[MinDurationPerSecondaryCharID] = texts[MinDurationPerSecondaryCharID - 1];
		texts[MaxDurationID] = i18n("Too long duration");
		texts[MinDurationID] = i18n("Too short duration");
		texts[OverlapsWithNextID] = i18n("Overlapping times");
		texts[UntranslatedTextID] = i18n("Untranslated text");
		texts[UserMarkID] = i18n("User mark");
	}

	return (errorID < 0 || errorID >= ErrorSIZE) ? empty : texts[errorID];
}

QString
SubtitleLine::fullErrorText(SubtitleLine::ErrorFlag errorFlag) const
{
	return fullErrorText(errorID(errorFlag));
}

QString
SubtitleLine::fullErrorText(SubtitleLine::ErrorID errorID) const
{
	if(!(m_errorFlags & (0x1 << errorID)))
		return QString();

	switch(errorID) {
	case EmptyPrimaryTextID: return i18n("Has no primary text.");
	case EmptySecondaryTextID: return i18n("Has no translation text.");
	case MaxPrimaryCharsID: return i18np("Has too many characters in primary text (1 char).", "Has too many characters in primary text (%1 chars).", primaryCharacters());
	case MaxSecondaryCharsID: return i18np("Has too many characters in translation text (1 char).", "Has too many characters in translation text (%1 chars).", secondaryCharacters());
	case MaxPrimaryLinesID: return i18np("Has too many lines in primary text (1 line).", "Has too many lines in primary text (%1 lines).", primaryLines());
	case MaxSecondaryLinesID: return i18np("Has too many lines in translation text (1 line).", "Has too many lines in translation text (%1 lines).", secondaryLines());
	case PrimaryUnneededSpacesID: return i18n("Has unnecessary spaces in primary text.");
	case SecondaryUnneededSpacesID: return i18n("Has unnecessary spaces in translation text.");
	case PrimaryUnneededDashID: return i18n("Has unnecessary dash in primary text.");
	case SecondaryUnneededDashID: return i18n("Has unnecessary dash in translation text.");
	case PrimaryCapitalAfterEllipsisID: return i18n("Has capital letter after line continuation ellipsis in primary text.");
	case SecondaryCapitalAfterEllipsisID: return i18n("Has capital letter after line continuation ellipsis in translation text.");
	case MaxDurationPerPrimaryCharID: return i18np("Has too long duration per character in primary text (1 msec/char).", "Has too long duration per character in primary text (%1 msecs/char).", durationTime().toMillis() / primaryCharacters());
	case MaxDurationPerSecondaryCharID: return i18np("Has too long duration per character in translation text (1 msec/char).", "Has too long duration per character in translation text (%1 msecs/char).", durationTime().toMillis() / secondaryCharacters());
	case MinDurationPerPrimaryCharID: return i18np("Has too short duration per character in primary text (1 msec/char).", "Has too short duration per character in primary text (%1 msecs/char).", durationTime().toMillis() / primaryCharacters());
	case MinDurationPerSecondaryCharID: return i18np("Has too short duration per character in translation text (1 msec/char).", "Has too short duration per character in translation text (%1 msecs/char).", durationTime().toMillis() / secondaryCharacters());
	case MaxDurationID: return i18np("Has too long duration (1 msec).", "Has too long duration (%1 msecs).", durationTime().toMillis());
	case MinDurationID: return i18np("Has too short duration (1 msec).", "Has too short duration (%1 msecs).", durationTime().toMillis());
	case OverlapsWithNextID: return i18n("Overlaps with next line.");
	case UntranslatedTextID: return i18n("Has untranslated text.");
	case UserMarkID: return i18n("Has user mark.");
	default: return QString();
	}
}

SubtitleLine::SubtitleLine(const SString &pText, const SString &sText) :
	QObject(),
	m_subtitle(0),
	m_primaryText(pText),
	m_secondaryText(sText),
	m_showTime(),
	m_hideTime(),
	m_errorFlags(0),
	m_cachedIndex(-1),
	m_formatData(0)
{}

SubtitleLine::SubtitleLine(const SString &pText, const Time &showTime, const Time &hideTime) :
	QObject(),
	m_subtitle(0),
	m_primaryText(pText),
	m_secondaryText(QString()),
	m_showTime(showTime),
	m_hideTime(hideTime),
	m_errorFlags(0),
	m_cachedIndex(-1),
	m_formatData(0)
{}

SubtitleLine::SubtitleLine(const SString &pText, const SString &sText, const Time &showTime, const Time &hideTime) :
	QObject(),
	m_subtitle(0),
	m_primaryText(pText),
	m_secondaryText(sText),
	m_showTime(showTime),
	m_hideTime(hideTime),
	m_errorFlags(0),
	m_cachedIndex(-1),
	m_formatData(0)
{}

SubtitleLine::SubtitleLine(const SubtitleLine &line) :
	QObject(),
	m_subtitle(0),
	m_primaryText(line.m_primaryText),
	m_secondaryText(line.m_secondaryText),
	m_showTime(line.m_showTime),
	m_hideTime(line.m_hideTime),
	m_errorFlags(line.m_errorFlags),
	m_cachedIndex(-1),
	m_formatData(0)
{}

SubtitleLine &
SubtitleLine::operator=(const SubtitleLine &line)
{
	if(this == &line)
		return *this;

	m_primaryText = line.m_primaryText;
	m_secondaryText = line.m_secondaryText;
	m_showTime = line.m_showTime;
	m_hideTime = line.m_hideTime;
	m_errorFlags = line.m_errorFlags;

	return *this;
}

SubtitleLine::~SubtitleLine()
{
	delete m_formatData;
}

FormatData *
SubtitleLine::formatData() const
{
	return m_formatData;
}

void
SubtitleLine::setFormatData(const FormatData *formatData)
{
	delete m_formatData;

	m_formatData = formatData ? new FormatData(*formatData) : NULL;
}

int
SubtitleLine::number() const
{
	return index() + 1;
}

int
SubtitleLine::index() const
{
	if(!m_subtitle)
		return -1;

	if(m_cachedIndex < 0 || (m_cachedIndex > m_subtitle->m_lastValidCachedIndex)) {
		SubtitleLine *line;
		for(int index = m_subtitle->m_lastValidCachedIndex + 1, size = m_subtitle->m_lines.count(); index < size; ++index) {
			line = m_subtitle->m_lines.at(index);
			line->m_cachedIndex = index;
			if(line == this) {
				m_subtitle->setLastValidCachedIndex(index);
				break;
			}
		}
//		qDebug() << "searched index" << m_cachedIndex;
	}
//	qDebug() << "index" << m_cachedIndex << "| text" << m_primaryText.string();
	return m_cachedIndex;
}

Subtitle *
SubtitleLine::subtitle()
{
	return m_subtitle;
}

const Subtitle *
SubtitleLine::subtitle() const
{
	return m_subtitle;
}

SubtitleLine *
SubtitleLine::prevLine()
{
	return m_subtitle ? m_subtitle->line(index() - 1) : NULL;
}

SubtitleLine *
SubtitleLine::nextLine()
{
	return m_subtitle ? m_subtitle->line(index() + 1) : NULL;
}

const SString &
SubtitleLine::primaryText() const
{
	return m_primaryText;
}

void
SubtitleLine::setPrimaryText(const SString &pText)
{
	if(m_primaryText != pText)
		processAction(new SetLinePrimaryTextAction(*this, pText));
}

const SString &
SubtitleLine::secondaryText() const
{
	return m_secondaryText;
}

void
SubtitleLine::setSecondaryText(const SString &sText)
{
	if(m_secondaryText != sText)
		processAction(new SetLineSecondaryTextAction(*this, sText));
}

void
SubtitleLine::setTexts(const SString &pText, const SString &sText)
{
	if(m_primaryText != pText || m_secondaryText != sText)
		processAction(new SetLineTextsAction(*this, pText, sText));
}

SString
SubtitleLine::fixPunctuation(const SString &t, bool spaces, bool quotes, bool englishI, bool ellipsis, bool *cont)
{
	if(!t.length())
		return t;

	SString text(t);

	if(spaces) {
		text = simplifyTextWhiteSpace(text);

		// remove spaces after " or ' at the beginning of line
		text.replace(QRegExp("^([\"'])\\s"), "\\1");

		// remove space before " or ' at the end of line
		text.replace(QRegExp("\\s([\"'])$"), "\\1");

		// if not present, add space after '?', '!', ',', ';', ':', ')' and ']'
		text.replace(QRegExp("([\\?!,;:\\)\\]])([^\\s\"'])"), "\\1 \\2");

		// if not present, add space after '.'
		text.replace(QRegExp("(\\.)([^\\s\\.\"'])"), "\\1 \\2");

		// remove space after '¿', '¡', '(' and '['
		text.replace(QRegExp("([¿¡\\(\\[])\\s"), "\\1");

		// remove space before '?', '!', ',', ';', ':', '.', ')' and ']'
		text.replace(QRegExp("\\s([\\?!,;:\\.\\)\\]])"), "\\1");

		// remove space after ... at the beginning of sentence
		text.replace(QRegExp("^\\.\\.\\.?\\s"), "...");
	}

	if(quotes) { // quotes and double quotes
		text.replace(QRegExp("`|´|"), "'");
		text.replace(QRegExp("''|«|»"), "\"");
	}

	if(englishI) // fix english I pronoun capitalization
		text.replace(QRegExp("([\\s\"'\\(\\[])i([\\s'\",;:\\.\\?!\\]\\)]|$)"), "\\1I\\2");

	if(ellipsis) {                          // fix ellipsis
		text.replace(QRegExp("[,;]?\\.{2,}"), "...");
		text.replace(QRegExp("[,;]\\s*$"), "...");

		if(text.indexOf(QRegExp("[\\.:?!\\)\\]'\\\"]$")) == -1)
			text.append("...");

		if(cont) {
			if(*cont && text.indexOf(QRegExp("^\\s*\\.{3}[^\\.]?")) == -1)
				text.replace(QRegExp("^\\s*\\.*\\s*"), "...");

			*cont = text.indexOf(QRegExp("\\.{3,3}\\s*$")) != -1;
		}
	} else {
		if(cont) {
			*cont = text.indexOf(QRegExp("[?!\\)\\]'\\\"]\\s*$")) == -1;

			if(!*cont)
				*cont = text.indexOf(QRegExp("[^\\.]?\\.\\s*$")) == -1;
		}
	}

	return text;
}

SString
SubtitleLine::breakText(const SString &t, int minLengthForBreak)
{
	Q_ASSERT(minLengthForBreak >= 0);

	SString text(t);
	text.replace('\n', ' ');

	if(text.length() <= minLengthForBreak)
		return text;

	static const QRegExp spaceRegExp("[^ \t\n][ \t\n]");

	// this is a fricking test case
	// this is a fri|cking test case    IDEAL: 13
	// this is a| fricking test case    LOWER: 10 <--
	// this is a fricking| test case    UPPER: 18

	int position = text.length() / 2;
	int lowerPosition = text.lastIndexOf(spaceRegExp, position);
	int upperPosition = text.indexOf(spaceRegExp, position);

	if((lowerPosition == -1 || lowerPosition == 0) && (upperPosition == -1 || upperPosition == (text.length() - 1)))
		return text;
	else if(position - lowerPosition <= upperPosition - position)
		position = lowerPosition;
	else
		position = upperPosition;

	position++;
	text.insert(position, "\n");
	position++;
	while(text.at(position).isSpace() && position < text.length())
		text.remove(position, 1);

	return text;
}

void
SubtitleLine::breakText(int minLengthForBreak, TextTarget target)
{
	switch(target) {
	case Primary:
		setPrimaryText(breakText(m_primaryText, minLengthForBreak));
		break;
	case Secondary:
		setSecondaryText(breakText(m_secondaryText, minLengthForBreak));
		break;
	case Both:
		setTexts(breakText(m_primaryText, minLengthForBreak), breakText(m_secondaryText, minLengthForBreak));
		break;
	default:
		break;
	}
}

void
SubtitleLine::unbreakText(TextTarget target)
{
	switch(target) {
	case Primary:
		setPrimaryText(SString(primaryText()).replace('\n', ' '));
		break;
	case Secondary:
		setSecondaryText(SString(secondaryText()).replace('\n', ' '));
		break;
	case Both:
		setTexts(SString(primaryText()).replace('\n', ' '), SString(secondaryText()).replace('\n', ' '));
		break;
	default:
		break;
	}
}

QString
SubtitleLine::simplifyTextWhiteSpace(QString text)
{
	static const QRegExp regExp1(" *\n *");
	static const QRegExp regExp2(" +");
	static const QRegExp regExp3("\n+");

	text.replace('\t', ' ');
	text.replace(regExp1, "\n");
	text.replace(regExp2, " ");
	text.replace(regExp3, "\n");

	return text.trimmed();
}

SString
SubtitleLine::simplifyTextWhiteSpace(SString text)
{
	static const QRegExp regExp1(" *\n *");
	static const QRegExp regExp2(" +");
	static const QRegExp regExp3("\n+");

	text.replace('\t', ' ');
	text.replace(regExp1, "\n");
	text.replace(regExp2, " ");
	text.replace(regExp3, "\n");

	return text.trimmed();
}

void
SubtitleLine::simplifyTextWhiteSpace(TextTarget target)
{
	switch(target) {
	case Primary:
		setPrimaryText(simplifyTextWhiteSpace(m_primaryText));
		break;
	case Secondary:
		setSecondaryText(simplifyTextWhiteSpace(m_secondaryText));
		break;
	case Both:
		setTexts(simplifyTextWhiteSpace(m_primaryText), simplifyTextWhiteSpace(m_secondaryText));
		break;
	default:
		break;
	}
}

Time
SubtitleLine::showTime() const
{
	return m_showTime;
}

void
SubtitleLine::setShowTime(const Time &showTime)
{
	if(m_showTime == showTime)
		return;

	if(m_subtitle && m_subtitle->isLineAnchored(this)) {
		m_subtitle->beginCompositeAction(i18n("Set Line Show Time"));
		m_subtitle->shiftAnchoredLine(this, showTime);
		m_subtitle->endCompositeAction();
	} else {
		processAction(new SetLineShowTimeAction(*this, showTime));
	}
}

Time
SubtitleLine::hideTime() const
{
	return m_hideTime;
}

void
SubtitleLine::setHideTime(const Time &hideTime)
{
	if(m_hideTime != hideTime)
		processAction(new SetLineHideTimeAction(*this, hideTime));
}

Time
SubtitleLine::durationTime() const
{
	return Time(m_hideTime.toMillis() - m_showTime.toMillis());
}

void
SubtitleLine::setDurationTime(const Time &durationTime)
{
	setHideTime(m_showTime + durationTime);
}

void
SubtitleLine::setTimes(const Time &showTime, const Time &hideTime)
{
	if(m_showTime == showTime && m_hideTime == hideTime)
		return;

	if(m_subtitle && m_subtitle->isLineAnchored(this)) {
		m_subtitle->beginCompositeAction(i18n("Set Line Times"));
		m_subtitle->shiftAnchoredLine(this, showTime);
		m_subtitle->endCompositeAction();
	} else {
		processAction(new SetLineTimesAction(*this, showTime, hideTime, i18n("Set Line Times")));
	}
}

int
SubtitleLine::primaryCharacters() const
{
	return m_primaryText.string().simplified().length();
}

int
SubtitleLine::primaryWords() const
{
	QString text(m_primaryText.string().simplified());
	return text.length() ? text.count(' ') + 1 : 0;
}

int
SubtitleLine::primaryLines() const
{
	QString text(simplifyTextWhiteSpace(m_primaryText.string()));
	return text.length() ? text.count('\n') + 1 : 0;
}

int
SubtitleLine::secondaryCharacters() const
{
	return m_secondaryText.string().simplified().length();
}

int
SubtitleLine::secondaryWords() const
{
	QString text(m_secondaryText.string().simplified());
	return text.length() ? text.count(' ') + 1 : 0;
}

int
SubtitleLine::secondaryLines() const
{
	QString text(simplifyTextWhiteSpace(m_secondaryText.string()));
	return text.length() ? text.count('\n') + 1 : 0;
}

Time
SubtitleLine::autoDuration(const QString &t, int msecsPerChar, int msecsPerWord, int msecsPerLine)
{
	Q_ASSERT(msecsPerChar >= 0);
	Q_ASSERT(msecsPerWord >= 0);
	Q_ASSERT(msecsPerLine >= 0);

	QString text(simplifyTextWhiteSpace(t));
	if(!text.length())
		return 0;

	int chars = text.length();
	int lines = text.count('\n') + 1;
	int words = text.count(' ') + lines;

	return chars * msecsPerChar + words * msecsPerWord + lines * msecsPerLine;
}

Time
SubtitleLine::autoDuration(int msecsPerChar, int msecsPerWord, int msecsPerLine, TextTarget calculationTarget)
{
	switch(calculationTarget) {
	case Secondary:
		return autoDuration(m_secondaryText.string(), msecsPerChar, msecsPerWord, msecsPerLine);
	case Both: {
		Time primary = autoDuration(m_primaryText.string(), msecsPerChar, msecsPerWord, msecsPerLine);
		Time secondary = autoDuration(m_secondaryText.string(), msecsPerChar, msecsPerWord, msecsPerLine);
		return primary > secondary ? primary : secondary;
	}
	case Primary:
	default:
		return autoDuration(m_primaryText.string(), msecsPerChar, msecsPerWord, msecsPerLine);
	}
}

void
SubtitleLine::shiftTimes(long mseconds)
{
	if(!mseconds)
		return;

	processAction(new SetLineTimesAction(*this, m_showTime.shifted(mseconds), m_hideTime.shifted(mseconds), i18n("Shift Line Times")));
}

void
SubtitleLine::adjustTimes(double shiftMseconds, double scaleFactor)
{
	if(shiftMseconds || scaleFactor != 1.0)
		processAction(new SetLineTimesAction(*this, m_showTime.adjusted(shiftMseconds, scaleFactor), m_hideTime.adjusted(shiftMseconds, scaleFactor), i18n("Adjust Line Times")));
}

/// ERRORS

int
SubtitleLine::errorFlags() const
{
	return m_errorFlags;
}

int
SubtitleLine::errorCount() const
{
	return bitsCount(m_errorFlags);
}

void
SubtitleLine::setErrorFlags(int errorFlags)
{
	if(m_errorFlags != errorFlags)
		processAction(new SetLineErrorsAction(*this, errorFlags));
}

void
SubtitleLine::setErrorFlags(int errorFlags, bool value)
{
	setErrorFlags(value ? (m_errorFlags | errorFlags) : (m_errorFlags & ~errorFlags));
}

bool
SubtitleLine::checkEmptyPrimaryText(bool update)
{
	static const QRegExp emptyTextRegExp("^\\s*$");

	bool error = m_primaryText.isEmpty() || m_primaryText.indexOf(emptyTextRegExp) != -1;

	if(update)
		setErrorFlags(EmptyPrimaryText, error);

	return error;
}

bool
SubtitleLine::checkEmptySecondaryText(bool update)
{
	static const QRegExp emptyTextRegExp("^\\s*$");

	bool error = m_secondaryText.isEmpty() || m_secondaryText.indexOf(emptyTextRegExp) != -1;

	if(update)
		setErrorFlags(EmptySecondaryText, error);

	return error;
}

bool
SubtitleLine::checkUntranslatedText(bool update)
{
	bool error = m_primaryText.string() == m_secondaryText.string();

	if(update)
		setErrorFlags(UntranslatedText, error);

	return error;
}

bool
SubtitleLine::checkOverlapsWithNext(bool update)
{
	SubtitleLine *nextLine = this->nextLine();
	bool error = nextLine && nextLine->m_showTime <= m_hideTime;

	if(update)
		setErrorFlags(OverlapsWithNext, error);

	return error;
}

bool
SubtitleLine::checkMinDuration(int minMsecs, bool update)
{
	Q_ASSERT(minMsecs >= 0);

	bool error = durationTime().toMillis() < minMsecs;

	if(update)
		setErrorFlags(MinDuration, error);

	return error;
}

bool
SubtitleLine::checkMaxDuration(int maxMsecs, bool update)
{
	Q_ASSERT(maxMsecs >= 0);

	bool error = durationTime().toMillis() > maxMsecs;

	if(update)
		setErrorFlags(MaxDuration, error);

	return error;
}

bool
SubtitleLine::checkMinDurationPerPrimaryChar(int minMsecsPerChar, bool update)
{
	Q_ASSERT(minMsecsPerChar >= 0);

	int characters = primaryCharacters();
	bool error = characters ? (durationTime().toMillis() / characters < minMsecsPerChar) : false;

	if(update)
		setErrorFlags(MinDurationPerPrimaryChar, error);

	return error;
}

bool
SubtitleLine::checkMinDurationPerSecondaryChar(int minMsecsPerChar, bool update)
{
	Q_ASSERT(minMsecsPerChar >= 0);

	int characters = secondaryCharacters();
	bool error = characters ? (durationTime().toMillis() / characters < minMsecsPerChar) : false;

	if(update)
		setErrorFlags(MinDurationPerSecondaryChar, error);

	return error;
}

bool
SubtitleLine::checkMaxDurationPerPrimaryChar(int maxMsecsPerChar, bool update)
{
	Q_ASSERT(maxMsecsPerChar >= 0);

	int characters = primaryCharacters();
	bool error = characters ? (durationTime().toMillis() / characters > maxMsecsPerChar) : false;

	if(update)
		setErrorFlags(MaxDurationPerPrimaryChar, error);

	return error;
}

bool
SubtitleLine::checkMaxDurationPerSecondaryChar(int maxMsecsPerChar, bool update)
{
	Q_ASSERT(maxMsecsPerChar >= 0);

	int characters = secondaryCharacters();
	bool error = characters ? (durationTime().toMillis() / characters > maxMsecsPerChar) : false;

	if(update)
		setErrorFlags(MaxDurationPerSecondaryChar, error);

	return error;
}

bool
SubtitleLine::checkMaxPrimaryChars(int maxCharacters, bool update)
{
	Q_ASSERT(maxCharacters >= 0);

	bool error = primaryCharacters() > maxCharacters;

	if(update)
		setErrorFlags(MaxPrimaryChars, error);

	return error;
}

bool
SubtitleLine::checkMaxSecondaryChars(int maxCharacters, bool update)
{
	Q_ASSERT(maxCharacters >= 0);

	bool error = secondaryCharacters() > maxCharacters;

	if(update)
		setErrorFlags(MaxSecondaryChars, error);

	return error;
}

bool
SubtitleLine::checkMaxPrimaryLines(int maxLines, bool update)
{
	Q_ASSERT(maxLines >= 0);

	bool error = primaryLines() > maxLines;

	if(update)
		setErrorFlags(MaxPrimaryLines, error);

	return error;
}

bool
SubtitleLine::checkMaxSecondaryLines(int maxLines, bool update)
{
	Q_ASSERT(maxLines >= 0);

	bool error = secondaryLines() > maxLines;

	if(update)
		setErrorFlags(MaxSecondaryLines, error);

	return error;
}

bool
SubtitleLine::checkPrimaryUnneededSpaces(bool update)
{
	static const QRegExp unneededSpaceRegExp("(^\\s|\\s$|¿\\s|¡\\s|\\s\\s|\\s!|\\s\\?|\\s:|\\s;|\\s,|\\s\\.)");

	bool error = m_primaryText.indexOf(unneededSpaceRegExp) != -1;

	if(update)
		setErrorFlags(PrimaryUnneededSpaces, error);

	return error;
}

bool
SubtitleLine::checkSecondaryUnneededSpaces(bool update)
{
	static const QRegExp unneededSpaceRegExp("(^\\s|\\s$|¿\\s|¡\\s|\\s\\s|\\s!|\\s\\?|\\s:|\\s;|\\s,|\\s\\.)");

	bool error = m_secondaryText.indexOf(unneededSpaceRegExp) != -1;

	if(update)
		setErrorFlags(SecondaryUnneededSpaces, error);

	return error;
}

bool
SubtitleLine::checkPrimaryCapitalAfterEllipsis(bool update)
{
	QRegExp capitalAfterEllipsisRegExp("^\\s*\\.\\.\\.[¡¿\\.,;\\(\\[\\{\"'\\s]*");

	bool error = capitalAfterEllipsisRegExp.indexIn(m_primaryText.string(), QRegExp::CaretAtZero) != -1;
	if(error) {
		QChar chr = m_primaryText.at(capitalAfterEllipsisRegExp.matchedLength());
		error = chr.isLetter() && chr == chr.toUpper();
	}

	if(update)
		setErrorFlags(PrimaryCapitalAfterEllipsis, error);

	return error;
}

bool
SubtitleLine::checkSecondaryCapitalAfterEllipsis(bool update)
{
	QRegExp capitalAfterEllipsisRegExp("^\\s*\\.\\.\\.[¡¿\\.,;\\(\\[\\{\"'\\s]*");

	bool error = capitalAfterEllipsisRegExp.indexIn(m_secondaryText.string(), QRegExp::CaretAtZero) != -1;
	if(error) {
		QChar chr = m_secondaryText.at(capitalAfterEllipsisRegExp.matchedLength());
		error = chr.isLetter() && chr == chr.toUpper();
	}

	if(update)
		setErrorFlags(SecondaryCapitalAfterEllipsis, error);

	return error;
}

bool
SubtitleLine::checkPrimaryUnneededDash(bool update)
{
	static const QRegExp unneededDashRegExp("(^|\n)\\s*-[^-]");

	bool error = m_primaryText.count(unneededDashRegExp) == 1;

	if(update)
		setErrorFlags(PrimaryUnneededDash, error);

	return error;
}

bool
SubtitleLine::checkSecondaryUnneededDash(bool update)
{
	static const QRegExp unneededDashRegExp("(^|\n)\\s*-[^-]");

	bool error = m_secondaryText.count(unneededDashRegExp) == 1;

	if(update)
		setErrorFlags(SecondaryUnneededDash, error);

	return error;
}

int
SubtitleLine::check(int errorFlagsToCheck, int minDurationMsecs, int maxDurationMsecs, int minMsecsPerChar, int maxMsecsPerChar, int maxChars, int maxLines, bool update)
{
	int lineErrorFlags = m_errorFlags & ~errorFlagsToCheck; // clear the flags we're going to (re)check

	if(errorFlagsToCheck & EmptyPrimaryText)
		if(checkEmptyPrimaryText(false))
			lineErrorFlags |= EmptyPrimaryText;

	if(errorFlagsToCheck & EmptySecondaryText)
		if(checkEmptySecondaryText(false))
			lineErrorFlags |= EmptySecondaryText;

	if(errorFlagsToCheck & OverlapsWithNext)
		if(checkOverlapsWithNext(false))
			lineErrorFlags |= OverlapsWithNext;

	if(errorFlagsToCheck & UntranslatedText)
		if(checkUntranslatedText(false))
			lineErrorFlags |= UntranslatedText;

	if(errorFlagsToCheck & MaxDuration)
		if(checkMaxDuration(maxDurationMsecs, false))
			lineErrorFlags |= MaxDuration;

	if(errorFlagsToCheck & MinDuration)
		if(checkMinDuration(minDurationMsecs, false))
			lineErrorFlags |= MinDuration;

	if(errorFlagsToCheck & MaxDurationPerPrimaryChar)
		if(checkMaxDurationPerPrimaryChar(maxMsecsPerChar, false))
			lineErrorFlags |= MaxDurationPerPrimaryChar;

	if(errorFlagsToCheck & MaxDurationPerSecondaryChar)
		if(checkMaxDurationPerSecondaryChar(maxMsecsPerChar, false))
			lineErrorFlags |= MaxDurationPerSecondaryChar;

	if(errorFlagsToCheck & MinDurationPerPrimaryChar)
		if(checkMinDurationPerPrimaryChar(minMsecsPerChar, false))
			lineErrorFlags |= MinDurationPerPrimaryChar;

	if(errorFlagsToCheck & MinDurationPerSecondaryChar)
		if(checkMinDurationPerSecondaryChar(minMsecsPerChar, false))
			lineErrorFlags |= MinDurationPerSecondaryChar;

	if(errorFlagsToCheck & MaxPrimaryChars)
		if(checkMaxPrimaryChars(maxChars, false))
			lineErrorFlags |= MaxPrimaryChars;

	if(errorFlagsToCheck & MaxSecondaryChars)
		if(checkMaxSecondaryChars(maxChars, false))
			lineErrorFlags |= MaxSecondaryChars;

	if(errorFlagsToCheck & MaxPrimaryLines)
		if(checkMaxPrimaryLines(maxLines, false))
			lineErrorFlags |= MaxPrimaryLines;

	if(errorFlagsToCheck & MaxSecondaryLines)
		if(checkMaxSecondaryLines(maxLines, false))
			lineErrorFlags |= MaxSecondaryLines;

	if(errorFlagsToCheck & MaxSecondaryLines)
		if(checkMaxSecondaryLines(maxLines, false))
			lineErrorFlags |= MaxSecondaryLines;

	if(errorFlagsToCheck & PrimaryUnneededSpaces)
		if(checkPrimaryUnneededSpaces(false))
			lineErrorFlags |= PrimaryUnneededSpaces;

	if(errorFlagsToCheck & SecondaryUnneededSpaces)
		if(checkSecondaryUnneededSpaces(false))
			lineErrorFlags |= SecondaryUnneededSpaces;

	if(errorFlagsToCheck & PrimaryCapitalAfterEllipsis)
		if(checkPrimaryCapitalAfterEllipsis(false))
			lineErrorFlags |= PrimaryCapitalAfterEllipsis;

	if(errorFlagsToCheck & SecondaryCapitalAfterEllipsis)
		if(checkSecondaryCapitalAfterEllipsis(false))
			lineErrorFlags |= SecondaryCapitalAfterEllipsis;

	if(errorFlagsToCheck & PrimaryUnneededDash)
		if(checkPrimaryUnneededDash(false))
			lineErrorFlags |= PrimaryUnneededDash;

	if(errorFlagsToCheck & SecondaryUnneededDash)
		if(checkSecondaryUnneededDash(false))
			lineErrorFlags |= SecondaryUnneededDash;

	if(update)
		setErrorFlags(lineErrorFlags);

	return lineErrorFlags;
}

void
SubtitleLine::processAction(Action *action)
{
	if(m_subtitle) {
		m_subtitle->processAction(action);
	} else {
		action->redo();
		delete action;
	}
}


