﻿using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
using Chameleon;
using Chameleon.Calls;
using Chameleon.Services;
using Array = System.Array;
using Console = System.Console;
using IntPtr = System.IntPtr;
using Type = System.Type;
using System;

public class ChameleonBehaviour : MonoBehaviour
{
	public static ChameleonBehaviour Startup(System.Action<bool> afterStartup)
	{
		Type chameleonType = typeof(ChameleonBehaviour);
		ChameleonBehaviour chameleon = null;
		GameObject chameleonObj;
		for(;;)
		{
			chameleonObj = GameObject.Find(chameleonType.Name);
			if(chameleonObj == null)
				break;
			chameleon = (ChameleonBehaviour)chameleonObj.GetComponent(chameleonType);
			if(chameleon != null)
				break;
			DestroyImmediate(chameleonObj);
		}
		if(chameleon == null)
		{
			chameleonObj = new GameObject(chameleonType.Name, chameleonType);
			chameleonObj.hideFlags = HideFlags.DontSave;
			chameleon = (ChameleonBehaviour)chameleonObj.GetComponent(chameleonType);
		}
		chameleon._afterStartup += afterStartup;
		return chameleon;
	}
	public static void StartupOnly(System.Action<bool> afterStartup)
	{
		Startup(afterStartup)._noUpdate = true;
	}
	void OnDisable()
	{
		if(hasStarted)
		{
			Debug.Log("ChameleonBehaviour.OnDisable,cleanup");
			Request.ClearAll();
			Skin.FreeFinal();
		}
#if UNITY_EDITOR || UNITY_STANDALONE
#if UNITY_WEBGL
		_web = null;
#endif
		if(_webs != null)
		{
			foreach(WebViewController web in _webs.Values)
				DestroyImmediate(web.gameObject);
			_webs = null;
		}
	}
	static Dictionary<int, WebViewController> _webs;
	public static Dictionary<int, WebViewController> webs
	{
		get
		{
			if(_webs == null)
				_webs = new Dictionary<int, WebViewController>();
			return _webs;
		}
	}
#else
	}
#endif
	void OnApplicationQuit() { OnDisable(); }
	bool _noUpdate;
	void Update()
	{
		if(!hasStarted)
			return;
		else if(_afterStartup != null)
		{
			_afterStartup(!_startFailed);
			_afterStartup = null;
		}
		if(_noUpdate)
			return;
#if DEBUG
		for(string log = Skin.Dump(); !string.IsNullOrEmpty(log); log = Skin.Dump())
			Console.WriteLine(log);
#endif
#if UNITY_EDITOR && UNITY_WEBGL
		bool enableLog = _web._enableLog;
		_web._enableLog = false;
#endif
		Chameleon.Calls.Request.UpdateAll();
#if UNITY_EDITOR && UNITY_WEBGL
		_web._enableLog = enableLog;
#endif
	}
	public bool hasStarted { get { return !_startFailed && Skin._extern != null; } }
	System.Action<bool> _afterStartup;
	bool _startFailed;
	IEnumerator Start()
	{
#if UNITY_EDITOR && UNITY_WEBGL
		_web = WebViewController.Create(true, true, "file:///" + Application.dataPath + "/Plugins/Chameleon/chameleon.html");
#if DEBUG
		_web._enableLog = true;
#endif
		while(!_web.hasStarted)
			yield return null;
		while(!(bool)_web.RunScript("runtimeInitialized;", false))
			yield return null;
		Debug.Log("TestChameleon.Web[" + _web.id + "],runtimeInitialized=true");
		_web._onUnloadWeb += Skin.FreeFinal;
		Skin._extern = new UnitySkin();
		_startFailed = !CheckWebSanity();
		if(!_startFailed)
			UnityPlugin.RegisterWeb();
	}
	public static WebViewController _web;
	bool CheckWebSanity()
	{
		Message m = new Message();
		if(m.Native() == System.IntPtr.Zero)
			return false;
		m.SetInt32("SanityKey0", 12345);
		if(m.GetInt32("SanityKey0") != 12345)
			return false;
		m.SetString("SanityKey1", "SanityValue");
		if(m.GetString("SanityKey1") != "SanityValue")
			return false;
		if(m.Count != 2 || m.GetName(0) != "SanityKey0" || m.GetName(1) != "SanityKey1")
			return false;
		m.Dispose();
		if(m.Native() != System.IntPtr.Zero)
			return false;
		while(!string.IsNullOrEmpty(Skin.Dump()));
		Skin.Log("SanityLog");
		string log = Skin.Dump();
		if(log != "SanityLog")
			return false;
		return true;
	}
#else
		if(Skin._extern == null)
			Skin._extern = new UnitySkin();
#if !UNITY_EDITOR && UNITY_ANDROID
		using(Sysinfo sysinfo = new Sysinfo())
		using(AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
		using(AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
			sysinfo.Context = currentActivity.GetRawObject();
#endif
		UnityPlugin.RegisterWeb();
		yield break;
	}
#endif
}
#if UNITY_EDITOR && UNITY_WEBGL
namespace Chameleon
{
	partial class _Skin
	{
		static partial void RunScriptVoid(string script)
		{
			ChameleonBehaviour._web.RunScript(script, false);
		}
		static partial void RunScriptBool(string script, ref bool b)
		{
			b = (bool)ChameleonBehaviour._web.RunScript(script, false, JsType.Bool);
		}
		static partial void RunScriptInt(string script, ref int i)
		{
			i = (int)ChameleonBehaviour._web.RunScript(script, false, JsType.Int);
		}
		static partial void RunScriptIntArray(string script, ref int[] intArray)
		{
			intArray = (int[])ChameleonBehaviour._web.RunScript(script, false, JsType.IntArray);
		}
		static partial void RunScriptString(string script, ref string str)
		{
			str = (string)ChameleonBehaviour._web.RunScript(script, false, JsType.String);
		}
		static partial void RunScriptObject(string script, ref object obj)
		{
			obj = ChameleonBehaviour._web.RunScript(script, true);
		}
	}
}
#endif
class UnitySkin : _Skin
{
	bool _cacheDirOverriden;
	public override void CollectEnvironment(IntPtr record, Consts[] factors, string[] keys)
	{
#if UNITY_EDITOR && !UNITY_WEBGL
		for(int i = 0; i < factors.Length; ++i)
		{
			int f = CacheDir;
			if(factors[i]._id == f && !_cacheDirOverriden)
				_extern.SetString(record, USE_CONST, ref f, Application.temporaryCachePath);
		}
#endif
		base.CollectEnvironment(record, factors, keys);
	}
	public override void OverrideEnvironment(IntPtr record, Consts factor, string key)
	{
		_cacheDirOverriden |= factor._id == CacheDir._id;
		base.OverrideEnvironment(record, factor, key);
	}
}

class UnityPlugin
{
	static void Register(Proc1<Message>.Del handler)
	{
		System.Type type = handler.Target.GetType();
		Request.Register(type.Name, type, handler.Method.Name);
	}
	static void Register<_Ret>(Func1<_Ret, Message>.Del handler)
	{
		System.Type type = handler.Target.GetType();
		Request.Register(type.Name, type, handler.Method.Name);
	}

	public static void RegisterWeb()
	{
#if UNITY_EDITOR || UNITY_STANDALONE
		Register((new Show_WebRequest()).Handle);
		Register<int>((new Show_FinishRequest()).Handle);
#endif
	}
#if UNITY_EDITOR || UNITY_STANDALONE
	class Show_WebRequest : Request
	{
		public const string Prefix = "Chameleon.Show.Web";
		int? _webId;
		int _navigateId, _loadId;
		public void Handle(Message message)
		{
			WebViewController web;
			if(_webId == null)
			{
				string url = message.GetString(Show.WEB_URL);
				if(string.IsNullOrEmpty(url))
				{
					Debug.LogError("UnityPlugin.Show_WebRequest,no url for showing web!");
					return;
				}
				int flags = message.GetInt32(Show.VIEW_FLAGS);
				Debug.Log("Show_WebRequest.Handle,flags=" + flags);
				_webId = message.GetInt32(Show.VIEW_ID);
				if(!ChameleonBehaviour.webs.TryGetValue(_webId.Value, out web))
				{
					float[] metrics = (float[])message.GetValues(Show.VIEW_METRICS, Skin.SINGLE_TYPE);
					web = WebViewController.Create(false, flags, url, Prefix + _webId);
					web._rect = new Rect(metrics[0], metrics[1], metrics[2], metrics[3]);
					ChameleonBehaviour.webs.Add(_webId.Value, web);
				}
				else
				{
					Debug.Log("UnityPlugin.Show_WebRequest,load only");
					web._options = (web._options & ~Show.WEB_NO_COOKIES) | flags;
					web.url = url;
					Quit();
					return;
				}
				_navigateId = message.GetInt32(Show.WEB_NAVIGATE);
				_loadId = message.GetInt32(Show.WEB_LOAD);
			}
			else if(ChameleonBehaviour.webs.TryGetValue(_webId.Value, out web))
			{
				if(!string.IsNullOrEmpty(web._browsableUrl))
				{
					if(_navigateId != 0)
					{
						message.SetString(Show.WEB_URL, web._browsableUrl);
						web._browsableUrl = null; // otherwise, same event will be fired again
						//Debug.Log("UnityPlugin.Show_WebRequest," + message.GetString(1));
						Request.AsyncCall(_navigateId, message, null, Browse);
					}
					else if(web._browsableUrl.StartsWith("http://") || web._browsableUrl.StartsWith("https://"))
						web.url = web._browsableUrl; // will clear _browsableUrl
				}
				if(web.justLoaded && _loadId != 0)
				{
					web.justLoaded = false;
					message.SetString(Show.WEB_SOURCE, web.source);
					Request.AsyncCall(_loadId, message, null, Load);
				}
			}
			else
			{
				Debug.Log("UnityPlugin.Show_WebRequest,quit");
				Quit();
				return;
			}
			Skip();
		}
		void Browse(Message response)
		{
			string url = response.GetString(Show.WEB_URL);
			if(!string.IsNullOrEmpty(url))
				ChameleonBehaviour.webs[_webId.Value].url = url;
		}
		void Load(Message response)
		{
		}
	}

	class Show_FinishRequest : Request
	{
		public int Handle(Message message)
		{
			int id = message.GetInt32(0);
			WebViewController web;
			if(!ChameleonBehaviour.webs.TryGetValue(id, out web))
			{
				Debug.LogError("UnityPlugin.Show_finishRequest.Handle,unknown," + id);
				return -1;
			}
			ChameleonBehaviour.webs.Remove(id);
			GameObject.DestroyImmediate(web.gameObject);
			Debug.Log("UnityPlugin.Show_FinishRequest.Handle," + id);
			return 0;
		}
	}
#endif
}
#if UNITY_WEBPLAYER || CHAMELEON_MONOLITHIC
public class UnityFactory : IFactory
{
	IFactory _oldInstance;
	public static IFactory _instance;
	public UnityFactory(IFactory oldInstance) { _oldInstance = oldInstance; _instance = this; }
	public IEvent MakeEvent(int slot) { return _oldInstance.MakeEvent(slot); }
	public ISocket MakeSocket(int[] classes, int index)
	{
		switch(classes[index])
		{
		case Skin.TCP_SOCKET:
#if UNITY_WEBPLAYER
			return new PolicySocket();
#elif !UNITY_EDITOR //UNITY_WEBGL
			return null; // no tcp sockets supported on WebGL, instead use WebSocket
#else
			break;
#endif
		case Skin.HTTP_SOCKET: return new WwwSocket();
#if !UNITY_EDITOR && CHAMELEON_MONOLITHIC
		case Skin.WEB_SOCKET: return new JsSocket();
#endif
		}
		return _oldInstance.MakeSocket(classes, index);
	}
	public ITranslator MakeTranslator(int translatorClass) { return _oldInstance.MakeTranslator(translatorClass); }
}

abstract class _Socket : ISocket
{
	IEvent _event;
	protected _Socket() { _event = UnityFactory._instance.MakeEvent(2); }
	public abstract void Dispose();

	public ITimeStamp TimeStamp(int slot) { return _event.TimeStamp(slot); }
	public int Reserve(int slot) { return _event.Reserve(slot); }
	public void Set(int slot, Func1<int, IEvent>.Del handler) { _event.Set(slot, handler); }
	public void Reset(int slot) { _event.Reset(slot); }
	public int Fire(int slot) { return _event.Fire(slot); }

	public abstract string Connect(IRecord options, ref int error);
	public abstract bool Reconnect(ref int error);
	public abstract void Close();
	public abstract bool Send(byte[] data, ref int error);
	public abstract byte[] GetBuffer(ref int index, ref int count, ref int error);
	public abstract bool Receive(byte[] data, int index, int count, ref int error);

	public bool Lock() { return _event.Reserve(0) >= 0; }
	public void Unlock() { _event.Reset(0); }
	public void WatchInput(Func1<int, IEvent>.Del watcher) { _event.Set(0, watcher); }
	public void KeepAlive(Func1<int, IEvent>.Del pacer) { _event.Set(1, pacer); }

	public abstract bool SendRecord(IRecord record, ref int error);
	public abstract IRecord ReceiveRecord(string id, ref int error);
	public abstract ISocket GetBase(int cls);
}

class WwwSocket : _Socket
{
	WWW _www;
	public WwwSocket() {}
	public override void Dispose() { Close(); }

	string _url;
	IRecord _options;
	public override string Connect(IRecord options, ref int error)
	{
		if(options == null)
			return Connect(_options, ref error);
		_url = options.GetString(0);
		if(!_url.StartsWith("http://") && !_url.StartsWith("https://"))
			_url += !options.GetBool(1) ? "http://" : "https://";
		_options = options;
		error = 0;
		return _url;
	}
	public override bool Reconnect(ref int error) { error = 0; return true; }
	public override void Close() { if(_www != null && _www.isDone) _www.Dispose(); _www = null; }
	public override bool Send(byte[] data, ref int error)
	{
		if(_www != null && _www.isDone)
			_www.Dispose();
		_www = data != null ? new WWW(_url, data) : new WWW(_url);
		error = 0;
		return true;
	}
	public override byte[] GetBuffer(ref int index, ref int count, ref int error)
	{
		if(_www == null)
		{
			error = 0;
			return null;
		}
		error = 0;
		if(!_www.isDone)
			return null;
		ULog.Write("WwwSocket.GetBuffer," + _www.text + ",error=" + _www.error);
		byte[] result = _www.bytes;
		index = 0;
		count = result.Length;
		return result;
	}
	public override bool Receive(byte[] buffer, int index, int count, ref int error)
	{
		if(_www == null || !_www.isDone)
		{
			error = -1;
			return false;
		}
		if(_www.bytesDownloaded < count)
		{
			error = -1;
			return false;
		}
		error = 0;
		if(buffer != null)
			Array.Copy(_www.bytes, 0, buffer, index, count);
		return true;
	}

	public override bool SendRecord(IRecord record, ref int error)
	{
		if(_www != null && _www.isDone)
			_www.Dispose();
		string url = _url + record.GetString(1);
		_www = record != null && record.IndexOfName(Skin.HttpBody.ToString()) >= 0 ? new WWW(url) : new WWW(url, (byte[])record.GetPointer(Skin.HttpBody.ToString()));
		error = 0;
		return true;
	}
	public override IRecord ReceiveRecord(string id, ref int error)
	{
		if(_www == null || !_www.isDone)
			return null;
		string status = _www.responseHeaders["STATUS"];
		IRecord result = UnityFactory._instance.MakeTranslator(Skin.HTTP_TRANSLATOR).ToRecord(status, 0, status.Length);
		if(!string.IsNullOrEmpty(id))
			result.SetId(id);
		foreach(KeyValuePair<string, string> h in _www.responseHeaders)
			result.SetString(h.Key, h.Value);
		byte[] bytes = _www.bytes;
		if(bytes != null)
			result.SetBytes(Skin.HttpBody.ToString(), bytes);
		if(!string.IsNullOrEmpty(_www.error))
			error = -1;
		return result;
	}

	public override ISocket GetBase(int cls) { return cls == Skin.HTTP_SOCKET ? this : null; }
}
#if UNITY_WEBPLAYER
class PolicySocket : Socket
{
	string _ip;
	public override string Connect(StreamRecord options, ref int error)
	{
		string url = options.GetString(0);
		if(url != null)
		{
			int i = url.IndexOf("//"), j;
			if(i < 0)
			{
				i = 0;
				j = url.IndexOfAny(new char[] { ':', '/' });
			}
			else
			{
				i += 2;
				j = url.IndexOfAny(new char[] { ':', '/' }, i);
			}
			if(j < 0)
				_ip = url.Substring(i);
			else
				_ip = url.Substring(i, j - i);
			if(!Array.TrueForAll(_ip.ToCharArray(), c => char.IsDigit(c) || c == '.'))
				_ip = System.Net.Dns.GetHostEntry(_ip).AddressList[0].ToString();
		}
		if(!Security.PrefetchSocketPolicy(_ip, 10843))
		{
			error = -1;
			return null;
		}
		return base.Connect(options, ref error);
	}
}
#elif !UNITY_EDITOR //UNITY_WEBGL
class JsSocket : _Socket
{
	int _socket;
	public JsSocket() { _socket = -1; }
	public override void Dispose() { Close(); }

	StreamRecord _options;
	public override string Connect(StreamRecord options, ref int error)
	{
		if(options == null && _options != null)
			return Connect(_options, ref error);
		if(_socket >= 0)
			return null;
		string url = options.GetString(0);
		_socket = CreateSocket(url);
		_options = options;
		error = Skin.ERROR_NONE;
		return url;
	}
	public override bool Reconnect(ref int error) { error = 0; return true; }
	public override void Close() { if(_socket >= 0) DeleteSocket(_socket); _socket = -1; }
	public override bool Send(byte[] data, ref int error)
	{
		error = SendSocket(_socket, data, data.Length);
		return true;
	}
	byte[] _buffer = new byte[65536];
	int _received;
	public override byte[] GetBuffer(ref int index, ref int count, ref int error)
	{
		int received = ReceiveSocket(_socket, _buffer, _received, _buffer.Length - _received);
		if(received < 0)
		{
			error = received;
			return null;
		}
		error = Skin.ERROR_NONE;
		if(received > 0)
		{
			_received += received;
			index = 0;
			count = _received;
			return _buffer;
		}
		count = 0;
		return null;
	}
	public override bool Receive(byte[] buffer, int index, int count, ref int error)
	{
		int i = 0, c = 0;
		GetBuffer(ref i, ref c, ref error);
		if(count > _received)
			return false;
		if(buffer != null)
			Array.Copy(_buffer, 0, buffer, index, count);
		_received -= count;
		Array.Copy(_buffer, count, _buffer, 0, _received);
		error = 0;
		return true;
	}

	public override bool SendRecord(StreamRecord record, ref int error) { return false; }
	public override StreamRecord ReceiveRecord(string id, ref int error) { return null; }

	public override ISocket GetBase(int cls) { return cls == Skin.WEB_SOCKET ? this : null; }

	[DllImport("__Internal")]
	static extern int CreateSocket(string url);
	[DllImport("__Internal")]
	static extern void DeleteSocket(int socket);
	[DllImport("__Internal")]
	static extern int SendSocket(int socket, byte[] data, int size);
	[DllImport("__Internal")]
	static extern int ReceiveSocket(int socket, byte[] buffer, int index, int count);
}
#endif
#endif