/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "common/endian.h"
#include "mm/xeen/font.h"
#include "mm/xeen/resources.h"
#include "mm/xeen/xeen.h"

namespace MM {
namespace Xeen {

namespace {
const int kBig5Width = 16;
const int kBig5Height = 14;
}

const byte *FontData::_fontData;
Common::Point *FontData::_fontWritePos;
byte FontData::_textColors[4];
byte FontData::_bgColor;
bool FontData::_fontReduced;
Justify FontData::_fontJustify;
Graphics::Big5Font *FontData::_big5Font;
	
FontSurface::FontSurface() : XSurface(), _msgWraps(false), _displayString(nullptr),
		_writePos(*FontData::_fontWritePos) {
	setTextColor(0);

	lang = g_vm->getLanguage();
	if (Common::RU_RUS == lang) {
		_fntEnOffset            = 0x0000;
		_fntNonEnOffset         = 0x0800;
		_fntEnReducedOffset     = 0x1000;
		_fntNonEnReducedOffset  = 0x1800;
		_fntEnWOffset           = 0x2000;
		_fntNonEnWOffset        = 0x2080;
		_fntEnReducedWOffset    = 0x2100;
		_fntNonEnReducedWOffset = 0x2180;
	} else {
		_fntEnOffset            = 0x0000;
		_fntEnReducedOffset     = 0x0800;
		_fntEnWOffset           = 0x1000;
		_fntEnReducedWOffset    = 0x1080;
	}
	_isBig5 = Common::ZH_TWN == lang;
}

FontSurface::FontSurface(int wv, int hv) : XSurface(wv, hv),
		_msgWraps(false), _displayString(nullptr), _writePos(*FontData::_fontWritePos) {
	create(w, h);
	setTextColor(0);

	lang = g_vm->getLanguage();
	if (Common::RU_RUS == lang) {
		_fntEnOffset            = 0x0000;
		_fntNonEnOffset         = 0x0800;
		_fntEnReducedOffset     = 0x1000;
		_fntNonEnReducedOffset  = 0x1800;
		_fntEnWOffset           = 0x2000;
		_fntNonEnWOffset        = 0x2080;
		_fntEnReducedWOffset    = 0x2100;
		_fntNonEnReducedWOffset = 0x2180;
	} else {
		_fntEnOffset            = 0x0000;
		_fntEnReducedOffset     = 0x0800;
		_fntEnWOffset           = 0x1000;
		_fntEnReducedWOffset    = 0x1080;
	}
	_isBig5 = Common::ZH_TWN == lang;
}

void FontSurface::writeSymbol(int symbolId) {
	const byte *srcP = &Res.SYMBOLS[symbolId][0];

	for (int yp = 0; yp < FONT_HEIGHT; ++yp) {
		byte *destP = (byte *)getBasePtr(_writePos.x, _writePos.y + yp);

		for (int xp = 0; xp < FONT_WIDTH; ++xp, ++destP) {
			byte b = *srcP++;
			if (b)
				*destP = b;
		}
	}

	_writePos.x += 8;
}

bool FontSurface::isSpace(char c) {
	if (Common::RU_RUS == lang) 
		return c == ' ';
	return (c & 0x7f) == ' ';
}

const char *FontSurface::writeString(const Common::String &s, const Common::Rect &bounds, bool ttsVoiceText, Common::String *ttsMessage) {
	_displayString = s.c_str();
	assert(_fontData);

#ifdef USE_TTS
	bool deleteTTSMessage = false;
	if (!ttsMessage) {
		ttsMessage = new Common::String();
		deleteTTSMessage = true;
	}
#endif

	for (;;) {
		const char *msgStartP = _displayString;
		_msgWraps = false;

		// Get the size of the string that can be displayed on the line
		int xp = _fontJustify == JUSTIFY_CENTER ? bounds.left : _writePos.x;
		while (!getNextCharWidth(xp)) {
			if (xp >= bounds.right) {
				--_displayString;
				_msgWraps = true;
				break;
			}
		}

		// Get the end point of the text that can be displayed
		const char *displayEnd = _displayString;
		_displayString = msgStartP;

		if (_msgWraps && _fontJustify != JUSTIFY_RIGHT && xp >= bounds.right) {
			// Need to handle justification of text
			// First, move backwards to find the end of the previous word
			// for a convenient point to break the line at
			const char *endP = displayEnd;
			while (endP > _displayString && !isSpace(*endP))
				--endP;

			if (endP == _displayString) {
				// There was no word breaks at all in the string
				// Since the loop below includes displayEnd, we need to
				// subtract 1 to compensate and another time to delete last
				// char.
				displayEnd -= 2;
				if (_fontJustify == JUSTIFY_NONE && _writePos.x != bounds.left) {
					// Move to the next line
					if (!newLine(bounds))
						continue;
					// Ran out of space to display string
					break;
				}
			} else {
				// Found word break, find end of previous word
				while (endP > _displayString && !isSpace(*endP))
					--endP;

				displayEnd = endP;
			}
		}

		// Justification adjustment
		if (_fontJustify != JUSTIFY_NONE) {
			// Figure out the width of the selected portion of the string
			int totalWidth = 0;
			while (!getNextCharWidth(totalWidth)) {
				if (_displayString > displayEnd) {
					if (*displayEnd == ' ') {
						// Don't include any ending space as part of the total
						totalWidth -= _fontReduced ? 4 : 5;
					}
					break;
				}
			}

			// Reset starting position back to the start of the string portion
			_displayString = msgStartP;

			if (_fontJustify == JUSTIFY_RIGHT) {
				// Right aligned
				if (_writePos.x == bounds.left)
					_writePos.x = bounds.right;
				_writePos.x -= totalWidth + 1;
			} else {
				// Center aligned
				if (_writePos.x == bounds.left)
					_writePos.x = (bounds.left + bounds.right + 1 - totalWidth) / 2;
				else
					_writePos.x = (_writePos.x * 2 - totalWidth) / 2;
 			}
		}

		// Main character display loop
		while (_displayString <= displayEnd) {
			uint16_t c = getNextChar();

			if (c == ' ') {
				_writePos.x += _fontReduced ? 3 : 4;
#ifdef USE_TTS
				*ttsMessage += ' ';
#endif
			} else if (c == '\r') {
				fillRect(bounds, _bgColor);
				addDirtyRect(bounds);
				_writePos = Common::Point(bounds.left, bounds.top);
#ifdef USE_TTS
				*ttsMessage += '\n';
#endif
			} else if (c == 1) {
				// Turn off reduced font mode
				_fontReduced = false;
			} else if (c == 2) {
				// Turn on reduced font mode
				_fontReduced = true;
			} else if (c == 3) {
				// Justify text
				c = getNextChar();
				if (c == 'r')
					_fontJustify = JUSTIFY_RIGHT;
				else if (c == 'c')
					_fontJustify = JUSTIFY_CENTER;
				else
					_fontJustify = JUSTIFY_NONE;
			} else if (c == 4) {
				// Draw an empty box of a given width
				int wv = fontAtoi();
				Common::Point pt = _writePos;
				if (_fontJustify == JUSTIFY_RIGHT)
					pt.x -= wv;

				Common::Rect r(pt.x, pt.y, pt.x + wv, pt.y + (_fontReduced ? 9 : 10));
				fillRect(r, _bgColor);
			} else if (c == 5) {
				continue;
			} else if (c == 6) {
				// Non-breakable space
				writeChar(' ', bounds);
#ifdef USE_TTS
				*ttsMessage += ' ';
#endif
			} else if (c == 7) {
				// Set text background color
				int bgColor = fontAtoi();
				_bgColor = (bgColor < 0 || bgColor > 255) ? DEFAULT_BG_COLOR : bgColor;
			} else if (c == 8) {
				// Draw a character outline
				c = getNextChar();
				if (c == ' ') {
					c = '\0';
					_writePos.x -= 3;
				} else {
					if (c == 6)
						c = ' ';
					int offset_charW = c & 0x80 ?
						(_fontReduced ? _fntNonEnReducedWOffset : _fntNonEnWOffset) + (int)(c & 0x7F) :
						(_fontReduced ? _fntEnReducedWOffset : _fntEnWOffset) + (int)c;
					byte charSize = _fontData[offset_charW];

					_writePos.x -= charSize;
				}

				if (_writePos.x < bounds.left)
					_writePos.x = bounds.left;

				if (c) {
					int oldX = _writePos.x;
					byte oldColor[4];
					Common::copy(&_textColors[0], &_textColors[4], &oldColor[0]);

					_textColors[1] = _textColors[2] = _textColors[3] = _bgColor;
					writeChar(c, bounds);

					Common::copy(&oldColor[0], &oldColor[4], &_textColors[0]);
					_writePos.x = oldX;
				}
			} else if (c == 9) {
				// Skip x position
				int xAmount = fontAtoi();
				_writePos.x = MIN(bounds.left + xAmount, (int)bounds.right);
#ifdef USE_TTS
				*ttsMessage += '\n';
#endif
			} else if (c == 10) {
#ifdef USE_TTS
				*ttsMessage += '\n';
#endif

				// Newline
				if (newLine(bounds))
					return _displayString;
			} else if (c == 11) {
				// Set y position
				int yp = fontAtoi();
				_writePos.y = MIN(bounds.top + yp, (int)bounds.bottom);
#ifdef USE_TTS
				*ttsMessage += '\n';
#endif
			} else if (c == 12) {
				// Set text colors
				int idx = fontAtoi(2);
				if (idx < 0)
					idx = 0;
				setTextColor(idx);
			} else if (Common::RU_RUS == lang && (c & 0x80)) {
				writeChar(c, bounds);
#ifdef USE_TTS
				*ttsMessage += c;
#endif
			} else if (c < ' ') {
				// End of string or invalid command
				_displayString = nullptr;
#ifdef USE_TTS
				*ttsMessage += '\n';
#endif
				break;
			} else {
				// Standard character - write it out
				writeChar(c, bounds);
#ifdef USE_TTS
				*ttsMessage += c;
#endif
			}
		}

		if (!_displayString)
			break;
		if ( _displayString > displayEnd && _fontJustify != JUSTIFY_RIGHT && _msgWraps
				&& newLine(bounds))
			break;
	}

#ifdef USE_TTS
	if (ttsVoiceText) {
		g_vm->sayText(*ttsMessage);
	}

	if (deleteTTSMessage) {
		delete ttsMessage;
		ttsMessage = nullptr;
	}
#endif

	return _displayString;
}

void FontSurface::writeCharacter(uint16_t c, const Common::Rect &clipRect) {
	Justify justify = _fontJustify;
	_fontJustify = JUSTIFY_NONE;
	if (_isBig5 && c >= 0x100)
		writeString(Common::String::format("%c%c", c >> 8, c & 0xff), clipRect);
	else
		writeString(Common::String::format("%c", c), clipRect);
	_fontJustify = justify;
}

uint16_t FontSurface::getNextChar() {
	if (_isBig5) {
		uint8_t lead = *_displayString++;
		if (!(lead & 0x80))
			return lead;
		return (lead << 8) | (*_displayString++ & 0xff);
	} else if (Common::RU_RUS == lang)
		return *_displayString++ & 0xff;
	else
		return *_displayString++ & 0x7f;
}

bool FontSurface::getNextCharWidth(int &total) {
	uint16_t c = getNextChar();

	if (_isBig5 && c > 0xff) {
		total += kBig5Width;
		return false;
	} if (c > ' ') {
		total += _fontData[(_fontReduced ? _fntEnReducedWOffset : _fntEnWOffset) + (int)c];
		return false;
	} else if (c == ' ') {
		total += 4;
		return false;
	} else if (c == 8) {
		c = getNextChar();
		if (c == ' ') {
			total -= 2;
			return false;
		} else {
			_displayString -= 2;
			return true;
		}
	} else if (c == 12) {
		c = getNextChar();
		if (c != 'd')
			getNextChar();
		return false;
	} else if (Common::RU_RUS == lang && (c & 0x80)) {
		total += _fontData[(_fontReduced ? _fntNonEnReducedWOffset : _fntNonEnWOffset) + (int)(c & 0x7F)];
		return false;
	} else {
		--_displayString;
		return true;
	}
}

bool FontSurface::newLine(const Common::Rect &bounds) {
	// Move past any spaces currently being pointed to
	while (isSpace(*_displayString))
		++_displayString;

	_msgWraps = false;
	_writePos.x = bounds.left;

	int hv = _isBig5 && _big5Font ? _big5Font->getFontHeight() + 1 : (_fontReduced ? 9 : 10);
	_writePos.y += hv;

	return ((_writePos.y + hv - 1) > bounds.bottom);
}

int FontSurface::fontAtoi(int len) {
	int total = 0;
	for (int i = 0; i < len; ++i) {
		uint16_t c = getNextChar();
		if (c == ' ')
			c = '0';

		int digit = c - '0';
		if (digit < 0 || digit > 9)
			return -1;

		total = total * 10 + digit;
	}

	return total;
}

void FontSurface::setTextColor(int idx) {
	const byte *colP = (g_vm->_mode == MODE_STARTUP) ? &Res.TEXT_COLORS_STARTUP[idx][0] : &Res.TEXT_COLORS[idx][0];
	Common::copy(colP, colP + 4, &_textColors[0]);
}

void FontSurface::writeChar(uint16_t c, const Common::Rect &clipRect) {
	// Get y position, handling kerning
	int y = _writePos.y;
	if (_isBig5 && c > 0xff) {
		_big5Font->drawBig5Char(surfacePtr(), c, _writePos, _textColors[0]);
		_writePos.x++;
		_big5Font->drawBig5Char(surfacePtr(), c, _writePos, _textColors[3]);
		_writePos.x += kBig5Width - 1;
		addDirtyRect(Common::Rect(_writePos.x, _writePos.y, _writePos.x + kBig5Width,
					  _writePos.y + kBig5Height));

		return;
	}
	if (c == 'g' || c == 'p' || c == 'q' || c == 'y')
		++y;
	if (_isBig5 && _big5Font) {
		y += (_big5Font->getFontHeight() - FONT_HEIGHT) / 2;
	}
	int yStart = y;

	// Get pointers into font data and surface to write pixels to
	int offset_charData;
	int offset_charW;
	if (Common::RU_RUS == lang && (c & 0x80)) {
		offset_charData = (_fontReduced ? _fntNonEnReducedOffset : _fntNonEnOffset) + (int)(c & 0x7F) * 16;
		offset_charW = (_fontReduced ? _fntNonEnReducedWOffset : _fntNonEnWOffset) + (int)(c & 0x7F);
	} else {
		offset_charData = (_fontReduced ? _fntEnReducedOffset : _fntEnOffset) + (int)c * 16;
		offset_charW = (_fontReduced ? _fntEnReducedWOffset : _fntEnWOffset) + (int)c;
	}
	const byte *srcP = &_fontData[offset_charData];

	for (int yp = 0; yp < FONT_HEIGHT; ++yp, ++y) {
		uint16 lineData = READ_LE_UINT16(srcP); srcP += 2;
		byte *destP = (byte *)getBasePtr(_writePos.x, y);

		// Ignore line if it's outside the clipping rect
		if (y < clipRect.top || y >= clipRect.bottom)
			continue;
		const byte *lineStart = (const byte *)getBasePtr(clipRect.left, y);
		const byte *lineEnd = (const byte *)getBasePtr(clipRect.right, y);

		for (int xp = 0; xp < FONT_WIDTH; ++xp, ++destP) {
			int colIndex = lineData & 3;
			lineData >>= 2;

			if (colIndex && destP >= lineStart && destP < lineEnd)
				*destP = _textColors[colIndex];
		}
	}

	addDirtyRect(Common::Rect(_writePos.x, yStart, _writePos.x + FONT_WIDTH,
		yStart + FONT_HEIGHT));
	_writePos.x += _fontData[offset_charW];
}

} // End of namespace Xeen
} // End of namespace MM
