/*
 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
 * All rights reserved. Distributed under the terms of the MIT License.
 */
 
#include "Paragraph.h"
 
#include <algorithm>
#include <stdio.h>
 
 
Paragraph::Paragraph()
	:
	fStyle()
{
}
 
 
Paragraph::Paragraph(const ParagraphStyle& style)
	:
	fStyle(style),
	fTextSpans(),
	fCachedLength(-1)
{
}
 
 
Paragraph::Paragraph(const Paragraph& other)
	:
	fStyle(other.fStyle),
	fTextSpans(other.fTextSpans),
	fCachedLength(other.fCachedLength)
{
}
 
 
Paragraph&
Paragraph::operator=(const Paragraph& other)
{
	fStyle = other.fStyle;
	fTextSpans = other.fTextSpans;
	fCachedLength = other.fCachedLength;
 
	return *this;
}
 
 
bool
Paragraph::operator==(const Paragraph& other) const
{
	if (this == &other)
		return true;
 
	return fStyle == other.fStyle
		&& fTextSpans == other.fTextSpans;
}
 
 
bool
Paragraph::operator!=(const Paragraph& other) const
{
	return !(*this == other);
}
 
 
void
Paragraph::SetStyle(const ParagraphStyle& style)
{
	fStyle = style;
}
 
 
bool
Paragraph::Prepend(const TextSpan& span)
{
	_InvalidateCachedLength();
 
	// Try to merge with first span if the TextStyles are equal
	if (fTextSpans.CountItems() > 0) {
		const TextSpan& firstSpan = fTextSpans.ItemAtFast(0);
		if (firstSpan.Style() == span.Style()) {
			BString text(span.Text());
			text.Append(firstSpan.Text());
			return fTextSpans.Replace(0, TextSpan(text, span.Style()));
		}
	}
	return fTextSpans.Add(span, 0);
}
 
 
bool
Paragraph::Append(const TextSpan& span)
{
	_InvalidateCachedLength();
 
	// Try to merge with last span if the TextStyles are equal
	if (fTextSpans.CountItems() > 0) {
		const TextSpan& lastSpan = fTextSpans.LastItem();
		if (lastSpan.Style() == span.Style()) {
			BString text(lastSpan.Text());
			text.Append(span.Text());
			fTextSpans.Remove();
			return fTextSpans.Add(TextSpan(text, span.Style()));
		}
	}
	return fTextSpans.Add(span);
}
 
 
bool
Paragraph::Insert(int32 offset, const TextSpan& newSpan)
{
	_InvalidateCachedLength();
 
	int32 index = 0;
	while (index < fTextSpans.CountItems()) {
		const TextSpan& span = fTextSpans.ItemAtFast(index);
		if (offset - span.CountChars() < 0)
			break;
		offset -= span.CountChars();
		index++;
	}
 
	if (fTextSpans.CountItems() == index)
		return Append(newSpan);
 
	// Try to merge with span at index if the TextStyles are equal
	TextSpan span = fTextSpans.ItemAtFast(index);
	if (span.Style() == newSpan.Style()) {
		span.Insert(offset, newSpan.Text());
		return fTextSpans.Replace(index, span);
	}
 
	if (offset == 0) {
		if (index > 0) {
			// Try to merge with TextSpan before if offset == 0 && index > 0
			TextSpan span = fTextSpans.ItemAtFast(index - 1);
			if (span.Style() == newSpan.Style()) {
				span.Insert(span.CountChars(), newSpan.Text());
				return fTextSpans.Replace(index - 1, span);
			}
		}
		// Just insert the new span before the one at index
		return fTextSpans.Add(newSpan, index);
	}
 
	// Split the span,
	TextSpan spanBefore = span.SubSpan(0, offset);
	TextSpan spanAfter = span.SubSpan(offset, span.CountChars() - offset);
 
	return fTextSpans.Replace(index, spanBefore)
		&& fTextSpans.Add(newSpan, index + 1)
		&& fTextSpans.Add(spanAfter, index + 2);
}
 
 
bool
Paragraph::Remove(int32 offset, int32 length)
{
	if (length == 0)
		return true;
 
	_InvalidateCachedLength();
 
	int32 index = 0;
	while (index < fTextSpans.CountItems()) {
		const TextSpan& span = fTextSpans.ItemAtFast(index);
		if (offset - span.CountChars() < 0)
			break;
		offset -= span.CountChars();
		index++;
	}
 
	if (index >= fTextSpans.CountItems())
		return false;
 
	TextSpan span(fTextSpans.ItemAtFast(index));
	int32 removeLength = std::min(span.CountChars() - offset, length);
	span.Remove(offset, removeLength);
	length -= removeLength;
	index += 1;
 
	// Remove more spans if necessary
	while (length > 0 && index < fTextSpans.CountItems()) {
		int32 spanLength = fTextSpans.ItemAtFast(index).CountChars();
		if (spanLength <= length) {
			fTextSpans.Remove(index);
			length -= spanLength;
		} else {
			// Reached last span
			removeLength = std::min(length, spanLength);
			TextSpan lastSpan = fTextSpans.ItemAtFast(index).SubSpan(
				removeLength, spanLength - removeLength);
			// Try to merge with first span, otherwise replace span at index
			if (lastSpan.Style() == span.Style()) {
				span.Insert(span.CountChars(), lastSpan.Text());
				fTextSpans.Remove(index);
			} else {
				fTextSpans.Replace(index, lastSpan);
			}
 
			break;
		}
	}
 
	// See if anything from the TextSpan at offset remained, keep it as empty
	// span if it is the last remaining span.
	index--;
	if (span.CountChars() > 0 || fTextSpans.CountItems() == 1) {
		fTextSpans.Replace(index, span);
	} else {
		fTextSpans.Remove(index);
		index--;
	}
 
	// See if spans can be merged after one has been removed.
	if (index >= 0 && index + 1 < fTextSpans.CountItems()) {
		const TextSpan& span1 = fTextSpans.ItemAtFast(index);
		const TextSpan& span2 = fTextSpans.ItemAtFast(index + 1);
		if (span1.Style() == span2.Style()) {
			span = span1;
			span.Append(span2.Text());
			fTextSpans.Replace(index, span);
			fTextSpans.Remove(index + 1);
		}
	}
 
	return true;
}
 
 
void
Paragraph::Clear()
{
	fTextSpans.Clear();
}
 
 
int32
Paragraph::Length() const
{
	if (fCachedLength >= 0)
		return fCachedLength;
 
	int32 length = 0;
	for (int32 i = fTextSpans.CountItems() - 1; i >= 0; i--) {
		const TextSpan& span = fTextSpans.ItemAtFast(i);
		length += span.CountChars();
	}
 
	fCachedLength = length;
	return length;
}
 
 
bool
Paragraph::IsEmpty() const
{
	return fTextSpans.CountItems() == 0;
}
 
 
bool
Paragraph::EndsWith(BString string) const
{
	int length = Length();
	int endLength = string.CountChars();
	int start = length - endLength;
	BString end = Text(start, endLength);
	return end == string;
}
 
 
BString
Paragraph::Text() const
{
	BString result;
 
	int32 count = fTextSpans.CountItems();
	for (int32 i = 0; i < count; i++)
		result << fTextSpans.ItemAtFast(i).Text();
 
	return result;
}
 
 
BString
Paragraph::Text(int32 start, int32 length) const
{
	Paragraph subParagraph = SubParagraph(start, length);
	return subParagraph.Text();
}
 
 
Paragraph
Paragraph::SubParagraph(int32 start, int32 length) const
{
	if (start < 0)
		start = 0;
 
	if (start == 0 && length == Length())
		return *this;
 
	Paragraph result(fStyle);
 
	int32 count = fTextSpans.CountItems();
	for (int32 i = 0; i < count; i++) {
		const TextSpan& span = fTextSpans.ItemAtFast(i);
		int32 spanLength = span.CountChars();
		if (spanLength == 0)
			continue;
		if (start > spanLength) {
			// Skip span if its before start
			start -= spanLength;
			continue;
		}
 
		// Remaining span length after start
		spanLength -= start;
		int32 copyLength = std::min(spanLength, length);
 
		if (start == 0 && length == spanLength)
			result.Append(span);
		else
			result.Append(span.SubSpan(start, copyLength));
 
		length -= copyLength;
		if (length == 0)
			break;
 
		// Next span is copied from its beginning
		start = 0;
	}
 
	return result;
}
 
 
void
Paragraph::PrintToStream() const
{
	int32 spanCount = fTextSpans.CountItems();
	if (spanCount == 0) {
		printf("  <p/>\n");
		return;
	}
	printf("  <p>\n");
	for (int32 i = 0; i < spanCount; i++) {
		const TextSpan& span = fTextSpans.ItemAtFast(i);
		if (span.CountChars() == 0)
			printf("    <span/>\n");
		else {
			BString text = span.Text();
			text.ReplaceAll("\n", "\\n");
			printf("    <span>%s</span>\n", text.String());
		}
	}
	printf("  </p>\n");
}
 
 
// #pragma mark -
 
 
void
Paragraph::_InvalidateCachedLength()
{
	fCachedLength = -1;
}

V730 Not all members of a class are initialized inside the constructor. Consider inspecting: fCachedLength.