/* 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 "graphics/managed_surface.h"

#include "mtropolis/plugin/obsidian.h"
#include "mtropolis/plugins.h"

#include "mtropolis/miniscript.h"

namespace MTropolis {

namespace Obsidian {

MovementModifier::MovementModifier() : _type(false), _rate(0), _frequency(0),
	_moveStartTime(0), _runtime(nullptr) {
}

MovementModifier::~MovementModifier() {
	if (_moveEvent)
		_moveEvent->cancel();
}

bool MovementModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::MovementModifier &data) {

	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent))
		return false;

	if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_disableWhen.load(data.disableWhen.value.asEvent))
		return false;

	if (data.rate.type != Data::PlugInTypeTaggedValue::kFloat)
		return false;

	_rate = data.rate.value.asFloat.toXPFloat().toDouble();

	if (data.frequency.type != Data::PlugInTypeTaggedValue::kInteger)
		return false;

	_frequency = data.frequency.value.asInt;

	if (data.type.type != Data::PlugInTypeTaggedValue::kBoolean)
		return false;

	_type = (data.type.value.asBoolean != 0);

	if (data.dest.type != Data::PlugInTypeTaggedValue::kPoint || !data.dest.value.asPoint.toScummVMPoint(_dest))
		return false;

	if (data.triggerEvent.type != Data::PlugInTypeTaggedValue::kEvent || !_triggerEvent.load(data.triggerEvent.value.asEvent))
		return false;

	return true;
}

bool MovementModifier::respondsToEvent(const Event &evt) const {
	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
}

VThreadState MovementModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
	if (_enableWhen.respondsTo(msg->getEvent())) {
		Structural *structural = findStructuralOwner();
		if (structural == nullptr || !structural->isElement() || !static_cast<Element *>(structural)->isVisual()) {
			warning("Movement modifier wasn't attached to a visual element");
			return kVThreadError;
		}
		VisualElement *visual = static_cast<VisualElement *>(structural);

		Common::Rect startRect = visual->getRelativeRect();
		_moveStartPoint = Common::Point(startRect.left, startRect.top);
		_moveStartTime = runtime->getPlayTime();

		if (!_moveEvent) {
			_runtime = runtime;
			_moveEvent = runtime->getScheduler().scheduleMethod<MovementModifier, &MovementModifier::triggerMove>(runtime->getPlayTime() + 1, this);
		}
	}
	if (_disableWhen.respondsTo(msg->getEvent())) {
		disable(runtime);
	}

	return kVThreadReturn;
}

void MovementModifier::disable(Runtime *runtime) {
	if (_moveEvent) {
		_moveEvent->cancel();
		_moveEvent.reset();
	}
}

MiniscriptInstructionOutcome MovementModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "type") {
		DynamicValueWriteBoolHelper::create(&_type, result);
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (attrib == "dest") {
		DynamicValueWritePointHelper::create(&_dest, result);
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (attrib == "rate") {
		DynamicValueWriteFloatHelper<double>::create(&_rate, result);
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (attrib == "frequency") {
		DynamicValueWriteIntegerHelper<int32>::create(&_frequency, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

#ifdef MTROPOLIS_DEBUG_ENABLE
void MovementModifier::debugInspect(IDebugInspectionReport *report) const {
	Modifier::debugInspect(report);

	report->declareDynamic("enableWhen", Common::String::format("Event(%i,%i)", static_cast<int>(_enableWhen.eventType), static_cast<int>(_enableWhen.eventInfo)));
	report->declareDynamic("disableWhen", Common::String::format("Event(%i,%i)", static_cast<int>(_disableWhen.eventType), static_cast<int>(_disableWhen.eventInfo)));
	report->declareDynamic("rate", Common::String::format("%g", _rate));
	report->declareDynamic("frequency", Common::String::format("%i", static_cast<int>(_frequency)));
	report->declareDynamic("type", Common::String::format(_type ? "true" : "false"));
	report->declareDynamic("dest", Common::String::format("(%i,%i)", static_cast<int>(_dest.x), static_cast<int>(_dest.y)));
	report->declareDynamic("triggerEvent", Common::String::format("Event(%i,%i)", static_cast<int>(_triggerEvent.eventType), static_cast<int>(_triggerEvent.eventInfo)));
}
#endif

Common::SharedPtr<Modifier> MovementModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new MovementModifier(*this));
}

const char *MovementModifier::getDefaultName() const {
	return "Movement";
}

void MovementModifier::triggerMove(Runtime *runtime) {
	_moveEvent.reset();

	Structural *structural = findStructuralOwner();
	if (structural == nullptr || !structural->isElement() || !static_cast<Element *>(structural)->isVisual()) {
		warning("Movement modifier wasn't attached to a visual element");
		return;
	}
	VisualElement *visual = static_cast<VisualElement *>(structural);


	Common::Point delta = _dest - _moveStartPoint;

	double deltaLength = sqrt(delta.x * delta.x + delta.y * delta.y);

	double progression = 1.0;
	if (deltaLength > 0.0 && _rate > 0.0) {
		double distance = static_cast<double>(runtime->getPlayTime() - _moveStartTime) * _rate / 1000.0;
		progression = distance / deltaLength;
		if (progression > 1.0)
			progression = 1.0;
		if (progression < 0.0)
			progression = 0.0;
	}

	int32 targetX = _moveStartPoint.x + static_cast<int32>(round((_dest.x - _moveStartPoint.x) * progression));
	int32 targetY = _moveStartPoint.y + static_cast<int32>(round((_dest.y - _moveStartPoint.y) * progression));

	Common::Rect relRect = visual->getRelativeRect();
	int32 xDelta = targetX - relRect.left;
	int32 yDelta = targetY - relRect.top;

	relRect.left += xDelta;
	relRect.right += xDelta;
	relRect.top += yDelta;
	relRect.bottom += yDelta;

	visual->setRelativeRect(relRect);

	if (progression == 1.0) {
		Common::SharedPtr<MessageProperties> props(new MessageProperties(_triggerEvent, DynamicValue(), visual->getSelfReference()));
		Common::SharedPtr<MessageDispatch> dispatch(new MessageDispatch(props, visual, true, true, false));
		runtime->sendMessageOnVThread(dispatch);
	} else {
		_moveEvent = runtime->getScheduler().scheduleMethod<MovementModifier, &MovementModifier::triggerMove>(runtime->getPlayTime() + 1, this);
	}
}

RectShiftModifier::RectShiftModifier() : _direction(0), _runtime(nullptr), _isActive(false) {
}

RectShiftModifier::~RectShiftModifier() {
	if (_isActive)
		_runtime->removePostEffect(this);
}

bool RectShiftModifier::respondsToEvent(const Event &evt) const {
	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
}

VThreadState RectShiftModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
	if (_enableWhen.respondsTo(msg->getEvent()) && !_isActive) {
		_runtime = runtime;
		_runtime->addPostEffect(this);
		_isActive = true;
	}
	if (_disableWhen.respondsTo(msg->getEvent()) && _isActive) {
		disable(runtime);
	}

	return kVThreadReturn;
}

void RectShiftModifier::disable(Runtime *runtime) {
	if (_isActive) {
		_isActive = false;
		_runtime->removePostEffect(this);
		_runtime = nullptr;
	}
}

bool RectShiftModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::RectShiftModifier &data) {
	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent))
		return false;

	if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_disableWhen.load(data.disableWhen.value.asEvent))
		return false;

	if (data.direction.type != Data::PlugInTypeTaggedValue::kInteger)
		return false;

	_direction = data.direction.value.asInt;

	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent || !_enableWhen.load(data.enableWhen.value.asEvent))
		return false;

	return true;
}

MiniscriptInstructionOutcome RectShiftModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "direction") {
		DynamicValueWriteIntegerHelper<int32>::create(&_direction, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

void RectShiftModifier::renderPostEffect(Graphics::ManagedSurface &surface) const {
	Structural *structural = findStructuralOwner();
	if (!structural)
		return;

	if (!structural->isElement() || !static_cast<Element *>(structural)->isVisual())
		return;

	VisualElement *visual = static_cast<VisualElement *>(structural);

	Common::Point absOrigin = visual->getCachedAbsoluteOrigin();
	Common::Rect relRect = visual->getRelativeRect();
	Common::Rect absRect(absOrigin.x, absOrigin.y, absOrigin.x + relRect.width(), absOrigin.y + relRect.height());

	if (absRect.left < 0)
		absRect.left = 0;
	if (absRect.right >= surface.w)
		absRect.right = surface.w;
	if (absRect.top < 0)
		absRect.top = 0;
	if (absRect.bottom >= surface.h)
		absRect.bottom = surface.h;

	if (_direction == 1) {
		if (absRect.bottom + 1 >= surface.h)
			absRect.bottom--;
	} else if (_direction == 4) {
		if (absRect.right + 1 >= surface.w)
			absRect.right--;
	} else
		return;

	if (!absRect.isValidRect())
		return;

	uint pitch = (absRect.right - absRect.left) * surface.format.bytesPerPixel;

	for (int32 y = absRect.top; y < absRect.bottom; y++) {
		void *destPixels = surface.getBasePtr(absRect.left, y);
		void *srcPixels = destPixels;

		if (_direction == 1)
			srcPixels = surface.getBasePtr(absRect.left, y + 1);
		else if (_direction == 4)
			srcPixels = surface.getBasePtr(absRect.left + 1, y);

		memmove(destPixels, srcPixels, pitch);
	}
}

#ifdef MTROPOLIS_DEBUG_ENABLE
void RectShiftModifier::debugInspect(IDebugInspectionReport *report) const {
	Modifier::debugInspect(report);

	report->declareDynamic("direction", Common::String::format("%i", static_cast<int>(_direction)));
}
#endif

Common::SharedPtr<Modifier> RectShiftModifier::shallowClone() const {
	Common::SharedPtr<RectShiftModifier> clone(new RectShiftModifier(*this));
	clone->_isActive = false;
	clone->_runtime = nullptr;
	return clone;
}

const char *RectShiftModifier::getDefaultName() const {
	return "RectShift";
}

TextWorkModifier::TextWorkModifier() : _firstChar(0), _lastChar(0) {
}

bool TextWorkModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::TextWorkModifier &data) {
	return true;
}

bool TextWorkModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
	if (attrib == "numchar") {
		result.setInt(_string.size());
		return true;
	} else if (attrib == "output") {
		int32 firstChar = _firstChar - 1;
		int32 len = _lastChar - _firstChar + 1;
		if (_firstChar < 0) {
			len += firstChar;
			firstChar = 0;
		}
		if (len <= 0 || static_cast<size_t>(firstChar) >= _string.size())
			result.setString("");
		else {
			const size_t availChars = _string.size() - firstChar;
			if (static_cast<size_t>(len) > availChars)
				len = availChars;
			result.setString(_string.substr(firstChar, len));
		}
		return true;
	} else if (attrib == "exists") {
		bool exists = (caseInsensitiveFind(_string, _token) != Common::String::npos);
		result.setInt(exists ? 1 : 0);
		return true;
	} else if (attrib == "index") {
		size_t index = caseInsensitiveFind(_string, _token);
		if (index == Common::String::npos)
			index = 0;
		else
			index++;

		result.setInt(index);
		return true;
	} else if (attrib == "numword") {
		int numWords = 0;
		bool lastWasWhitespace = true;
		for (size_t i = 0; i < _string.size(); i++) {
			char c = _string[i];
			bool isWhitespace = (c <= ' ');
			if (lastWasWhitespace && !isWhitespace)
				numWords++;
			lastWasWhitespace = isWhitespace;
		}

		result.setInt(numWords);
		return true;
	}

	return Modifier::readAttribute(thread, result, attrib);
}

MiniscriptInstructionOutcome TextWorkModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "string") {
		DynamicValueWriteStringHelper::create(&_string, result);
		return kMiniscriptInstructionOutcomeContinue;
	} else if (attrib == "firstchar") {
		DynamicValueWriteIntegerHelper<int32>::create(&_firstChar, result);
		return kMiniscriptInstructionOutcomeContinue;
	} else if (attrib == "lastchar") {
		DynamicValueWriteIntegerHelper<int32>::create(&_lastChar, result);
		return kMiniscriptInstructionOutcomeContinue;
	} else if (attrib == "token") {
		DynamicValueWriteStringHelper::create(&_token, result);
		return kMiniscriptInstructionOutcomeContinue;
	} else if (attrib == "firstword") {
		DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetFirstWord, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	} else if (attrib == "lastword") {
		DynamicValueWriteFuncHelper<TextWorkModifier, &TextWorkModifier::scriptSetLastWord, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

Common::SharedPtr<Modifier> TextWorkModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new TextWorkModifier(*this));
}

const char *TextWorkModifier::getDefaultName() const {
	return "TextWork";
}

MiniscriptInstructionOutcome TextWorkModifier::scriptSetFirstWord(MiniscriptThread *thread, const DynamicValue &value) {
	// This and lastword are only used in tandem with lastword, exact functionality is unclear since it's
	// also used in tandem with "output" which is normally used with firstchar+lastchar.
	// We attempt to emulate it by setting firstchar+lastchar to the correct values
	int32 asInteger = 0;
	if (!value.roundToInt(asInteger))
		return kMiniscriptInstructionOutcomeFailed;

	int numWords = 0;
	bool lastWasWhitespace = true;
	for (size_t i = 0; i < _string.size(); i++) {
		char c = _string[i];
		bool isWhitespace = (c <= ' ');
		if (lastWasWhitespace && !isWhitespace) {
			numWords++;

			if (numWords == asInteger) {
				_firstChar = i + 1;
				return kMiniscriptInstructionOutcomeContinue;
			}
		}
		lastWasWhitespace = isWhitespace;
	}

	thread->error("Invalid index for 'firstword'");
	return kMiniscriptInstructionOutcomeFailed;
}

MiniscriptInstructionOutcome TextWorkModifier::scriptSetLastWord(MiniscriptThread *thread, const DynamicValue &value) {
	int32 asInteger = 0;
	if (!value.roundToInt(asInteger))
		return kMiniscriptInstructionOutcomeFailed;

	int numWordEnds = 0;
	bool lastWasWhitespace = true;
	for (size_t i = 0; i < _string.size(); i++) {
		char c = _string[i];
		bool isWhitespace = (c <= ' ');
		if (!lastWasWhitespace && isWhitespace) {
			numWordEnds++;

			if (numWordEnds == asInteger) {
				_firstChar = i - 1;
				return kMiniscriptInstructionOutcomeContinue;
			}
		}
		lastWasWhitespace = isWhitespace;

		if (numWordEnds == asInteger) {
			_lastChar = i;
			return kMiniscriptInstructionOutcomeContinue;
		}
	}

	if (!lastWasWhitespace) {
		numWordEnds++;
		if (numWordEnds == asInteger) {
			_lastChar = _string.size();
			return kMiniscriptInstructionOutcomeContinue;
		}
	}

	thread->error("Invalid index for 'firstword'");
	return kMiniscriptInstructionOutcomeFailed;
}

DictionaryModifier::DictionaryModifier() : _plugIn(nullptr), _isIndexResolved(false), _index(0) {
}

bool DictionaryModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::DictionaryModifier &data) {
	if (data.str.type != Data::PlugInTypeTaggedValue::kString)
		return false;

	_str = data.str.value.asString;

	if (data.index.type != Data::PlugInTypeTaggedValue::kInteger)
		return false;

	_index = data.index.value.asInt;
	_isIndexResolved = true;
	_plugIn = static_cast<ObsidianPlugIn *>(context.plugIn);

	return true;
}

bool DictionaryModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
	if (attrib == "index") {
		resolveStringIndex();
		result.setInt(_index);
		return true;
	}
	if (attrib == "string") {
		result.setString(_str);
		return true;
	}

	return Modifier::readAttribute(thread, result, attrib);
}

MiniscriptInstructionOutcome DictionaryModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "index") {
		DynamicValueWriteFuncHelper<DictionaryModifier, &DictionaryModifier::scriptSetIndex, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (attrib == "string") {
		DynamicValueWriteFuncHelper<DictionaryModifier, &DictionaryModifier::scriptSetString, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

void DictionaryModifier::resolveStringIndex() {
	if (_isIndexResolved)
		return;

	_index = 0;
	_isIndexResolved = true;

	const Common::Array<WordGameData::WordBucket> &wordBuckets = _plugIn->getWordGameData()->getWordBuckets();

	size_t strLength = _str.size();
	if (strLength >= wordBuckets.size())
		return;

	const WordGameData::WordBucket &bucket = wordBuckets[strLength];

	size_t lowOffsetInclusive = 0;
	size_t highOffsetExclusive = bucket._wordIndexes.size();

	const char *strChars = _str.c_str();

	// Binary search
	while (lowOffsetInclusive != highOffsetExclusive) {
		const size_t midOffset = (lowOffsetInclusive + highOffsetExclusive) / 2;
		const char *chars = &bucket._chars[bucket._spacing * midOffset];

		bool isMidGreater = false;
		bool isMidLess = false;
		for (size_t i = 0; i < strLength; i++) {
			if (chars[i] > strChars[i]) {
				isMidGreater = true;
				break;
			} else if (chars[i] < strChars[i]) {
				isMidLess = true;
				break;
			}
		}

		if (isMidLess)
			lowOffsetInclusive = midOffset + 1;
		else if (isMidGreater)
			highOffsetExclusive = midOffset;
		else {
			_index = bucket._wordIndexes[midOffset] + 1;
			break;
		}
	}
}

MiniscriptInstructionOutcome DictionaryModifier::scriptSetString(MiniscriptThread *thread, const DynamicValue &value) {
	if (value.getType() != DynamicValueTypes::kString) {
		thread->error("Tried to set dictionary string to something that wasn't a string");
		return kMiniscriptInstructionOutcomeFailed;
	}

	if (_str != value.getString()) {
		_str = value.getString();
		_isIndexResolved = false;
	}

	return kMiniscriptInstructionOutcomeContinue;
}

MiniscriptInstructionOutcome DictionaryModifier::scriptSetIndex(MiniscriptThread *thread, const DynamicValue &value) {
	int32 asInteger = 0;
	if (!value.roundToInt(asInteger)) {
		thread->error("Tried to set dictionary index to something that wasn't a number");
		return kMiniscriptInstructionOutcomeFailed;
	}

	_index = asInteger;
	if (_index < 1)
		_str.clear();
	else {
		const size_t indexAdjusted = static_cast<size_t>(_index) - 1;
		const Common::Array<WordGameData::SortedWord> &sortedWords = _plugIn->getWordGameData()->getSortedWords();
		if (indexAdjusted >= sortedWords.size())
			_str.clear();
		else
			_str = Common::String(sortedWords[indexAdjusted]._chars, sortedWords[indexAdjusted]._length);
	}

	_isIndexResolved = true;

	return kMiniscriptInstructionOutcomeContinue;
}

Common::SharedPtr<Modifier> DictionaryModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new DictionaryModifier(*this));
}

const char *DictionaryModifier::getDefaultName() const {
	return "Dictionary";
}

WordMixerModifier::WordMixerModifier() : _matches(0), _result(0), _plugIn(nullptr) {
}

bool WordMixerModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::WordMixerModifier &data) {
	_plugIn = static_cast<const ObsidianPlugIn *>(context.plugIn);

	return true;
}

bool WordMixerModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
	if (attrib == "result") {
		result.setInt(_result);
		return true;
	}
	if (attrib == "matches") {
		result.setInt(_matches);
		return true;
	}
	if (attrib == "output") {
		result.setString(_output);
		return true;
	}

	return Modifier::readAttribute(thread, result, attrib);
}

MiniscriptInstructionOutcome WordMixerModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "input") {
		DynamicValueWriteFuncHelper<WordMixerModifier, &WordMixerModifier::scriptSetInput, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (attrib == "search") {
		DynamicValueWriteFuncHelper<WordMixerModifier, &WordMixerModifier::scriptSetSearch, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

MiniscriptInstructionOutcome WordMixerModifier::scriptSetInput(MiniscriptThread *thread, const DynamicValue &value) {
	if (value.getType() != DynamicValueTypes::kString) {
		thread->error("Invalid type for WordMixer input attribute");
		return kMiniscriptInstructionOutcomeFailed;
	}

	_input = value.getString();

	Common::Array<char> sourceChars;
	for (char c : _input) {
		if (c > ' ')
			sourceChars.push_back(invariantToLower(c));
	}

	Common::Array<bool> charIsUsed;
	charIsUsed.resize(sourceChars.size());

	const Common::Array<WordGameData::WordBucket> &wordBuckets = _plugIn->getWordGameData()->getWordBuckets();

	_output.clear();
	_matches = 0;

	size_t numWordBuckets = wordBuckets.size();
	for (size_t rbucket = 0; rbucket < numWordBuckets; rbucket++) {
		size_t wordLength = numWordBuckets - 1 - rbucket;

		const WordGameData::WordBucket &bucket = wordBuckets[wordLength];

		size_t numWords = bucket._wordIndexes.size();

		for (size_t wi = 0; wi < numWords; wi++) {
			const char *wordChars = &bucket._chars[bucket._spacing * wi];

			for (bool &b : charIsUsed)
				b = false;

			bool isMatch = true;
			for (size_t ci = 0; ci < wordLength; ci++) {
				const char wordChar = wordChars[ci];

				bool foundAvailableSource = false;
				for (size_t srci = 0; srci < sourceChars.size(); srci++) {
					if (sourceChars[srci] == wordChar && !charIsUsed[srci]) {
						foundAvailableSource = true;
						charIsUsed[srci] = true;
						break;
					}
				}

				if (!foundAvailableSource) {
					isMatch = false;
					break;
				}
			}

			if (isMatch) {
				if (_matches > 0)
					_output += ' ';
				_output += Common::String(wordChars, wordLength);
				_matches++;
			}
		}

		if (_matches > 0)
			break;
	}

	if (_matches == 0)
		_output = "xxx";

	return kMiniscriptInstructionOutcomeContinue;
}

MiniscriptInstructionOutcome WordMixerModifier::scriptSetSearch(MiniscriptThread *thread, const DynamicValue &value) {
	if (value.getType() != DynamicValueTypes::kBoolean) {
		thread->error("Invalid type for WordMixer search attribute");
		return kMiniscriptInstructionOutcomeFailed;
	}

	if (!value.getBool())
		return kMiniscriptInstructionOutcomeContinue;

	size_t searchLength = _input.size();
	const Common::Array<WordGameData::WordBucket> &buckets = _plugIn->getWordGameData()->getWordBuckets();
	_result = 0;
	if (searchLength < buckets.size()) {
		const WordGameData::WordBucket &bucket = buckets[searchLength];

		for (size_t wi = 0; wi < bucket._wordIndexes.size(); wi++) {
			const char *wordChars = &bucket._chars[wi * bucket._spacing];

			bool isMatch = true;
			for (size_t ci = 0; ci < searchLength; ci++) {
				if (invariantToLower(_input[ci]) != wordChars[ci]) {
					isMatch = false;
					break;
				}
			}

			if (isMatch) {
				_result = 1;
				break;
			}
		}
	}

	return kMiniscriptInstructionOutcomeContinue;
}

Common::SharedPtr<Modifier> WordMixerModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new WordMixerModifier(*this));
}

const char *WordMixerModifier::getDefaultName() const {
	return "WordMixer";
}

XorModModifier::XorModModifier() : _shapeID(0) {
}

bool XorModModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorModModifier &data) {
	if (data.enableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
		return false;

	if (!_enableWhen.load(data.enableWhen.value.asEvent))
		return false;

	if (data.disableWhen.type != Data::PlugInTypeTaggedValue::kEvent)
		return false;

	if (!_disableWhen.load(data.disableWhen.value.asEvent))
		return false;

	if (data.shapeID.type != Data::PlugInTypeTaggedValue::kInteger)
		return false;

	_shapeID = data.shapeID.value.asInt;

	return true;
}

bool XorModModifier::respondsToEvent(const Event &evt) const {
	return _enableWhen.respondsTo(evt) || _disableWhen.respondsTo(evt);
}

VThreadState XorModModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
	if (_enableWhen.respondsTo(msg->getEvent())) {
		Structural *owner = findStructuralOwner();
		if (!owner)
			return kVThreadError;

		if (!owner->isElement())
			return kVThreadReturn;

		Element *element = static_cast<Element *>(owner);
		if (!element->isVisual())
			return kVThreadReturn;

		VisualElement *visual = static_cast<VisualElement *>(element);

		VisualElementRenderProperties renderProps = visual->getRenderProperties();
		renderProps.setInkMode(VisualElementRenderProperties::kInkModeXor);

		if (_shapeID == 0)
			renderProps.setShape(VisualElementRenderProperties::kShapeRect);
		else
			renderProps.setShape(static_cast<VisualElementRenderProperties::Shape>(VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 + _shapeID - 1));

		visual->setRenderProperties(renderProps, Common::WeakPtr<GraphicModifier>());

		return kVThreadReturn;
	}
	if (_disableWhen.respondsTo(msg->getEvent())) {
		disable(runtime);
		return kVThreadReturn;
	}

	return kVThreadReturn;
}

void XorModModifier::disable(Runtime *runtime) {
	// This is a special-purpose modifier and is never disabled
}

Common::SharedPtr<Modifier> XorModModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new XorModModifier(*this));
}

const char *XorModModifier::getDefaultName() const {
	return "XorMod";
}

XorCheckModifier::XorCheckModifier() : _allClear(false) {
}

bool XorCheckModifier::load(const PlugInModifierLoaderContext &context, const Data::Obsidian::XorCheckModifier &data) {
	return true;
}

bool XorCheckModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
	if (attrib == "allclear") {
		result.setBool(_allClear);
		return true;
	}

	return Modifier::readAttribute(thread, result, attrib);
}

MiniscriptInstructionOutcome XorCheckModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
	if (attrib == "checknow") {
		DynamicValueWriteFuncHelper<XorCheckModifier, &XorCheckModifier::scriptSetCheckNow, true>::create(this, result);
		return kMiniscriptInstructionOutcomeContinue;
	}

	return Modifier::writeRefAttribute(thread, result, attrib);
}

Common::SharedPtr<Modifier> XorCheckModifier::shallowClone() const {
	return Common::SharedPtr<Modifier>(new XorCheckModifier(*this));
}

const char *XorCheckModifier::getDefaultName() const {
	return "XorCheck";
}

MiniscriptInstructionOutcome XorCheckModifier::scriptSetCheckNow(MiniscriptThread *thread, const DynamicValue &value) {
	if (value.getType() != DynamicValueTypes::kBoolean)
		return kMiniscriptInstructionOutcomeFailed;

	if (!value.getBool())
		return kMiniscriptInstructionOutcomeContinue;

	Structural *scene = findStructuralOwner();
	while (!scene->getParent()->isSubsection())
		scene = scene->getParent();

	Common::Array<VisualElement *> xorElements;
	recursiveFindXorElements(scene, xorElements);

	Common::Rect triRects[4];
	for (int i = 0; i < 4; i++)
		triRects[i] = Common::Rect(0, 0, 0, 0);

	Common::Array<Common::Rect> pendingRects;

	for (VisualElement *element : xorElements) {
		VisualElementRenderProperties::Shape shape = element->getRenderProperties().getShape();
		Common::Rect rect = element->getRelativeRect();
		Common::Point absOrigin = element->getCachedAbsoluteOrigin();
		Common::Rect absRect = rect;
		absRect.translate(absOrigin.x - rect.left, absOrigin.y - rect.top);

		if (shape >= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1 && shape <= VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri4)
			triRects[shape - VisualElementRenderProperties::kShapeObsidianCanvasPuzzleTri1] = absRect;
		else
			pendingRects.push_back(absRect);
	}

	// The canvas puzzle has 4 triangles, right-angled in each corner, pairs 1-4 and 2-3 form rects.
	// It isn't possible to solve the puzzle unless both rects are formed.  So, we do this by forming the rects and
	// then eliminating overlaps.  If the rects can't be formed, the puzzle fails.
	if (triRects[0] == triRects[3])
		pendingRects.push_back(triRects[0]);
	else {
		_allClear = false;
		return kMiniscriptInstructionOutcomeContinue;
	}
	if (triRects[1] == triRects[2])
		pendingRects.push_back(triRects[1]);
	else {
		_allClear = false;
		return kMiniscriptInstructionOutcomeContinue;
	}

	Common::Array<Common::Rect> maskedRects;
	while (pendingRects.size() > 0) {
		const Common::Rect pendingRect = pendingRects.back();
		pendingRects.pop_back();

		bool hasIntersection = false;
		size_t intersectionIndex = 0;
		for (size_t j = 0; j < maskedRects.size(); j++) {
			if (maskedRects[j].intersects(pendingRect)) {
				hasIntersection = true;
				intersectionIndex = j;
			}
		}

		if (!hasIntersection) {
			maskedRects.push_back(pendingRect);
			continue;
		}

		if (pendingRect == maskedRects[intersectionIndex]) {
			// Total overlap
			maskedRects.remove_at(intersectionIndex);
			continue;
		}

		const Common::Rect intersectingRect = maskedRects[intersectionIndex];

		// Try to subdivide the intersecting rect using one of the axes of the incoming rect.
		// If this succeeds, requeue the intersecting rect fragments and add the pending rect
		// to the workspace.  Since that amounts to replacement, just replace the rect.
		if (sliceRectX(intersectingRect, pendingRect.left, pendingRects)
			|| sliceRectX(intersectingRect, pendingRect.right, pendingRects)
			|| sliceRectY(intersectingRect, pendingRect.top, pendingRects)
			|| sliceRectY(intersectingRect, pendingRect.bottom, pendingRects)) {
			maskedRects[intersectionIndex] = pendingRect;
			continue;
		}

		// Try to subdivide the pending rect using one of the axes of the intersecting rect.
		// If this succeeds, the fragments will be requeued and no further action is needed.
		if (sliceRectX(pendingRect, intersectingRect.left, pendingRects)
			|| sliceRectX(pendingRect, intersectingRect.right, pendingRects)
			|| sliceRectY(pendingRect, intersectingRect.top, pendingRects)
			|| sliceRectY(pendingRect, intersectingRect.bottom, pendingRects)) {
			continue;
		}

		// This should never happen
		assert(false);
		return kMiniscriptInstructionOutcomeFailed;
	}

	_allClear = (maskedRects.size() == 0);

	return kMiniscriptInstructionOutcomeContinue;
}

void XorCheckModifier::recursiveFindXorElements(Structural *structural, Common::Array<VisualElement *> &elements) {
	for (const Common::SharedPtr<Structural> &child : structural->getChildren())
		recursiveFindXorElements(child.get(), elements);

	if (!structural->isElement())
		return;

	Element *element = static_cast<Element *>(structural);
	if (!element->isVisual())
		return;

	VisualElement *visual = static_cast<VisualElement *>(element);
	if (visual->getRenderProperties().getInkMode() == VisualElementRenderProperties::kInkModeXor)
		elements.push_back(visual);
}

bool XorCheckModifier::sliceRectX(const Common::Rect &rect, int32 x, Common::Array<Common::Rect> &outSlices) {
	if (x > rect.left && x < rect.right) {
		Common::Rect leftSlice = Common::Rect(rect.left, rect.top, x, rect.bottom);
		Common::Rect rightSlice = Common::Rect(x, rect.top, rect.right, rect.bottom);
		outSlices.push_back(leftSlice);
		outSlices.push_back(rightSlice);
		return true;
	}

	return false;
}

bool XorCheckModifier::sliceRectY(const Common::Rect &rect, int32 y, Common::Array<Common::Rect> &outSlices) {
	if (y > rect.top && y < rect.bottom) {
		Common::Rect topSlice = Common::Rect(rect.left, rect.top, rect.right, y);
		Common::Rect bottomSlice = Common::Rect(rect.left, y, rect.right, rect.bottom);
		outSlices.push_back(topSlice);
		outSlices.push_back(bottomSlice);
		return true;
	}

	return false;
}

ObsidianPlugIn::ObsidianPlugIn(const Common::SharedPtr<WordGameData> &wgData)
	: _movementModifierFactory(this), _rectShiftModifierFactory(this), _textWorkModifierFactory(this),
	  _dictionaryModifierFactory(this), _wordMixerModifierFactory(this), _xorModModifierFactory(this),
	  _xorCheckModifierFactory(this), _wgData(wgData) {
}

void ObsidianPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
	registrar->registerPlugInModifier("Movement", &_movementModifierFactory);
	registrar->registerPlugInModifier("rectshift", &_rectShiftModifierFactory);
	registrar->registerPlugInModifier("TextWork", &_textWorkModifierFactory);
	registrar->registerPlugInModifier("Dictionary", &_dictionaryModifierFactory);
	registrar->registerPlugInModifier("WordMixer", &_wordMixerModifierFactory);
	registrar->registerPlugInModifier("xorMod", &_xorModModifierFactory);
	registrar->registerPlugInModifier("xorCheck", &_xorCheckModifierFactory);
}

const Common::SharedPtr<WordGameData>& ObsidianPlugIn::getWordGameData() const {
	return _wgData;
}


WordGameData::WordBucket::WordBucket() : _spacing(0) {
}

WordGameData::SortedWord::SortedWord() : _chars(nullptr), _length(0) {
}

bool WordGameData::load(Common::SeekableReadStream *stream, const WordGameLoadBucket *buckets, uint numBuckets, uint alignment, bool backwards) {
	_buckets.resize(numBuckets);

	size_t totalWords = 0;
	for (size_t i = 0; i < numBuckets; i++) {
		const WordGameLoadBucket &inBucket = buckets[i];
		WordBucket &outBucket = _buckets[i];

		uint32 sizeBytes = inBucket.endAddress - inBucket.startAddress;
		uint wordLength = i;
		uint spacing = (wordLength + alignment) - (wordLength % alignment);

		outBucket._spacing = spacing;
		outBucket._chars.resize(sizeBytes);

		assert(sizeBytes % alignment == 0);

		if (sizeBytes > 0) {
			if (!stream->seek(inBucket.startAddress, SEEK_SET))
				return false;
			stream->read(&outBucket._chars[0], sizeBytes);
		}

		uint numWords = sizeBytes / spacing;
		outBucket._wordIndexes.resize(numWords);

		if (backwards) {
			for (size_t wordIndex = 0; wordIndex < numWords / 2; wordIndex++) {
				char *swapA = &outBucket._chars[wordIndex * spacing];
				char *swapB = &outBucket._chars[(numWords - 1 - wordIndex) * spacing];

				for (size_t chIndex = 0; chIndex < wordLength; chIndex++) {
					char temp = swapA[chIndex];
					swapA[chIndex] = swapB[chIndex];
					swapB[chIndex] = temp;
				}
			}
		}

		totalWords += numWords;
	}

	_sortedWords.resize(totalWords);

	Common::Array<size_t> currentWordIndexes;
	currentWordIndexes.resize(numBuckets);

	for (size_t i = 0; i < numBuckets; i++)
		currentWordIndexes[i] = 0;

	for (size_t wordIndex = 0; wordIndex < totalWords; wordIndex++) {
		size_t bestBucket = numBuckets;
		const char *bestChars = nullptr;
		for (size_t bucketIndex = 0; bucketIndex < numBuckets; bucketIndex++) {
			size_t wordOffset = currentWordIndexes[bucketIndex] * _buckets[bucketIndex]._spacing;
			if (wordOffset < _buckets[bucketIndex]._chars.size()) {
				const char *candidate = &_buckets[bucketIndex]._chars[wordOffset];
				bool isWorse = true;
				if (bestChars == nullptr)
					isWorse = false;
				else  {
					// The best bucket will always be shorter if it's set, so this is only better if it precedes it alphabetically
					for (size_t i = 0; i < bestBucket; i++) {
						if (candidate[i] > bestChars[i]) {
							break;
						} else if (candidate[i] < bestChars[i]) {
							isWorse = false;
							break;
						}
					}
				}

				if (!isWorse) {
					bestBucket = bucketIndex;
					bestChars = candidate;
				}
			}
		}

		assert(bestChars != nullptr);

		const size_t bucketWordIndex = currentWordIndexes[bestBucket];
		_buckets[bestBucket]._wordIndexes[bucketWordIndex] = wordIndex;
		currentWordIndexes[bestBucket]++;

		_sortedWords[wordIndex]._chars = bestChars;
		_sortedWords[wordIndex]._length = bestBucket;
	}

	return !stream->err();
}

const Common::Array<WordGameData::WordBucket> &WordGameData::getWordBuckets() const {
	return _buckets;
}

const Common::Array<WordGameData::SortedWord>& WordGameData::getSortedWords() const {
	return _sortedWords;
}

} // End of namespace ObsidianPlugIn

namespace PlugIns {

Common::SharedPtr<PlugIn> createObsidian(const Common::SharedPtr<Obsidian::WordGameData> &wgData) {
	return Common::SharedPtr<PlugIn>(new Obsidian::ObsidianPlugIn(wgData));
}

} // End of namespace PlugIns

} // End of namespace MTropolis
