/***************************************************************************
*   Copyright (C) 2005 - 2007 by Alexander Nemish                         *
*   atlanter@gmail.com                                                    *
*                                                                         *
*   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.,                                       *
*   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
***************************************************************************/
#include <kdebug.h>
#include <tqfontmetrics.h>
#include <tqpainter.h>
#include <vector>
#include <cmath>
#include <iostream>
#include "renderer.h"

Renderer::Renderer():
		m_pageCount(0),
		m_linesPerPage(0),
		m_paraOffset(50),
		m_fontMetrics(new TQFontMetrics(m_font)),
		m_curParagraph(0),
		m_isRendering(false)
{
	connect(&m_timer, TQ_SIGNAL(timeout()), this, TQ_SLOT(timeout()));
}


Renderer::~Renderer()
{
}

/**
*	If the list is empty do nothing.
*	\param a_list List of strings to render
*/
void Renderer::load(const TQStringList & a_list)
{
	if (a_list.isEmpty())
		return;
	m_text.clear();
		
	TQString string;
	TStringIter it(a_list.constBegin());
	
	while (it != a_list.constEnd())
	{
		//skip spaces;
		string = (*it).stripWhiteSpace();
		//process string until paragraph ends
		while (++it != a_list.constEnd())
		{
			TQChar const ch((*it)[0]);
			//insert empty lines
			if ((*it).isEmpty())
				break;
			//check whether paragraph not ends
			if (!ch.isSpace() && (ch != '-'))
			{
				string += " " + *it;
			}
			else break;
		}
		// add paragraph
		m_text.push_back(string);
		//add empty line if we have one
		if ((*it).isEmpty())
			m_text.push_back("");
	}
	render();
}

/**
*	\note You need manually update your widget. Like
*	\code
*	renderer->render();
*	update();
*	\endcode
*/
void Renderer::render()
{
	if (isEmpty())
		return;
	if (busy())
		cancel();
	clear();
	m_isRendering = true;
	m_linesPerPage = m_pageSize.height() / (TQFontMetrics(font()).height());
	m_timer.start(0, false);
}

void Renderer::timeout()
{
	if (m_curParagraph < m_text.size())
	{
		parseParagraph(m_curParagraph);
		m_curParagraph++;
	}
	else
	{
		cancel();
		emit renderingFinished();
	}
	m_pageCount = m_linesPerPage ? (m_lines.size() / m_linesPerPage) + 1 : 0;
}

void Renderer::cancel()
{
	m_timer.stop();
	m_curParagraph = 0;
	m_isRendering = false;
}

/**
 *	Call it for simple view clearing.
*	\code
*	renderer->clear();
*	update();
*	\endcode
*/
void Renderer::clear()
{
	m_lines.clear();
	m_pageCount = 0;
	m_linesPerPage = 0;
}

/**
*	\param a_string Line to parse
*/
void Renderer::parseParagraph(TIndex paraIndex)
{
	//Don't parse empty lines
	const TQString a_string(m_text[paraIndex]);
	if (a_string.isEmpty())
		return;
		
	TQString string(a_string);
	const int avgCharWidth = m_fontMetrics->width(" ");
	//calc approx string width
	unsigned int avgLen = m_pageSize.width() / avgCharWidth;
	unsigned int len;
	int begin = 0;
	m_isStartAdded = false;
	int curWidth = width(string);
	
	// whole paragraph in single line
	if (curWidth <= m_pageSize.width())
	{
		addLine(TextLine(paraIndex, 0, a_string.length(), TextLine::PARA_BOTH));
		return;
	}
	
	for (; curWidth > m_pageSize.width(); curWidth = width(string))
	{
		len = avgLen;
		//turn left if missed
		int w = width(string.left(len));
		for (; (w > m_pageSize.width()) && (len > 0); w = width(string.left(len)))
		{
			int diff = w - m_pageSize.width();
			int charDiff = diff / 10;
			if (charDiff > len)
				charDiff = charDiff % len;
			charDiff = (charDiff == 0) ? 1 : charDiff;
			len -= charDiff;
		}
		//turn right if missed
		while ((width(string.left(len)) <= m_pageSize.width()) 
				&& (len < string.length()))
			++len;
		--len;
		
		// update statistics on average length
		avgLen = (unsigned int) (len / (1 + 1. / avgLen));
		
		//check whether we in a word
		if (!(string[len - 1].isSpace() || string[len].isSpace()))
		{
			//find last word start index
			const int wordBegin = wordAt(string, len);
			//check whether its width less than page width
			if (width(getWord(string, wordBegin)) <= m_pageSize.width())
			{
				//if so, move last word to the next line
				// invariant: wordBegin > 0,
				// because otherwise the word is greater than page width
				
				// i points to a space before the word
				int i = wordBegin -1;
				// skip spaces
				while (i && string[i].isSpace())
					--i;
				addLine(TextLine(paraIndex, begin, begin + i + 1));
				string = string.right(string.length() - wordBegin);
				begin += wordBegin;
			}
			else
			{
				//if its width greater than page width - split it
				addLine(TextLine(paraIndex, begin, begin + len));
				string = string.right(string.length() - len);
				begin += len;
			}
		}
		// line ends with spaces and next line begins with spaces,
		// trim them
		else if (string[len - 1].isSpace() && string[len].isSpace())
		{
			int idx = len - 1;
			while (string[--idx] == ' ');
			addLine(TextLine(paraIndex, begin, begin + idx + 1));
			idx = len;
			while (string[++idx] == ' ');
			string = string.right(string.length() - idx);
			begin += idx;
		}
		else if (string[len - 1].isSpace())
		{
			int idx = len - 1;
			while (string[--idx] == ' ');
			addLine(TextLine(paraIndex, begin, begin + idx + 1));
			string = string.right(string.length() - len);
			begin += len;
		}
		else if (string[len].isSpace())
		{
			int idx = len;
			while (string[++idx] == ' ');
			addLine(TextLine(paraIndex, begin, begin + len));
			string = string.right(string.length() - idx);
			begin += idx;
		}
	}
	//last line in multiline para.
	len = string.length();
	addLine(TextLine(paraIndex, begin, begin + len, TextLine::PARA_END));
}

int Renderer::width(const TQString & a_string) const
{
	int w = m_fontMetrics->width(a_string);
	return m_isStartAdded ? w : w + paraOffset();
}

void Renderer::addLine(TextLine line)
{
	if (!m_isStartAdded)
		line.setParagraphFlags(line.paragraphFlags() | TextLine::PARA_START);
	m_lines.push_back(line);
	m_isStartAdded = true;
}

int Renderer::wordAt(const TQString & string, int len)
{
	while (--len >= 0)
		if (string[len] == ' ')
			return ++len;
	return 0;
}

TQString Renderer::getWord(const TQString & a_string, int a_idx)
{
	int idx = a_idx;
	while (a_string[++idx] != ' ' && idx < a_string.length());
	return TQString(a_string.mid(a_idx, idx - a_idx));
}

/**
*	Draws page number \c pageNumber on \c paint 
*	bounding \c rect rectangle
*/
void Renderer::drawPage(TQPainter & paint, TQRect rect, int pageNumber)
{
	int height = m_fontMetrics->height();
	int line = 1;
	TLines::size_type count = m_lines.size();
	
	if (count == 0)
		return;
	
	if ((pageNumber * m_linesPerPage) >= count)
		return;
	
	TLines::size_type idx = pageNumber * m_linesPerPage;
	for (; (line <= m_linesPerPage) && idx < m_lines.size(); ++idx, ++line)
		drawLine(paint, rect.left(), rect.top() + line * height, idx);
}

void Renderer::drawLine(TQPainter & paint, int x, int y, const TLines::size_type index)
{
	const TextLine textLine(m_lines[index]);
	const int length = textLine.size();
	const TQString & paragraph = m_text[textLine.paragraphIndex()];
	const TQString string = paragraph.mid(textLine.begin(), textLine.size());
	
	// indent paragraph
	if (textLine.isParaStart())
		x += paraOffset();
	
	// don't justify on paragraph end
	if (textLine.isParaEnd())
	{
		paint.drawText(x, y, string);
		return;
	}
	
	const int pageWidth = m_pageSize.width();
	const int spaceWidth = m_fontMetrics->width(" ");
	int width = m_fontMetrics->width(string);
	const int curWidth = textLine.isParaStart() ? width + paraOffset() : width;
	
	if (curWidth == pageWidth)
	{
		paint.drawText(x, y, string);
	}
	
	// string width is lower than page width. justify by space width
	long pos = 0, off = 0;
	
	//count spaces
	std::vector<int> spaces;
	spaces.reserve(50);
	while (((pos = string.find(' ', off)) != -1) && (pos < length))
	{
		spaces.push_back(pos);
		off = pos + 1;
	}
	const std::vector<int>::size_type spacesCount = spaces.size();
	// no spaces no justifications
	if (!spacesCount)
	{
		paint.drawText(x, y, string);
		return;
	}
	// justify line
	double x1 = x;
	// calc average space width
	const double avgLen = ((double)(pageWidth - curWidth) / spacesCount);
	int start = 0;
	TQString tmp;
	for (std::vector<int>::size_type i = 0; i < spacesCount; ++i)
	{
		pos = spaces[i];
		tmp = string.mid(start, pos - start);
		paint.drawText((int)std::ceil(x1), y, tmp);
		x1 += m_fontMetrics->width(tmp);
		x1 += spaceWidth + avgLen;
		start = pos + 1;
	}
	//last chunk
	paint.drawText((int)std::ceil(x1), y, string.right(length - start));
}

/**
 *	Sets current font to \c font and re-renders text.
 *	You don't need to directly call render().
 */
void Renderer::setFont(const TQFont & font)
{
	if (font == m_font) return;
	m_font = font;
	m_fontMetrics.reset(new TQFontMetrics(m_font));
	render();
}

/**
*	Sets current paragraph offset to \c offset 
*	and re-renders text if it's changed.
*/
void Renderer::setParaOffset(int offset)
{
	if (offset == m_paraOffset) return;
	m_paraOffset = offset;
	render();
}

/**
 *	Sets current page size to \c size. 
 *	\note It don't call render() after changing.
 */
void Renderer::setPageSize(const TQSize & size)
{
	m_pageSize = size;
}

#include "renderer.moc"
