// This file is part of VSTGUI. It is subject to the license terms 
// in the LICENSE file found in the top-level directory of this
// distribution and at http://github.com/steinbergmedia/vstgui/LICENSE

#include "ctextlabel.h"
#include "../platform/iplatformfont.h"
#include "../cdrawmethods.h"
#include "../cdrawcontext.h"
#include <sstream>

namespace VSTGUI {

//------------------------------------------------------------------------
// CTextLabel
//------------------------------------------------------------------------
/*! @class CTextLabel
*/
//------------------------------------------------------------------------
/**
 * CTextLabel constructor.
 * @param size the size of this view
 * @param txt the initial text as c string (UTF-8 encoded)
 * @param background the background bitmap
 * @param style the display style (see CParamDisplay for styles)
 */
//------------------------------------------------------------------------
CTextLabel::CTextLabel (const CRect& size, UTF8StringPtr txt, CBitmap* background, const int32_t style)
: CParamDisplay (size, background, style)
, textTruncateMode (kTruncateNone)
{
	setText (txt);
}

//------------------------------------------------------------------------
CTextLabel::CTextLabel (const CTextLabel& v)
: CParamDisplay (v)
, textTruncateMode (v.textTruncateMode)
{
	setText (v.getText ());
}

//------------------------------------------------------------------------
void CTextLabel::registerTextLabelListener (ITextLabelListener* listener)
{
	if (!listeners)
		listeners = std::unique_ptr<TextLabelListenerList> (new TextLabelListenerList ());
	listeners->add (listener);
}

//------------------------------------------------------------------------
void CTextLabel::unregisterTextLabelListener (ITextLabelListener* listener)
{
	if (listeners)
		listeners->remove (listener);
}

//------------------------------------------------------------------------
void CTextLabel::setText (const UTF8String& txt)
{
	if (text == txt)
		return;
	text = txt;
	if (textTruncateMode != kTruncateNone)
		calculateTruncatedText ();
	setDirty (true);
}

//------------------------------------------------------------------------
void CTextLabel::setTextTruncateMode (TextTruncateMode mode)
{
	if (textTruncateMode != mode)
	{
		textTruncateMode = mode;
		calculateTruncatedText ();
	}
}

//------------------------------------------------------------------------
void CTextLabel::calculateTruncatedText ()
{
	if (textRotation != 0.) // currently truncation is only supported when not rotated
	{
		truncatedText = "";
		return;
	}
	if (!(textTruncateMode == kTruncateNone || text.empty () || fontID == nullptr || fontID->getPlatformFont () == nullptr || fontID->getPlatformFont ()->getPainter () == nullptr))
	{
		CDrawMethods::TextTruncateMode mode = textTruncateMode == kTruncateHead ? CDrawMethods::kTextTruncateHead : CDrawMethods::kTextTruncateTail;
		truncatedText = CDrawMethods::createTruncatedText (mode, text, fontID, getWidth () - getTextInset ().x * 2.);
		if (truncatedText == text)
			truncatedText.clear ();
		if (listeners)
		{
			listeners->forEach (
			    [this] (ITextLabelListener* l) { l->onTextLabelTruncatedTextChanged (this); });
		}
	}
	else if (!truncatedText.empty ())
		truncatedText.clear ();
}

//------------------------------------------------------------------------
const UTF8String& CTextLabel::getText () const
{
	return text;
}

//------------------------------------------------------------------------
void CTextLabel::draw (CDrawContext *pContext)
{
	drawBack (pContext);
	drawPlatformText (pContext, truncatedText.empty () ? text : truncatedText);
	setDirty (false);
}

//------------------------------------------------------------------------
bool CTextLabel::sizeToFit ()
{
	if (fontID == nullptr || fontID->getPlatformFont () == nullptr || fontID->getPlatformFont ()->getPainter () == nullptr)
		return false;
	CCoord width = fontID->getPlatformFont ()->getPainter ()->getStringWidth (nullptr, text.getPlatformString (), true);
	if (width > 0)
	{
		width += (getTextInset ().x * 2.);
		CRect newSize = getViewSize ();
		newSize.setWidth (width);
		setViewSize (newSize);
		setMouseableArea (newSize);
		return true;
	}
	return false;
}

//------------------------------------------------------------------------
void CTextLabel::setViewSize (const CRect& rect, bool invalid)
{
	CRect current (getViewSize ());
	CParamDisplay::setViewSize (rect, invalid);
	if (textTruncateMode != kTruncateNone && current.getWidth () != getWidth ())
	{
		calculateTruncatedText ();
	}
}

//------------------------------------------------------------------------
void CTextLabel::drawStyleChanged ()
{
	if (textTruncateMode != kTruncateNone)
	{
		calculateTruncatedText ();
	}
	CParamDisplay::drawStyleChanged ();
}

//------------------------------------------------------------------------
void CTextLabel::valueChanged ()
{
	if (valueToStringFunction)
	{
		std::string string;
		if (valueToStringFunction (getValue (), string, this))
			setText (UTF8String (std::move (string)));
	}
	CParamDisplay::valueChanged ();
}

//------------------------------------------------------------------------
//------------------------------------------------------------------------
//------------------------------------------------------------------------
CMultiLineTextLabel::CMultiLineTextLabel (const CRect& size)
: CTextLabel (size)
{
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setValue (float val)
{
	CTextLabel::setValue (val);

	if (valueToStringFunction)
	{
		std::string string;
		if (valueToStringFunction (value, string, this))
			setText (UTF8String (string));
	}
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setTextTruncateMode (TextTruncateMode)
{
	// not supported on multi line labels
	CTextLabel::setTextTruncateMode (kTruncateNone);
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setLineLayout (LineLayout layout)
{
	if (lineLayout == layout)
		return;
	lineLayout = layout;
	lines.clear ();
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setAutoHeight (bool state)
{
	if (autoHeight == state)
		return;
	autoHeight = state;
	if (autoHeight && isAttached ())
	{
		if (lines.empty ())
			recalculateLines (nullptr);
		recalculateHeight ();
	}
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setVerticalCentered (bool state)
{
	if (verticalCentered == state)
		return;
	verticalCentered = state;
	lines.clear ();
}

//------------------------------------------------------------------------
CCoord CMultiLineTextLabel::getMaxLineWidth ()
{
	if (lines.empty () && getText ().empty () == false)
		recalculateLines (nullptr);
	CCoord maxWidth {};
	for (const auto& line : lines)
	{
		if (line.r.getWidth () > maxWidth)
			maxWidth = line.r.getWidth ();
	}
	return maxWidth;
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::drawRect (CDrawContext* pContext, const CRect& updateRect)
{
	if (getText ().empty () == false && lines.empty ())
		recalculateLines (pContext);
	drawBack (pContext);
	
	CRect newClip (updateRect);
	newClip.inset (getTextInset ());
	ConcatClip clip (*pContext, newClip);
	newClip = clip.get ();

	pContext->setDrawMode (kAntiAliasing);
	pContext->setFont (getFont ());
	
	newClip.offsetInverse (getViewSize().getTopLeft ());

	CDrawContext::Transform t (*pContext, CGraphicsTransform ().translate (getViewSize ().getTopLeft ()));

	if (style & kShadowText)
	{
		CDrawContext::Transform t2 (*pContext, CGraphicsTransform ().translate (shadowTextOffset));
		pContext->setFontColor (getShadowColor ());
		for (const auto& line : lines)
		{
			if (line.r.rectOverlap (newClip))
				pContext->drawString (line.str.getPlatformString (), line.r, getHoriAlign (), getAntialias ());
		}
	}

	pContext->setFontColor (getFontColor ());
	for (const auto& line : lines)
	{
		if (line.r.rectOverlap (newClip))
			pContext->drawString (line.str.getPlatformString (), line.r, getHoriAlign (), getAntialias ());
		else if (line.r.bottom > newClip.bottom)
			break;
	}

	setDirty (false);
}

//------------------------------------------------------------------------
bool CMultiLineTextLabel::sizeToFit ()
{
	return false;
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setText (const UTF8String& txt)
{
	if (getText () == txt)
		return;
	CTextLabel::setText (txt);
	lines.clear ();
	if (autoHeight && isAttached ())
	{
		recalculateLines (nullptr);
		recalculateHeight ();
	}
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::recalculateHeight ()
{
	auto viewSize = getViewSize ();
	if (lines.empty ())
		viewSize.setHeight (0.);
	else
	{
		auto lastLine = lines.back ().r;
		viewSize.setHeight (lastLine.bottom + getTextInset ().y);
	}
	CTextLabel::setViewSize (viewSize);
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::setViewSize (const CRect& rect, bool invalid)
{
	auto viewSize = getViewSize ();
	auto normRect = rect;
	viewSize.originize ();
	normRect.originize ();
	if (viewSize != normRect)
	{
		if (lineLayout != LineLayout::clip ||
		    (lineLayout == LineLayout::clip &&
		     viewSize.getHeight () != normRect.getHeight ()))
		{
			lines.clear ();
		}
	}
	CTextLabel::setViewSize (rect, invalid);
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::drawStyleChanged ()
{
	lines.clear ();
	CTextLabel::drawStyleChanged ();
}

//------------------------------------------------------------------------
inline bool isLineBreakSeparator (char32_t c)
{
	switch (c)
	{
		case '-': return true;
		case '_': return true;
		case '/': return true;
		case '\\': return true;
		case '.': return true;
		case ',': return true;
		case ':': return true;
		case ';': return true;
		case '?': return true;
		case '!': return true;
		case '*': return true;
		case '+': return true;
		case '&': return true;
	}
	return false;
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::calculateWrapLine  (CDrawContext* context,
                                       std::pair<UTF8String, double>& element,
                                       const IFontPainter* const& fontPainter, double lineHeight,
                                       double lineWidth, double maxWidth, const CPoint& textInset,
                                       CCoord& y)
{
	auto start = element.first.begin ();
	auto lastSeparator = start;
	auto pos = start;
	while (pos != element.first.end () && *pos != 0)
	{
		if (isspace (*pos))
			lastSeparator = pos;
		else if (isLineBreakSeparator (*pos))
			lastSeparator = ++pos;
		if (pos == element.first.end ())
			break;
		auto tmpEnd = pos;
		UTF8String tmp ({start.base (), (++tmpEnd).base ()});
		auto width = fontPainter->getStringWidth (
			context ? context->getPlatformDeviceContext () : nullptr, tmp.getPlatformString ());
		if (width > maxWidth)
		{
			if (lastSeparator == element.first.end ())
				lastSeparator = pos;
			if (start == lastSeparator)
				lastSeparator = pos;
			lines.emplace_back (
			    Line {CRect (textInset.x, y, lineWidth, y + lineHeight + textInset.y),
			          UTF8String ({start.base (), lastSeparator.base ()})});
			y += lineHeight;
			pos = lastSeparator;
			start = pos;
			if (isspace (*start))
				++start;
			lastSeparator = element.first.end ();
		}
		++pos;
	}
	if (start != element.first.end ())
	{
		lines.emplace_back (Line {CRect (textInset.x, y, lineWidth, y + lineHeight + textInset.y),
		                          UTF8String ({start.base (), element.first.end ().base ()})});
		y += lineHeight;
	}
}

//------------------------------------------------------------------------
void CMultiLineTextLabel::recalculateLines (CDrawContext* context)
{
	const auto& font = getFont ()->getPlatformFont ();
	const auto& fontPainter = getFont ()->getFontPainter ();
	auto ascent = font->getAscent ();
	auto descent = font->getDescent ();
	auto leading = font->getLeading ();
	auto lineHeight = ascent + descent + leading;

	const auto& textInset = getTextInset ();
	auto maxWidth = getWidth () - (textInset.x * 2);

	std::vector<std::pair<UTF8String, CCoord>> elements;
	std::stringstream stream (getText ().getString ());
	std::string line;
	while (std::getline (stream, line, '\n'))
	{
		UTF8String str (std::move (line));
		auto width = fontPainter->getStringWidth (
			context ? context->getPlatformDeviceContext () : nullptr, str.getPlatformString ());
		elements.emplace_back (std::move (str), width);
	}

	CCoord y = textInset.y;

	auto lineWidth = getWidth () - textInset.x;
	
	for (auto& element : elements)
	{
		if (lineLayout == LineLayout::clip)
		{
			lines.emplace_back (Line {
			    CRect (textInset.x, y, element.second + textInset.x, y + lineHeight + textInset.y),
			    std::move (element.first)});
		}
		else
		{
			if (element.second > maxWidth)
			{
				if (lineLayout == LineLayout::truncate)
				{
					element.first = CDrawMethods::createTruncatedText (
					    CDrawMethods::kTextTruncateTail, element.first, fontID, maxWidth);
				}
				else // wrap
				{
					calculateWrapLine (context, element, fontPainter, lineHeight, lineWidth,
					                   maxWidth, textInset, y);
					continue;
				}
			}
			lines.emplace_back (
			    Line {CRect (textInset.x, y, lineWidth, y + lineHeight + textInset.y),
			          std::move (element.first)});
		}
		y += lineHeight;
	}
	if (getVerticalCentered () && !lines.empty ())
	{
		auto maxHeight = lines.back ().r.bottom;
		auto offset = ((getHeight () - textInset.y) - maxHeight) / 2.;
		if (offset > 0)
		{
			for (auto& l : lines)
				l.r.offset (0, offset);
		}
	}
}

} // VSTGUI
