/**
 * \file InsetMathChar.cpp
 * This file is part of LyX, the document processor.
 * Licence details can be found in the file COPYING.
 *
 * \author Alejandro Aguilar Sierra
 * \author André Pönitz
 *
 * Full author contact details are available in file CREDITS.
 */

#include <config.h>

#include "InsetMathChar.h"

#include "MathParser.h"
#include "MathSupport.h"
#include "MathStream.h"
#include "MetricsInfo.h"

#include "Dimension.h"
#include "BufferEncodings.h"
#include "LaTeXFeatures.h"
#include "TextPainter.h"

#include "frontends/FontMetrics.h"

#include "support/debug.h"
#include "support/textutils.h"

#include <algorithm>

using namespace std;


namespace lyx {


namespace {
latexkeys const * makeSubstitute(char_type c)
{
	std::string name;
	switch (c) {
	// Latex replaces ', *, -, and : with specific symbols. With unicode-math,
	// these symbols are replaced respectively by ^U+2032, U+2217, U+2212 and
	// U+2236 (the latter substitution can be turned off with a package
	// option). Unicode-math also replaces ` with \backprime.
		// prime needs to be placed in superscript unless an opentype font is used.
		//case '\'':
		//name = "prime";
		//break;
	case '*':
		name = "ast";
		break;
	case '-':
		name = "lyxminus";// unicode-math: "minus"
		break;
	case ':':
		name = "ordinarycolon";// unicode-math: "mathratio"
		break;
	// The remaining replacements are not real character substitutions (from a
	// unicode point of view) but are done here: 1. for cosmetic reasons, in the
	// context of being stuck with CM fonts at the moment, to ensure consistency
	// with related symbols: -, \leq, \geq, etc.  2. to get the proper spacing
	// as defined in lib/symbols.
	case '+':
		name = "lyxplus";//unicode-math: "mathplus"
		break;
	case '>':
		name = "lyxgt";//unicode-math: "greater"
		break;
	case '<':
		name = "lyxlt";//unicode-math: "less"
		break;
	case '=':
		name = "lyxeqrel";//unicode-math: "equal"
		break;
	//case ','://unicode-math: "mathcomma"
	//case ';'://unicode-math: "mathsemicolon"
	default:
		return nullptr;
	}
	return in_word_set(from_ascii(name));
}

} //anonymous namespace


static bool slanted(char_type c)
{
	return isAlphaASCII(c) || Encodings::isMathAlpha(c);
}


InsetMathChar::InsetMathChar(Buffer * buf, char_type c)
	: InsetMath(buf), char_(c), kerning_(0), subst_(makeSubstitute(c))
{}



Inset * InsetMathChar::clone() const
{
	return new InsetMathChar(*this);
}


void InsetMathChar::metrics(MetricsInfo & mi, Dimension & dim) const
{
	string const & f = mi.base.fontname;
	if (isMathFont(f) && subst_) {
		// If the char has a substitute, draw the replacement symbol
		// instead, but only in math mode.
		kerning_ = mathedSymbolDim(mi.base, dim, subst_);
	} else if (!slanted(char_) && f == "mathnormal") {
		Changer dummy = mi.base.font.changeShape(UP_SHAPE);
		dim = theFontMetrics(mi.base.font).dimension(char_);
		kerning_ = 0;
	} else if (!isASCII(char_) && Encodings::unicodeCharInfo(char_).isUnicodeSymbol()) {
		bool special_font = (f == "mathbb" ||
				     f == "mathds" ||
				     f == "mathfrak" ||
				     f == "mathcal" ||
				     f == "mathscr");
		Changer dummy1 = special_font
				? mi.base.changeFontSet("mathnormal")
				: noChange();
		Changer dummy2 = Encodings::isMathAlpha(char_)
				? noChange()
				: mi.base.font.changeShape(UP_SHAPE);
		dim = theFontMetrics(mi.base.font).dimension(char_);
		kerning_ = -mathed_char_kerning(mi.base.font, char_);
	} else {
		frontend::FontMetrics const & fm = theFontMetrics(mi.base.font);
		dim = fm.dimension(char_);
		kerning_ = mathed_char_kerning(mi.base.font, char_);
		// cmmi has almost no left bearing: simulate this
		if (f == "mathnormal") {
			dim.wid += max(-fm.lbearing(char_), 0);
		}
		// Rule 17 from Appendix G
		// These are the fonts with fontdimen(2)==0.
		// To properly handle the case fontdimen(2)!=0 (that is for all other
		// math fonts), where the italic correction must also be converted into
		// kerning but only at the end of strings of characters with the same
		// font, one would need a more elaborate implementation in MathRow. For
		// now the case fontdimen(2)==0 is the most important.
		if (f == "mathnormal" || f == "mathscr" || f == "mathcal") {
			dim.wid += kerning_;
			// We use a negative value to tell InsetMathScript to move the
			// subscript leftwards instead of the superscript rightwards
			kerning_ = -kerning_;
		}
	}
}


void InsetMathChar::draw(PainterInfo & pi, int x, int y) const
{
	// lyxerr << "drawing '" << char_ << "' font: " << pi.base.fontname << std::endl;
	if (isMathFont(pi.base.fontname)) {
		if (subst_) {
			// If the char has a substitute, draw the replacement symbol
			// instead, but only in math mode.
			mathedSymbolDraw(pi, x, y, subst_);
			return;
		} else if (!slanted(char_) && pi.base.fontname == "mathnormal") {
			Changer dummy = pi.base.font.changeShape(UP_SHAPE);
			pi.draw(x, y, char_);
			return;
		} else if (!isASCII(char_) && Encodings::unicodeCharInfo(char_).isUnicodeSymbol()) {
			bool special_font = (pi.base.fontname == "mathbb" ||
					     pi.base.fontname == "mathds" ||
					     pi.base.fontname == "mathfrak" ||
					     pi.base.fontname == "mathcal" ||
					     pi.base.fontname == "mathscr");
			Changer dummy1 = special_font
				? pi.base.changeFontSet("mathnormal")
				: noChange();
			Changer dummy2 = Encodings::isMathAlpha(char_)
			        ? noChange()
			        : pi.base.font.changeShape(UP_SHAPE);
			pi.draw(x, y, char_);
			return;
		}
	}
	// cmmi has almost no left bearing: simulate this
	if (pi.base.fontname == "mathnormal") {
		x += max(-theFontMetrics(pi.base.font).lbearing(char_), 0);
	}
	pi.draw(x, y, char_);
}


void InsetMathChar::draw(PainterInfo & pi, int x, int y,
                         frontend::InputMethod const * im,
                         pos_type const char_format_index) const
{
	pi.draw(x, y, char_, im, char_format_index, &pi.base.font);
}


void InsetMathChar::metricsT(TextMetricsInfo const &, Dimension & dim) const
{
	dim.wid = 1;
	dim.asc = 1;
	dim.des = 0;
}


void InsetMathChar::drawT(TextPainter & pain, int x, int y) const
{
	//LYXERR0("drawing text '" << to_string(char_) << "' code: " << char_);
	pain.draw(x, y, char_);
}


void InsetMathChar::writeMath(TeXMathStream & os) const
{
	if (os.latex() && os.pendingSpace()) {
		if (isAlphaASCII(char_))
			os.os() << ' ';
		os.pendingSpace(false);
	}
	os.os().put(char_);
}


void InsetMathChar::validate(LaTeXFeatures & features) const
{
	if (!isASCII(char_))
		BufferEncodings::validate(char_, features, true);
}


void InsetMathChar::normalize(NormalStream & os) const
{
	os << "[char ";
	os.os().put(char_);
	os << " mathalpha]";
}


void InsetMathChar::octave(OctaveStream & os) const
{
	os.os().put(char_);
}


// We have a bit of a problem here. MathML wants to know whether the
// character represents an "identifier" or an "operator", and we have
// no general way of telling. So we shall guess: If it's alpha or
// mathalpha, then we'll treat it as an identifier, otherwise as an
// operator.
// Worst case: We get bad spacing, or bad italics.
// In any case, never let MathML stretch a single character when it
// is recognised as an operator, to match TeX' behaviour.
void InsetMathChar::mathmlize(MathMLStream & ms) const
{
	// The characters hard-coded here do not need to respect the font,
	// they do not have a Unicode mapping (in the sense of
	// unicode_alphanum_variants).
	std::string entity;
	switch (char_) {
	case '<': entity = "&lt;"; break;
	case '>': entity = "&gt;"; break;
	case '&': entity = "&amp;"; break;
	case '-':
		if (ms.version() == MathMLVersion::mathmlCore) {
			// − U+2212 MINUS SIGN
			entity = "&#x2212;";
		}
		break;
	case ' ': {
		ms << from_ascii("&#0160;");
		return;
	}
	default: break;
	}

	if (ms.inText()) {
		if (entity.empty())
			ms << StartRespectFont() << char_ << StopRespectFont();
		else
			ms << from_ascii(entity);
		return;
	}

	if (!entity.empty()) {
		ms << MTagInline("mo", "stretchy='false'")
		   << from_ascii(entity)
		   << ETagInline("mo");
		return;
	}

	char const * type =
		(isAlphaASCII(char_) || Encodings::isMathAlpha(char_))
			? "mi" : "mo";
	ms << MTagInline(type, std::string(type) == "mo" ? "stretchy='false'" : "")
	   << StartRespectFont() << char_type(char_) << StopRespectFont()
	   << ETagInline(type);
}


void InsetMathChar::htmlize(HtmlStream & ms) const
{
	std::string entity;
	// Not taking subst_ into account here because the MathML output of
	// <>=+-* looks correct as it is. FIXME: ' is not output as ^\prime
	switch (char_) {
		case '<': entity = "&lt;"; break;
		case '>': entity = "&gt;"; break;
		case '&': entity = "&amp;"; break;
		case ' ': entity = "&#160;"; break;
		default: break;
	}

	bool have_entity = !entity.empty();

	if (ms.inText()) {
		if (have_entity)
			ms << from_ascii(entity);
		else
			ms.os().put(char_);
		return;
	}

	if (have_entity) {
		// an operator, so give some space
		ms << ' ' << from_ascii(entity) << ' ';
		return;
	}

	if (isAlphaASCII(char_) || Encodings::isMathAlpha(char_))
		// we don't use MTag and ETag because we do not want the spacing
		ms << MTag("i") << char_type(char_) << ETag("i");
	else
		// an operator, so give some space
		ms << " " << char_type(char_) << " ";
}


MathClass InsetMathChar::mathClass() const
{
	// this information comes from fontmath.ltx in LaTeX source.
	if (subst_)
		return string_to_class(subst_->extra);

	if (!isASCII(char_))
		return MC_ORD;

	switch (static_cast<char>(char_)) {
	case ',':
	case ';':
		return MC_PUNCT;
	case '(':
	case '[':
		return MC_OPEN;
	case ')':
	case ']':
	case '!':
	case '?':
		return MC_CLOSE;
	default:
		return MC_ORD;
	}
}


} // namespace lyx
