#include <stdlib.h>
#include <algorithm>
#include <QString>
#include <QChar>
#include "boosty.h"
#include "scintillaeditor.h"
#include <Qsci/qscicommandset.h>
#include "Preferences.h"
#include "PlatformUtils.h"
#include "settings.h"

class SettingsConverter {
public:
	QsciScintilla::WrapMode toWrapMode(Value val);
	QsciScintilla::WrapVisualFlag toLineWrapVisualization(Value val);
	QsciScintilla::WrapIndentMode toLineWrapIndentationStyle(Value val);
	QsciScintilla::WhitespaceVisibility toShowWhitespaces(Value val);
};

QsciScintilla::WrapMode SettingsConverter::toWrapMode(Value val)
{
	std::string v = val.toString();
	if (v == "Char") {
		return QsciScintilla::WrapCharacter;
	} else if (v == "Word") {
		return QsciScintilla::WrapWord;
	} else {
		return QsciScintilla::WrapNone;
	}
}

QsciScintilla::WrapVisualFlag SettingsConverter::toLineWrapVisualization(Value val)
{
	std::string v = val.toString();
	if (v == "Text") {
		return QsciScintilla::WrapFlagByText;
	} else if (v == "Border") {
		return QsciScintilla::WrapFlagByBorder;
#if QSCINTILLA_VERSION >= 0x020700
	} else if (v == "Margin") {
		return QsciScintilla::WrapFlagInMargin;
#endif
	} else {
		return QsciScintilla::WrapFlagNone;
	}
}

QsciScintilla::WrapIndentMode SettingsConverter::toLineWrapIndentationStyle(Value val)
{
	std::string v = val.toString();
	if (v == "Same") {
		return QsciScintilla::WrapIndentSame;
	} else if (v == "Indented") {
		return QsciScintilla::WrapIndentIndented;
	} else {
		return QsciScintilla::WrapIndentFixed;
	}
}

QsciScintilla::WhitespaceVisibility SettingsConverter::toShowWhitespaces(Value val)
{
	std::string v = val.toString();
	if (v == "Always") {
		return QsciScintilla::WsVisible;
	} else if (v == "AfterIndentation") {
		return QsciScintilla::WsVisibleAfterIndent;
	} else {
		return QsciScintilla::WsInvisible;
	}
}

EditorColorScheme::EditorColorScheme(fs::path path) : path(path)
{
	try {
		boost::property_tree::read_json(boosty::stringy(path).c_str(), pt);
		_name = QString(pt.get<std::string>("name").c_str());
		_index = pt.get<int>("index");
	} catch (const std::exception & e) {
		PRINTB("Error reading color scheme file '%s': %s", path.c_str() % e.what());
		_name = "";
		_index = 0;
	}
}

EditorColorScheme::~EditorColorScheme()
{

}

bool EditorColorScheme::valid() const
{
	return !_name.isEmpty();
}

const QString & EditorColorScheme::name() const
{
	return _name;
}

int EditorColorScheme::index() const
{
	return _index;
}

const boost::property_tree::ptree & EditorColorScheme::propertyTree() const
{
	return pt;
}

ScintillaEditor::ScintillaEditor(QWidget *parent) : EditorInterface(parent)
{
	scintillaLayout = new QVBoxLayout(this);
	qsci = new QsciScintilla(this);

	// Force EOL mode to Unix, since QTextStream will manage local EOL modes.
	qsci->setEolMode(QsciScintilla::EolUnix);

	//
	// Remapping some scintilla key binding which conflict with OpenSCAD global
	// key bindings, as well as some minor scintilla bugs
	//
	QsciCommand *c;
#ifdef Q_OS_MAC
	// Alt-Backspace should delete left word (Alt-Delete already deletes right word)
	c = qsci->standardCommands()->find(QsciCommand::DeleteWordLeft);
	c->setKey(Qt::Key_Backspace | Qt::ALT);
#endif
	// Cmd/Ctrl-T is handled by the menu
	c = qsci->standardCommands()->boundTo(Qt::Key_T | Qt::CTRL);
	c->setKey(0);
	// Cmd/Ctrl-D is handled by the menu
	c = qsci->standardCommands()->boundTo(Qt::Key_D | Qt::CTRL);
	c->setKey(0);
	// Ctrl-Shift-Z should redo on all platforms
	c = qsci->standardCommands()->find(QsciCommand::Redo);
	c->setKey(Qt::Key_Z | Qt::CTRL | Qt::SHIFT);

	scintillaLayout->setContentsMargins(0, 0, 0, 0);
	scintillaLayout->addWidget(qsci);

	qsci->indicatorDefine(QsciScintilla::RoundBoxIndicator, indicatorNumber);
	qsci->markerDefine(QsciScintilla::Circle, markerNumber);
	qsci->setUtf8(true);
	qsci->setFolding(QsciScintilla::BoxedTreeFoldStyle, 4);

	lexer = new ScadLexer(this);
	qsci->setLexer(lexer);
	initMargin();

	connect(qsci, SIGNAL(textChanged()), this, SIGNAL(contentsChanged()));
	connect(qsci, SIGNAL(modificationChanged(bool)), this, SIGNAL(modificationChanged(bool)));
}

/**
 * Apply the settings that are changeable in the preferences. This is also
 * called in the event handler from the preferences.
 */
void ScintillaEditor::applySettings()
{
	SettingsConverter conv;
	Settings::Settings *s = Settings::Settings::inst();

	qsci->setIndentationWidth(s->get(Settings::Settings::indentationWidth).toDouble());
	qsci->setTabWidth(s->get(Settings::Settings::tabWidth).toDouble());
	qsci->setWrapMode(conv.toWrapMode(s->get(Settings::Settings::lineWrap)));
	qsci->setWrapIndentMode(conv.toLineWrapIndentationStyle(s->get(Settings::Settings::lineWrapIndentationStyle)));
	qsci->setWrapVisualFlags(conv.toLineWrapVisualization(s->get(Settings::Settings::lineWrapVisualizationEnd)),
		conv.toLineWrapVisualization(s->get(Settings::Settings::lineWrapVisualizationBegin)),
		s->get(Settings::Settings::lineWrapIndentation).toDouble());
	qsci->setWhitespaceVisibility(conv.toShowWhitespaces(s->get(Settings::Settings::showWhitespace)));
	qsci->setWhitespaceSize(s->get(Settings::Settings::showWhitespaceSize).toDouble());
	qsci->setAutoIndent(s->get(Settings::Settings::autoIndent).toBool());

	std::string indentStyle = s->get(Settings::Settings::indentStyle).toString();
	qsci->setIndentationsUseTabs(indentStyle == "Tabs");
	std::string tabKeyFunction = s->get(Settings::Settings::tabKeyFunction).toString();
	qsci->setTabIndents(tabKeyFunction == "Indent");

	qsci->setBraceMatching(s->get(Settings::Settings::enableBraceMatching).toBool() ? QsciScintilla::SloppyBraceMatch : QsciScintilla::NoBraceMatch);
	qsci->setCaretLineVisible(s->get(Settings::Settings::highlightCurrentLine).toBool());
}

void ScintillaEditor::setPlainText(const QString &text)
{
	qsci->setText(text);
	setContentModified(false);
}

QString ScintillaEditor::toPlainText()
{
	return qsci->text();
}

void ScintillaEditor::setContentModified(bool modified)
{
	// FIXME: Due to an issue with QScintilla, we need to do this on the document itself.
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
	qsci->SCN_SAVEPOINTLEFT();
#endif
	qsci->setModified(modified);
}

bool ScintillaEditor::isContentModified()
{
	return qsci->isModified();
}

void ScintillaEditor::highlightError(int error_pos)
{
	int line, index;
	qsci->lineIndexFromPosition(error_pos, &line, &index);
	qsci->fillIndicatorRange(line, index, line, index + 1, indicatorNumber);
	qsci->markerAdd(line, markerNumber);
}

void ScintillaEditor::unhighlightLastError()
{
	int totalLength = qsci->text().length();
	int line, index;
	qsci->lineIndexFromPosition(totalLength, &line, &index);
	qsci->clearIndicatorRange(0, 0, line, index, indicatorNumber);
	qsci->markerDeleteAll(markerNumber);
}

QColor ScintillaEditor::readColor(const boost::property_tree::ptree &pt, const std::string name, const QColor defaultColor)
{
	try {
		const std::string val = pt.get<std::string>(name);
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
		if ((val.length() == 9) && (val.at(0) == '#')) {
			const std::string rgb = std::string("#") + val.substr(3);
			QColor qcol(rgb.c_str());
			unsigned long alpha = std::strtoul(val.substr(1, 2).c_str(), 0, 16);
			qcol.setAlpha(alpha);
			return qcol;
		}
#endif
		return QColor(val.c_str());
	} catch (std::exception e) {
		return defaultColor;
	}
}

std::string ScintillaEditor::readString(const boost::property_tree::ptree &pt, const std::string name, const std::string defaultValue)
{
	try {
		const std::string val = pt.get<std::string>(name);
		return val;
	} catch (std::exception e) {
		return defaultValue;
	}
}

int ScintillaEditor::readInt(const boost::property_tree::ptree &pt, const std::string name, const int defaultValue)
{
	try {
		const int val = pt.get<int>(name);
		return val;
	} catch (std::exception e) {
		return defaultValue;
	}
}

void ScintillaEditor::setColormap(const EditorColorScheme *colorScheme)
{
	const boost::property_tree::ptree & pt = colorScheme->propertyTree();

	try {
          QFont font = lexer->font(lexer->defaultStyle());
		const QColor textColor(pt.get<std::string>("text").c_str());
		const QColor paperColor(pt.get<std::string>("paper").c_str());

		ScadLexer *l = new ScadLexer(this);

		// Keywords must be set before the lexer is attached to QScintilla
		// as they seem to be read and cached at attach time.
		boost::optional<const boost::property_tree::ptree&> keywords = pt.get_child_optional("keywords");
		if (keywords.is_initialized()) {
			l->setKeywords(1, readString(keywords.get(), "keyword-set1", ""));
			l->setKeywords(2, readString(keywords.get(), "keyword-set2", ""));
			l->setKeywords(3, readString(keywords.get(), "keyword-set-doc", ""));
			l->setKeywords(4, readString(keywords.get(), "keyword-set3", ""));
		}

		qsci->setLexer(l);
		delete lexer;
		lexer = l;

		// All other properties must be set after attaching to QSCintilla so
		// the editor gets the change events and updates itself to match
		l->setFont(font);
		l->setColor(textColor);
		l->setPaper(paperColor);
                // Somehow, the margin font got lost when we deleted the old lexer
                qsci->setMarginsFont(font);

		const boost::property_tree::ptree& colors = pt.get_child("colors");
		l->setColor(readColor(colors, "keyword1", textColor), QsciLexerCPP::Keyword);
		l->setColor(readColor(colors, "keyword2", textColor), QsciLexerCPP::KeywordSet2);
		l->setColor(readColor(colors, "keyword3", textColor), QsciLexerCPP::GlobalClass);
		l->setColor(readColor(colors, "number", textColor), QsciLexerCPP::Number);
		l->setColor(readColor(colors, "string", textColor), QsciLexerCPP::SingleQuotedString);
		l->setColor(readColor(colors, "string", textColor), QsciLexerCPP::DoubleQuotedString);
		l->setColor(readColor(colors, "operator", textColor), QsciLexerCPP::Operator);
		l->setColor(readColor(colors, "comment", textColor), QsciLexerCPP::Comment);
		l->setColor(readColor(colors, "commentline", textColor), QsciLexerCPP::CommentLine);
		l->setColor(readColor(colors, "commentdoc", textColor), QsciLexerCPP::CommentDoc);
		l->setColor(readColor(colors, "commentdoc", textColor), QsciLexerCPP::CommentLineDoc);
		l->setColor(readColor(colors, "commentdockeyword", textColor), QsciLexerCPP::CommentDocKeyword);

		const boost::property_tree::ptree& caret = pt.get_child("caret");
		qsci->setCaretWidth(readInt(caret, "width", 1));
		qsci->setCaretForegroundColor(readColor(caret, "foreground", textColor));
		qsci->setCaretLineBackgroundColor(readColor(caret, "line-background", paperColor));

		qsci->setMarkerBackgroundColor(readColor(colors, "error-marker", QColor(255, 0, 0, 100)), markerNumber);
		qsci->setIndicatorForegroundColor(readColor(colors, "error-indicator", QColor(255, 0, 0, 100)), indicatorNumber);
		qsci->setIndicatorOutlineColor(readColor(colors, "error-indicator-outline", QColor(255, 0, 0, 100)), indicatorNumber);
		qsci->setWhitespaceForegroundColor(readColor(colors, "whitespace-foreground", textColor));
		qsci->setMarginsBackgroundColor(readColor(colors, "margin-background", paperColor));
		qsci->setMarginsForegroundColor(readColor(colors, "margin-foreground", textColor));
		qsci->setFoldMarginColors(readColor(colors, "margin-background", paperColor),
                                          readColor(colors, "margin-background", paperColor));
		qsci->setMatchedBraceBackgroundColor(readColor(colors, "matched-brace-background", paperColor));
		qsci->setMatchedBraceForegroundColor(readColor(colors, "matched-brace-foreground", textColor));
		qsci->setUnmatchedBraceBackgroundColor(readColor(colors, "unmatched-brace-background", paperColor));
		qsci->setUnmatchedBraceForegroundColor(readColor(colors, "unmatched-brace-foreground", textColor));
		qsci->setSelectionForegroundColor(readColor(colors, "selection-foreground", paperColor));
		qsci->setSelectionBackgroundColor(readColor(colors, "selection-background", textColor));
		qsci->setEdgeColor(readColor(colors, "edge", textColor));
	} catch (std::exception e) {
		noColor();
	}
}

void ScintillaEditor::noColor()
{
	lexer->setPaper(Qt::white);
	lexer->setColor(Qt::black);
	qsci->setCaretWidth(2);
	qsci->setCaretForegroundColor(Qt::black);
	qsci->setMarkerBackgroundColor(QColor(255, 0, 0, 100), markerNumber);
	qsci->setIndicatorForegroundColor(QColor(255, 0, 0, 128), indicatorNumber);
	qsci->setIndicatorOutlineColor(QColor(0, 0, 0, 255), indicatorNumber); // only alpha part is used
	qsci->setCaretLineBackgroundColor(Qt::white);
	qsci->setWhitespaceForegroundColor(Qt::black);
	qsci->setSelectionForegroundColor(Qt::black);
	qsci->setSelectionBackgroundColor(QColor("LightSkyBlue"));
	qsci->setMatchedBraceBackgroundColor(QColor("LightBlue"));
	qsci->setMatchedBraceForegroundColor(Qt::black);
	qsci->setUnmatchedBraceBackgroundColor(QColor("pink"));
	qsci->setUnmatchedBraceForegroundColor(Qt::black);
	qsci->setMarginsBackgroundColor(QColor("whiteSmoke"));
	qsci->setMarginsForegroundColor(QColor("gray"));
	qsci->setFoldMarginColors(QColor("whiteSmoke"), QColor("whiteSmoke"));
	qsci->setEdgeColor(Qt::black);
}

void ScintillaEditor::enumerateColorSchemesInPath(ScintillaEditor::colorscheme_set_t &result_set, const fs::path path)
{
	const fs::path color_schemes = path / "color-schemes" / "editor";

	fs::directory_iterator end_iter;

	if (fs::exists(color_schemes) && fs::is_directory(color_schemes)) {
		for (fs::directory_iterator dir_iter(color_schemes); dir_iter != end_iter; ++dir_iter) {
			if (!fs::is_regular_file(dir_iter->status())) {
				continue;
			}

			const fs::path path = (*dir_iter).path();
			if (!(path.extension().string() == ".json")) {
				continue;
			}

			EditorColorScheme *colorScheme = new EditorColorScheme(path);
			if (colorScheme->valid()) {
				result_set.insert(colorscheme_set_t::value_type(colorScheme->index(), boost::shared_ptr<EditorColorScheme>(colorScheme)));
			} else {
				delete colorScheme;
			}
		}
	}
}

ScintillaEditor::colorscheme_set_t ScintillaEditor::enumerateColorSchemes()
{
	colorscheme_set_t result_set;

	enumerateColorSchemesInPath(result_set, PlatformUtils::resourceBasePath());
	enumerateColorSchemesInPath(result_set, PlatformUtils::userConfigPath());

	return result_set;
}

QStringList ScintillaEditor::colorSchemes()
{
	const colorscheme_set_t colorscheme_set = enumerateColorSchemes();

	QStringList colorSchemes;
	for (colorscheme_set_t::const_iterator it = colorscheme_set.begin(); it != colorscheme_set.end(); it++) {
		colorSchemes << (*it).second.get()->name();
	}
	colorSchemes << "Off";

	return colorSchemes;
}

void ScintillaEditor::setHighlightScheme(const QString &name)
{
	const colorscheme_set_t colorscheme_set = enumerateColorSchemes();

	for (colorscheme_set_t::const_iterator it = colorscheme_set.begin(); it != colorscheme_set.end(); it++) {
		const EditorColorScheme *colorScheme = (*it).second.get();
		if (colorScheme->name() == name) {
			setColormap(colorScheme);
			return;
		}
	}

	noColor();
}

void ScintillaEditor::insert(const QString &text)
{
	qsci->insert(text);
}

void ScintillaEditor::setText(const QString &text)
{
	qsci->selectAll(true);
	qsci->replaceSelectedText(text);
}

void ScintillaEditor::undo()
{
	qsci->undo();
}

void ScintillaEditor::redo()
{
	qsci->redo();
}

void ScintillaEditor::cut()
{
	qsci->cut();
}

void ScintillaEditor::copy()
{
	qsci->copy();
}

void ScintillaEditor::paste()
{
	qsci->paste();
}

void ScintillaEditor::zoomIn()
{
	qsci->zoomIn();
}

void ScintillaEditor::zoomOut()
{
	qsci->zoomOut();
}

void ScintillaEditor::initFont(const QString& fontName, uint size)
{
  this->currentFont = QFont(fontName, size);
  this->currentFont.setFixedPitch(true);
  lexer->setFont(this->currentFont);
  qsci->setMarginsFont(this->currentFont);
  onTextChanged(); // Update margin width
}

void ScintillaEditor::initMargin()
{
  qsci->setMarginLineNumbers(1, true);
  connect(qsci, SIGNAL(textChanged()), this, SLOT(onTextChanged()));
}

void ScintillaEditor::onTextChanged()
{
  QFontMetrics fontmetrics(this->currentFont);
  qsci->setMarginWidth(1, QString(trunc(log10(qsci->lines())+2), '0'));
}

bool ScintillaEditor::find(const QString &expr, bool findNext, bool findBackwards)
{
	int startline = -1, startindex = -1;

	// If findNext, start from the end of the current selection
	if (qsci->hasSelectedText()) {
		int lineFrom, indexFrom, lineTo, indexTo;
		qsci->getSelection(&lineFrom, &indexFrom, &lineTo, &indexTo);

		startline = !(findBackwards xor findNext) ? std::min(lineFrom, lineTo) : std::max(lineFrom, lineTo);
		startindex = !(findBackwards xor findNext) ? std::min(indexFrom, indexTo) : std::max(indexFrom, indexTo);
	}

	return qsci->findFirst(expr, false, false, false, true,
		!findBackwards, startline, startindex);
}

void ScintillaEditor::replaceSelectedText(const QString &newText)
{
	if (qsci->selectedText() != newText) qsci->replaceSelectedText(newText);
}

void ScintillaEditor::replaceAll(const QString &findText, const QString &replaceText)
{
  // We need to issue a Select All first due to a bug in QScintilla:
  // It doesn't update the find range when just doing findFirst() + findNext() causing the search
  // to end prematurely if the replaced string is larger than the selected string.
#if QSCINTILLA_VERSION >= 0x020700
  qsci->selectAll();
  if (qsci->findFirstInSelection(findText, 
                      false /*re*/, false /*cs*/, false /*wo*/, 
                      false /*wrap*/, true /*forward*/)) {
#else
    // findFirstInSelection() was introduced in QScintilla 2.7
  if (qsci->findFirst(findText, 
                      false /*re*/, false /*cs*/, false /*wo*/, 
                      false /*wrap*/, true /*forward*/, 0, 0)) {
#endif
    qsci->replace(replaceText);
    while (qsci->findNext()) {
      qsci->replace(replaceText);
    }
  }
}

void ScintillaEditor::getRange(int *lineFrom, int *lineTo)
{
	int indexFrom, indexTo;
	if (qsci->hasSelectedText()) {
		qsci->getSelection(lineFrom, &indexFrom, lineTo, &indexTo);
		if (indexTo == 0) {
			*lineTo = *lineTo - 1;
		}
	} else {
		qsci->getCursorPosition(lineFrom, &indexFrom);
		*lineTo = *lineFrom;
	}
}

void ScintillaEditor::indentSelection()
{
	int lineFrom, lineTo;
	getRange(&lineFrom, &lineTo);
	for (int line = lineFrom; line <= lineTo; line++) {
		qsci->indent(line);
	}
}

void ScintillaEditor::unindentSelection()
{
	int lineFrom, lineTo;
	getRange(&lineFrom, &lineTo);
	for (int line = lineFrom; line <= lineTo; line++) {
		qsci->unindent(line);
	}
}

void ScintillaEditor::commentSelection()
{
	bool hasSelection = qsci->hasSelectedText();

	int lineFrom, lineTo;
	getRange(&lineFrom, &lineTo);
	for (int line = lineFrom; line <= lineTo; line++) {
		qsci->insertAt("//", line, 0);
	}

	if (hasSelection) {
		qsci->setSelection(lineFrom, 0, lineTo, std::max(0, qsci->lineLength(lineTo) - 1));
	}
}

void ScintillaEditor::uncommentSelection()
{
	bool hasSelection = qsci->hasSelectedText();

	int lineFrom, lineTo;
	getRange(&lineFrom, &lineTo);
	for (int line = lineFrom; line <= lineTo; line++) {
		QString lineText = qsci->text(line);
		if (lineText.startsWith("//")) {
			qsci->setSelection(line, 0, line, 2);
			qsci->removeSelectedText();
		}
	}
	if (hasSelection) {
		qsci->setSelection(lineFrom, 0, lineTo, std::max(0, qsci->lineLength(lineTo) - 1));
	}
}

QString ScintillaEditor::selectedText()
{
	return qsci->selectedText();
}
