//#pragma once
#ifndef AnimationrigSpine_H
#define AnimationrigSpine_H

#include <CommonMemoryStream.h>	// unicode
#include <AnimationrigMisc.h>
#include <Animationrig.h>
#include <AnimationrigState.h>

#include <spine/Animation.h>
#include <spine/AnimationState.h>
#include <spine/AnimationStateData.h>
#include <spine/Skeleton.h>
#include <spine/SkeletonData.h>
#include <spine/Bone.h>
#include <spine/BoneData.h>

#include <math.h>

using namespace std;

namespace Animrig {
	class SpineRenderer : public StateRenderer {
	protected:
		std::map<wstring, spine::Bone*> boneCacheList;
		Matrix4 extractWorldMatrix(spine::Bone *bone)
		{
			Matrix4 r = Misc::IdentityMatrix4;
			r._11 = bone->getA();
			r._12 = bone->getC();
			r._21 = bone->getB();
			r._22 = bone->getD();
			r._41 = bone->getWorldX() * antiScaling;
			r._42 = bone->getWorldY() * antiScaling;
			return r;
		}
	public :
		// spine string convert
		static wstring convertSpineToWString(const spine::String &s) {
			string utf8;
			utf8.resize(s.length());
			memcpy(&utf8[0], s.buffer(), s.length());

			return UnicodeConverter::utf8to16(utf8);
		}
		static spine::String *convertWStringToSpine(const wstring &s) {
			auto utf8 = UnicodeConverter::utf16to8(s);
			auto ss = new spine::String(utf8.c_str());	// hard copy

			return ss;
		}
		// property
		const int SaveVersion = 1;
		spine::AnimationState *state;
		spine::Skeleton *skeleton;
		float antiScaling = 1.0f;
		// public
		SpineRenderer()
			: StateRenderer()
		{
			fObjectType = ObjectType::Spine;

			state = nullptr;
			skeleton = nullptr;

			clear();
		}
		// common 
		void clear() override
		{
			StateRenderer::clear();

			boneCacheList.clear();
			state = nullptr;
			skeleton = nullptr;
		}
		void update(double deltatime) override
		{
			if (state == nullptr) return;
			if (skeleton == nullptr) return;

			state->update((float)deltatime);
			state->apply(*skeleton);
			skeleton->updateWorldTransform();

			// physics
			addPhysicsTransform(Misc::IdentityMatrix4);
			updatePhysics(deltatime);
			applyPhysics();
		}
		// spine
		void setAnimation(int trackindex, int usetrack, const wstring &basename, bool loop, bool addanim, MixBlend trackmix, float duration, bool seekmode) override
		{
			struct LocalFunction {
				static void inSetEmpty(spine::AnimationState *state, int index, float dt, bool addanim)
				{
					if (dt < 0.0) dt = state->getData()->getDefaultMix();

					if (addanim)
						state->addEmptyAnimation(index, dt, 0.0f);
					else
						state->setEmptyAnimation(index, dt);
				}
				static void inSetAnimation(spine::AnimationState *state, int index, const wstring &name1, const wstring &name2, bool loop, bool addanim, MixBlend trackmix, float dt, bool seekmode)
				{
					// search
					spine::Animation *anim;
					{
						spine::String ss1(UnicodeConverter::utf16to8(name1).c_str());
						anim = state->getData()->getSkeletonData()->findAnimation(ss1);
						if (anim == nullptr) {
							spine::String ss2(UnicodeConverter::utf16to8(name2).c_str());
							anim = state->getData()->getSkeletonData()->findAnimation(ss2);
						}
					}
					if (anim == nullptr)
					{
						LocalFunction::inSetEmpty(state, index, dt, addanim);
					}
					else
					{
						// set animation
						spine::TrackEntry *track;
						if (addanim)
							track = state->addAnimation(index, anim, loop, 0.0f);
						else
							track = state->setAnimation(index, anim, loop);
						if (track != nullptr)
						{
							// mix blend
							if ((track->getMixBlend() == spine::MixBlend_Replace) || (track->getMixBlend() == spine::MixBlend_Add))     // safe code
							{
								switch (trackmix)
								{
								case MixBlend::Add:
									track->setMixBlend(spine::MixBlend_Add);
									break;
								case MixBlend::Replace:
									track->setMixBlend(spine::MixBlend_Replace);
									break;
								}
							}
							// duration
							if (dt < 0.0f)
							{
								// use default duration
							}
							else
							{
								track->setMixDuration(max(dt, 0.0f));
							}
							// seek mode
							if (seekmode)
								track->setTimeScale(0.0f);
							else
								track->setTimeScale(1.0f);
						}
					}
				}
			};

			if (state == nullptr) return;

			// layertype AnimBlend1
			if (usetrack == 1)
			{
				if (basename == L"empty")
				{
					LocalFunction::inSetEmpty(state, trackindex + 0, duration, addanim);
				}
				else
				{
					LocalFunction::inSetAnimation(state, trackindex + 0, basename, basename + L"@up", loop, addanim, trackmix, duration, seekmode);
				}
				return;
			}

			// layertype AnimBlend4
			if (usetrack == 4)
			{
				if (basename == L"empty")
				{
					LocalFunction::inSetEmpty(state,trackindex + 0, duration, addanim);
					LocalFunction::inSetEmpty(state,trackindex + 1, duration, addanim);
					LocalFunction::inSetEmpty(state,trackindex + 2, duration, addanim);
					LocalFunction::inSetEmpty(state,trackindex + 3, duration, addanim);
				}
				else
				{
					wstring s;
					LocalFunction::inSetAnimation(state, trackindex + 0, basename, basename + L"@up", loop, addanim, trackmix, duration, seekmode);
					LocalFunction::inSetAnimation(state, trackindex + 0, basename, basename + L"@right", loop, addanim, trackmix, duration, seekmode);
					LocalFunction::inSetAnimation(state, trackindex + 0, basename, basename + L"@down", loop, addanim, trackmix, duration, seekmode);
					LocalFunction::inSetAnimation(state, trackindex + 0, basename, basename + L"@left", loop, addanim, trackmix, duration, seekmode);
				}
			}
		}
		void setTrackBlendAlpha(int trackindex, int usetrack, float alpha1, float alpha2, float alpha3, float alpha4, bool exists) override
		{
			struct LocalFunction {
				static void inSet(spine::AnimationState *state, int index, float alpha) {
					auto track = state->getCurrent(index);
					if (track == nullptr) return;

					track->setAlpha(alpha);
				}
			};
			
			if (state == nullptr) return;

			if (usetrack == 1)
			{
				LocalFunction::inSet(state, trackindex + 0, alpha1);
			}
			else
			{
				LocalFunction::inSet(state, trackindex + 0, alpha1);
				LocalFunction::inSet(state, trackindex + 1, alpha2);
				LocalFunction::inSet(state, trackindex + 2, alpha3);
				LocalFunction::inSet(state, trackindex + 3, alpha4);
			}
		}
		void setTrackSeekTime(int trackindex, int usetrack, float time1, float time2, float time3, float time4, bool exists) override
		{
			struct LocalFunction {
				static void inSet(spine::AnimationState *state, int index, float time) {
					auto track = state->getCurrent(index);
					if (track == nullptr) return;

					track->setTrackTime(time);
				}
			};

			if (state == nullptr) return;

			if (usetrack == 1)
			{
				LocalFunction::inSet(state, trackindex + 0, time1);
			}
			else
			{
				LocalFunction::inSet(state, trackindex + 0, time1);
				LocalFunction::inSet(state, trackindex + 1, time2);
				LocalFunction::inSet(state, trackindex + 2, time3);
				LocalFunction::inSet(state, trackindex + 3, time4);
			}
		}
		void setMixDuration(float duration) override
		{
			if (state == nullptr) return;

			state->getData()->setDefaultMix(duration);
		}
		void setSkin(const wstring &skinname) override {
			if (skeleton == nullptr) return;

			auto ss = convertWStringToSpine(skinname);
			auto skin = skeleton->getData()->findSkin(*ss);
			if (skin != nullptr)
			{
				skeleton->setSkin(skin);
				skeleton->setToSetupPose();
			}
			delete ss;
		}
		Matrix4 getBoneMatrix(const wstring &bonename) override
		{
			if (skeleton == nullptr) return Misc::IdentityMatrix4;
			if (state == nullptr) return Misc::IdentityMatrix4;

			// cache
			spine::Bone *bone = nullptr;
			bool cachehit = false;
			auto itr = boneCacheList.find(bonename);
			if (itr != end(boneCacheList)) {
				bone = itr->second;
				cachehit = true;
			}
			if (bone == nullptr) {
				auto ss = convertWStringToSpine(bonename);
				bone = skeleton->findBone(*ss);
				delete ss;
			}

			if (!cachehit) {
				boneCacheList[bonename] = bone;
			}

			if (bone == nullptr) return Misc::IdentityMatrix4;
			return extractWorldMatrix(bone);
		}
	};

	class SpinePhysicsRenderer : public SpineRenderer
	{
	protected:
		PhysicsMatrix4 extractWorldMatrix(spine::Bone *bone)
		{
			Matrix4 m = Misc::IdentityMatrix4;

			m._11 = bone->getA();
			m._12 = bone->getC();
			m._21 = bone->getB();
			m._22 = bone->getD();
			m._41 = bone->getWorldX();
			m._42 = bone->getWorldY();

			return m;
		}
		// get setting value
		double getSettingFloat(const map<wstring, wstring> &dic, int index, const wstring &key, double def_, double min_, double max_)
		{

			wstring s = L"";
			auto itr = dic.find(key);
			if (itr != end(dic)) {
				s = itr->second;
			}

			auto sl = Misc::divideString(',', s);
			if (index < (int)sl->size()) s = sl->at(index);
			delete sl;

			auto f = Misc::toFloatDef(s, (float)def_);
			return fmin(fmax(f, min_), max_);
		}
		wstring getSettingString(const map<wstring, wstring> &dic, const wstring &key, const wstring def_)
		{
			wstring s = L"";
			auto itr = dic.find(key);
			if (itr != end(dic)) {
				s = itr->second;
			}
			if (s == L"") s = def_;
			return s;
		}
		// bone
		void addBone(PhysicsGroupSpringBone *g, spine::Bone *bone, PhysicsNodeBone *parent)
		{
			auto &data = bone->getData();
			if (parent != nullptr)
			{
				auto &spinestr = data.getName();
				auto s = convertSpineToWString(spinestr);

				if (s.find(L"@end") > -1) return;  // terminate code
				if (s.find(L"@") > -1) return;     // when other code exists, safe exit.
			}

			PhysicsNodeBone *b = new PhysicsNodeBone(parent);
			b->userObject = bone;
			b->localAngle = data.getRotation() / 360.0;
			b->localLength = data.getLength() * antiScaling;
			b->localPosition.x = data.getX() * antiScaling;
			b->localPosition.y = data.getY() * antiScaling;
			g->addBone(b);

			auto &children = bone->getChildren();
			for (auto i = 0; i < (int)children.size(); i++) {
				addBone(g, children[i], b);
			}

			// create terminate bone
			if (children.size() == 0)
			{
				parent = b;
				b = new PhysicsNodeBone(parent);
				b->userObject = nullptr;
				b->localAngle = 0;
				b->localLength = 100;
				b->localPosition.x = parent->localLength;
				b->localPosition.y = 0;
				g->addBone(b);
			}
		}
		// spring
		void addSpring(PhysicsGroupSpringBone *g, PhysicsNodeBone *bone)
		{
			for(auto b : bone->children)
			{
				auto s = new PhysicsSpringBone(g, bone, b);
				g->addSpring(s);
				addSpring(g, b);
			}
		}
		// spring group
		void addSpringGroup(const map<wstring, wstring> &dic, wstring name, wstring alias, spine::Bone *bone)
		{
			// need has parent
			auto root = bone->getParent();
			if (root == nullptr) return;

			if (alias == L"") alias = name;

			// enable / disable
			if (getSettingString(dic, alias + L".enable", L"true") == L"false") return;

			auto g = new PhysicsGroupSpringBone();
			g->name = name;
			g->aliasName = alias;
			g->rootUserObject = root;

			auto s = getSettingString(dic, alias + L".mode", L"bone");
			if (s == L"strpoint") g->mode = PhysicsGroupSpringBoneMode::StrPoint;
			if (s == L"point") g->mode = PhysicsGroupSpringBoneMode::Point;
			if (s == L"strbone") g->mode = PhysicsGroupSpringBoneMode::StrBone;
			if (s == L"bone") g->mode = PhysicsGroupSpringBoneMode::Bone;

			g->K = getSettingFloat(dic, 0, alias + L".k", g->K, 0.1, 100 * 100);
			g->damping = getSettingFloat(dic, 0, alias + L".damping", g->damping, 0.1, 100 * 100);
			g->mass = getSettingFloat(dic, 0, alias + L".mass", g->mass, 0.001, 1000);
			g->limitVelocity = getSettingFloat(dic, 0, alias + L".velocitylimit", g->limitVelocity, 1, 0xffff);   // 100 = physics frame
			g->gravity = getSettingFloat(dic, 0, alias + L".gravity", g->gravity, 0, 10000 * 100);
			g->gravityVector.x = getSettingFloat(dic, 0, alias + L".gravityvector", g->gravityVector.x, -1000, +1000);
			g->gravityVector.y = getSettingFloat(dic, 1, alias + L".gravityvector", g->gravityVector.y, -1000, +1000);
			g->gravityVector.z = getSettingFloat(dic, 2, alias + L".gravityvector", g->gravityVector.z, -1000, +1000);
			g->gravityVector.normalize();
			g->limitAngleMin = getSettingFloat(dic, 0, alias + L".limitangle", g->limitAngleMin, -0xff, 0);
			g->limitAngleMax = getSettingFloat(dic, 1, alias + L".limitangle", g->limitAngleMax, 0, +0xff);
			g->limitMoveRatio = getSettingFloat(dic, 0, alias + L".limitmove", g->limitMoveRatio, 0, +0xff);
			// private
			g->forceRotateRatio = getSettingFloat(dic, 0, alias + L".forcerotate", g->forceRotateRatio, 0.0, 10.0);
			g->forceRatio = getSettingFloat(dic, 0, alias + L".force", g->forceRatio, 0.0, 10.0);

			addBone(g, bone, nullptr);
			addSpring(g, g->getRootBone());

			g->rootMatrix = extractWorldMatrix(root);
			g->setupNatural();

			//auto o = new unique_ptr(g);
			physicsGroups.emplace_back(g);
		}
		// apply bones
		void updateWorldMatrix(spine::Bone *bone)
		{
			if (bone == nullptr) return;

			bone->updateWorldTransform();

			auto &children = bone->getChildren();
			for (auto i = 0; i < (int)children.size(); i++) {
				updateWorldMatrix(children[i]);
			}
		}
		void applyBone(PhysicsGroupSpringBone *g, spine::Skeleton *skeleton)
		{
			for(auto &n : g->nodes)
			{
				auto b = static_cast<PhysicsNodeBone*>(n.get());
				if (b->userObject != nullptr)
				{
					auto sb = reinterpret_cast<spine::Bone*>(b->userObject);
					switch (g->mode)
					{
					case PhysicsGroupSpringBoneMode::Bone:
						sb->setRotation((float)(b->aLocalAngle * 360.0));
						break;
					case PhysicsGroupSpringBoneMode::StrBone:
						sb->setRotation((float)(b->aLocalAngle * 360.0));
						sb->setX((float)b->aLocalPosition.x / antiScaling);
						sb->setY((float)b->aLocalPosition.y / antiScaling);
						break;
					case PhysicsGroupSpringBoneMode::Point:
					case PhysicsGroupSpringBoneMode::StrPoint:
						sb->setX((float)b->aLocalPosition.x / antiScaling);
						sb->setY((float)b->aLocalPosition.y / antiScaling);
						break;
					}
				}
			}
			//updateWorldMatrix(reinterpret_cast<spine::Bone*>(g->getRootBone()->userObject));
		}
	public:
		void addPhysicsTransform(const Matrix4 &externalmatrix)
		{
			for(auto &g : physicsGroups)
			{
				if (g->groupType == PhysicsGroupType::Spring)
				{
					auto gs = static_cast<PhysicsGroupSpringBone*>(g.get());
					auto m = extractWorldMatrix(reinterpret_cast<spine::Bone*>(gs->rootUserObject));
					Misc::m4PushMatrix(m,transMatrix);
					gs->rootMatrix = m;
					gs->externalMatrix = externalmatrix;
				}
			}
		}
		void addPhysicsForce(const wstring &target, double x, double y, double z) override
		{
			// safe check
			if (state == nullptr) return;
			if (skeleton == nullptr) return;

			if (target == L"none") return;

			PhysicsVector3 v;
			v.setVector(x, y, z);
			for(auto &g : physicsGroups)
			{
				if ((g->aliasName == target) || (target == L"all")) g->addForce(v);
			}
		}
		void setupPhysics(const vector<wstring> &setting) override
		{
			// do not forget to clear PhysicsGroups
			SpineRenderer::setupPhysics(setting);

			if (state == nullptr) return;
			if (skeleton == nullptr) return;

			map<wstring, wstring> settingdic;
			Misc::convertKeyValue(settingdic, setting);

			skeleton->updateWorldTransform();

			auto &bones = skeleton->getBones();
			for (auto i = 0; i < (int)bones.size(); i++) {
				auto b = bones[i];
				auto &spinestr = b->getData().getName();
				auto s = convertSpineToWString(spinestr);

				auto idx = s.find(L"@");
				if (idx == -1) continue;
				if (idx >= s.length()-1) continue;

				// name @ spring,aliasname
				wstring op = L"";
				wstring alias = L"";
				wstring name = s.substr(0, idx);
				auto sl = Misc::divideString(',', s.substr(idx + 1));

				if (sl->size() > 0) op = sl->at(0);
				if (sl->size() > 1) alias = sl->at(1);

				if (op == L"spring") addSpringGroup(settingdic, name, alias, b);
				delete sl;
			}
		}
		void applyPhysics() override
		{
			// safe check
			if (state == nullptr) return;
			if (skeleton == nullptr) return;

			for(auto &g : physicsGroups)
			{
				g->apply();
				if (g->groupType == PhysicsGroupType::Spring)
				{
					applyBone(static_cast<PhysicsGroupSpringBone*>(g.get()), skeleton);
				}

				skeleton->updateWorldTransform();
			}
		}
		void appeasePhysics() override
		{
			if (state == nullptr) return;
			if (skeleton == nullptr) return;

			for(auto &g : physicsGroups)
			{
				g->appease();
			}
		}
	};
}

#endif

