using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Drawing;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Reflection;
using System.Concurrency;

namespace Aqua877.WinApp.IronLivetube
{
	public class LivetubeCommentData
	{
		public Image UserThumbnail { get; set; }
		public int Number { get; set; }
		public bool ContributorCanBroadcasting { get; set; }
		public string Name { get; set; }
		public string ID { get; set; }
		public string Text { get; set; }
		public DateTime PostedDate { get; set; }
		public string Host { get; set; }
		public bool IsBanned { get; set; }

		public override bool Equals(object obj)
		{
			if (!(obj is LivetubeCommentData))
			{
				return false;
			}
			var compare = (obj as LivetubeCommentData);
			return ((compare.Number == this.Number) && (compare.Name == this.Name) && (compare.Text == this.Text));
		}

		public override int GetHashCode()
		{
			return base.GetHashCode();
		}
	}

	public class LivetubeCommentReader
	{
		public LivetubeInformation LiveInfo;
		public List<LivetubeCommentData> CommentContainer { get; set; }
		public bool IsReading = false;
		public bool IsTracingIp = false;
		public event Action<bool> OnLoaded;
		public event Action<IEnumerable<LivetubeCommentData>> OnCaughtComment;
		public event Action<bool, Exception> OnPostCommentFinished;
		public event Action<int, bool, Exception> OnBanCommentFinished;
		public event Action<int, bool, Exception> OnUnBanCommentFinished;
		public event Action<string> OnStatusTextChanged;
		public event Action<Exception, bool> OnExceptionThrown;
		public event Action<bool, Exception> OnLoginFinished;
		public event Action<IEnumerable<int>, IEnumerable<int>> OnBannedCommentsListRefreshed;
		public event Action<int, string> OnGetHostFinished;

		private string StreamId;
		private WebClient CommentReader;

		public void LoadAsync(Uri url)
		{
			this.NotifyStatusTextChanged("StreamID取得開始");

			using (var streamIdGetter = new WebClient { Encoding = Encoding.UTF8 })
			{
				streamIdGetter.DownloadStringCompleted += this.OnGetStreamIdFinished;
				streamIdGetter.DownloadStringAsync(url);
			}
		}

		private void OnGetStreamIdFinished(object arg, DownloadStringCompletedEventArgs e)
		{
			if (e == null) throw new ArgumentNullException("e");
			if (e.Error != null)
			{
				this.NotifyLoaded(false);
				this.NotifyStatusTextChanged("StreamID取得失敗");
				this.NotifyExceptionThrown(e.Error, true);

				return;
			}

			this.NotifyStatusTextChanged("StreamID取得成功");

			string data = e.Result;
			string streamID = Regex.Match(data, GlobalValues.GetStreamIDPattern).Groups[1].Value;

			if (streamID == "")
			{
				this.NotifyLoaded(false);
				return;
			}

			this.NotifyStatusTextChanged("コメント取得準備中...");
			this.LoadAsync(streamID);
		}

		public void LoadAsync(string streamId)
		{
			this.IsReading = true;
			this.StreamId = streamId;
			this.CommentContainer = new List<LivetubeCommentData>();
			this.CommentReader = new WebClient { Encoding = Encoding.UTF8 };
			
			this.CommentReader.DownloadStringCompleted += this.ParseComment;
			this.CommentReader.DownloadProgressChanged += this.CommentReaderProgressChanged;

			string url = String.Format("http://livetube.cc/stream/{0}.comments", streamId);
			if (GlobalValues.Setting.IsSelectServer)
			{
				switch (GlobalValues.Setting.SelectedServer)
				{
					case LivetubeServers.Default: url.Insert(0, ""); break;
					case LivetubeServers.H: url.Insert(0, "h."); break;
					case LivetubeServers.Large03: url.Insert(0, "large03."); break;
					case LivetubeServers.Large04: url.Insert(0, "large04."); break;
					case LivetubeServers.Large05: url.Insert(0, "large05."); break;
					case LivetubeServers.Tes01: url.Insert(0, "tes01."); break;
					default: break;
				}
			}
			else
			{
				//自動的なサーバー選択
			}

			this.CommentReader.DownloadStringAsync(new Uri(url));
		}

		private void CommentReaderProgressChanged(object sender, DownloadProgressChangedEventArgs e)
		{
			this.NotifyStatusTextChanged(String.Format("取得中...({0}%:{1}KB/{2}KB)", e.ProgressPercentage, e.BytesReceived, (e.TotalBytesToReceive == -1) ? "?" : e.TotalBytesToReceive.ToString()));
		}

		private void ParseComment(object sender, DownloadStringCompletedEventArgs e)
		{
			if (e.Error != null)
			{
				//エラー処理
				if (e.Error is WebException)
				{
					if ((e.Error as WebException).Status == WebExceptionStatus.ProtocolError)
					{
						var response = (e.Error as WebException).Response as HttpWebResponse;

						switch (response.StatusCode)
						{
							//続行不可能なエラーの場合はエラーを通知してコメントのロードを中止
							case HttpStatusCode.NotFound:
							case HttpStatusCode.Forbidden:
							{
								if (this.CommentContainer.Count == 0)
								{
									this.NotifyLoaded(false);
								}
								this.NotifyExceptionThrown(e.Error, true);
								this.StopLoad();
								return;
							}

							default:
							{
								this.NotifyExceptionThrown(e.Error, false);
								break;
							}
						}
					}
				}
				else
				{
					this.NotifyExceptionThrown(e.Error, false);
				}
			}
			else
			{
				if (this.CommentContainer.Count == 0)
				{
					this.NotifyLoaded(true);
				}

				//コメントの追加処理
				var extractCommentPattern = new Regex(GlobalValues.ExtractCommentDataPattern);
				var extractedComments = extractCommentPattern.Matches(e.Result);

				//正規表現で抜き出したコメントをもとにデータを生成
				var additionalComments = extractedComments.Cast<Match>().Select(
					extractedText =>
					{
						var newComment = new LivetubeCommentData
						                 	{
							Number = int.Parse(extractedText.Groups[7].Value),
							Name =
							HttpUtility.HtmlDecode(
								string.IsNullOrEmpty(extractedText.Groups[13].Value)
									? (string.IsNullOrEmpty(extractedText.Groups[10].Value))
										? ""
										: extractedText.Groups[10].Value
									: extractedText.Groups[13].Value
							),
							//改行をスペースに
							Text = HttpUtility.HtmlDecode(extractedText.Groups[15].Value.Replace("<br/>", " ")),
							PostedDate = DateTime.ParseExact(extractedText.Groups[14].Value, "M/d H:m:s", null),
							ID = extractedText.Groups[2].Value,
							ContributorCanBroadcasting = extractedText.Groups[9].Value.Length != 0
						};

						//URLをエスケープ
						newComment.Text = Regex.Replace(newComment.Text, @"<a href=""(.*?)"" target=""_blank"">(.*?)</a>", @"$1");
						return newComment;
					}
				);

				//重複削除
				additionalComments = additionalComments.Distinct(c => c.Number);

				if (additionalComments.Any())
				{
					if (additionalComments.Count() == 1 && this.IsReading)
					{
						this.NotifyStatusTextChanged(String.Format("コメント抽出完了({0})", this.CommentContainer.Count + additionalComments.Count()));
					}
					else
					{
						this.NotifyStatusTextChanged(String.Format("コメント抽出完了({0}～{1})", this.CommentContainer.Count + 1, this.CommentContainer.Count + additionalComments.Count()));
					}

					this.CommentContainer.AddRange(additionalComments);
					this.NotifyCaughtComment(additionalComments);

					//IP tracing
					this.TraceIpAddressAsync(additionalComments.Select(d => d.Number));
				}

				//BANされたコメントの一覧を更新
				var webRes = this.CommentReader.GetType().InvokeMember("m_WebResponse", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance, null, this.CommentReader, null) as HttpWebResponse;
				string responseJson = webRes.Headers["X-JSON"];

				Codeplex.Data.DynamicJson banList = Codeplex.Data.DynamicJson.Parse(responseJson).cc;
				this.CommentContainer
					.Where(s => s.IsBanned)
					.Select(s => s.Number)
					.Let(recentBannedComments =>
					{
						var currentBannedComments = banList.Deserialize<List<int>>();

						//BAN解除されたコメント
						var unBannedComments = recentBannedComments.Except(currentBannedComments).Do(
							i => this.CommentContainer[i].IsBanned = false
						);

						//新規BANされたコメント
						var bannedComments = currentBannedComments.Except(recentBannedComments).Do(
							i => this.CommentContainer[i].IsBanned = true
						);

						this.NotifyBannedCommentsListFinished(unBannedComments, bannedComments);
						return recentBannedComments;
					});
			}

			//再びコネクションを張る
			if (this.IsReading)
			{
				this.CommentReader.DownloadStringAsync(new Uri(String.Format("http://livetube.cc/stream/{0}.comments.{1}", this.StreamId, this.CommentContainer.Count)));
			}
		}

		public void RefreshConnection()
		{
			if (this.CommentReader.IsBusy)
			{
				this.CommentReader.CancelAsync();
			}

			this.CommentReader.DownloadStringAsync(new Uri(String.Format("http://livetube.cc/stream/{0}.comments.{1}", this.StreamId, this.CommentContainer.Count)));
		}

		//コメント読み込み停止
		public void StopLoad()
		{
			if (this.IsReading)
			{
				//後片付け
				this.IsReading = false;
				this.StreamId = "";

				if (this.CommentReader.IsBusy)
				{
					this.CommentReader.CancelAsync();
				}

				this.CommentReader.Dispose();
				this.CommentContainer.Clear();
			}
		}

		public bool BanCommentAsync(IEnumerable<int> id)
		{
			if (GlobalValues.Setting.CredentialData.Length == 0)
			{
				return false;
			}

			if (!(id.Count() > 0))
			{
				return false;
			}

			var client = new WebClient() { Encoding = Encoding.UTF8 };

			client.DownloadStringCompleted += this.BanCommentCallBack;
			client.Headers[HttpRequestHeader.Cookie] = string.Join(";", GlobalValues.Setting.CredentialData);

			id.ForEach(
				i => client.DownloadStringAsync(new Uri(String.Format("http://livetube.cc/stream/{0}.moder.{1}.-1", this.StreamId, id)), new { i, client })
			);
			return true;
		}

		private void BanCommentCallBack(object sender, DownloadStringCompletedEventArgs e)
		{
			dynamic arg = e.UserState;

			if (e.Error != null)
			{
				this.NotifyBanCommentFinished(arg.i, false, e.Error);
			}
			else
			{
				string result = e.Result;

				if (result == "alert( 'マークしました(3)');")
				{
					this.CommentContainer[arg.i].IsBanned = true;
					this.NotifyBanCommentFinished(arg.i, true, null);
				}
				else
				{
					this.NotifyBanCommentFinished(arg.i, false, new Exception("コメントのBANに失敗しました(ID : " + arg.i));
				}
			}

			arg.client.Dispose();
		}

		public bool UnBanCommentAsync(IEnumerable<int> id)
		{
			if (GlobalValues.Setting.CredentialData.Length == 0)
			{
				return false;
			}

			if (!(id.Count() > 0))
			{
				return false;
			}

			var client = new WebClient { Encoding = Encoding.UTF8 };

			client.DownloadStringCompleted += this.BanCommentCallBack;
			client.Headers[HttpRequestHeader.Cookie] = string.Join(";", GlobalValues.Setting.CredentialData);

			id.ForEach(
				i => client.DownloadStringAsync(new Uri(String.Format("http://livetube.cc/stream/{0}.moder.{1}.1", this.StreamId, id)), new { i, client })
			);
			return true;
		}

		private void UnBanCommentCallBack(object sender, DownloadStringCompletedEventArgs e)
		{
			dynamic arg = e.UserState;

			if (e.Error != null)
			{
				this.NotifyBanCommentFinished(arg.i, false, e.Error);
			}
			else
			{
				string result = e.Result;

				if (result == "alert( 'マークしました(0)');")
				{
					
					this.CommentContainer[arg.i].IsBanned = false;
					this.NotifyBanCommentFinished(arg.i, true, null);
				}
				else
				{
					this.NotifyBanCommentFinished(arg.i, false, new Exception("コメントのBAN解除に失敗しました(ID : " + arg.i + ")"));
				}
			}

			arg.client.Dispose();
		}

		public void LoginAsync(string userId, string password)
		{
			//認証済みなら再度認証は行わない
			if (GlobalValues.Setting.CredentialData.Length != 0)
			{
				return;
			}

			var client = new WebClient { Encoding = Encoding.UTF8 };

			client.UploadValuesCompleted += this.LoginCallBack;
			client.UploadValuesAsync(new Uri("http://livetube.cc/login"), "POST", new NameValueCollection
			{
				{ "user", userId },
				{ "password", password }
			}
			, new { userID = userId, client });
		}

		private void LoginCallBack(object sender, UploadValuesCompletedEventArgs e)
		{
			dynamic arg = e.UserState;

			if (e.Error != null)
			{
				this.NotifyLoginFinished(false, e.Error);
			}
			else
			{
				string result = Encoding.UTF8.GetString(e.Result);
				

				if (result.Contains("パスワードが違うようです。再度の入力をお願いします"))
				{
					this.NotifyLoginFinished(false, new Exception("ユーザーIDとパスワードが一致しません"));
				}
				else if (result.Contains("ユーザが見つかりません。"))
				{
					this.NotifyLoginFinished(false, new Exception("ユーザーIDが存在しません"));
				}
				else
				{
					if (Regex.Match(result, "<h5><span id=\"u\">(.*?)</span></h5>").Groups[1].Value != arg.userID)
					{
						this.NotifyLoginFinished(false, new Exception("ログインに失敗しました"));
					}
					else
					{
						string setCookie = arg.client.ResponseHeaders[HttpResponseHeader.SetCookie];
						GlobalValues.Setting.CredentialData = Regex.Split(setCookie, "(?<!expires=.{3}),")
							.Select(s => s.Split(';').First().Split('='))
							.Select(xs => new { Name = xs.First(), Value = string.Join("=", xs.Skip(1).ToArray()) })
							.Select(a => a.Name + "=" + a.Value)
							.ToArray();

						this.NotifyLoginFinished(true, null);
					}
				}
			}

			arg.client.Dispose();
		}

		public void Logout()
		{
			GlobalValues.Setting.LivetubeUserID = "";
			GlobalValues.Setting.LivetubePassword = "";
			GlobalValues.Setting.CredentialData = new string[] { };
		}

		public void PostCommentAsync(string text, string name)
		{
			var client = new WebClient { Encoding = Encoding.UTF8 };
			client.Headers[HttpRequestHeader.Cookie] = string.Join(";", GlobalValues.Setting.CredentialData);
			client.UploadValuesCompleted += this.PostCommentCallBack;
			client.UploadValuesAsync(new Uri(String.Format("http://livetube.cc/stream/{0}.comments", this.StreamId)), "POST", new NameValueCollection
			{
				{ "name", name },
				{ "c", text }
			}
			,client);
		}

		private void PostCommentCallBack(object sender, UploadValuesCompletedEventArgs e)
		{
			//403 Forbidden Invokes.
			if (e.Error != null)
			{
				this.NotifyPostCommentFinished(false, e.Error);
			}

			string result = Encoding.UTF8.GetString(e.Result);

			if (!Regex.IsMatch(result, GlobalValues.ExtractCommentDataPattern))
			{
				if (result == "alert( '現在コメントを書き込むことができません。');")
				{
					this.NotifyPostCommentFinished(false, new Exception("配信者にBANされているためコメント出来ません。"));
				}
				else
				{
					//this.NotifyPostCommentFinished(false, new Exception("コメント投稿に失敗した可能性があります"));
					this.NotifyPostCommentFinished(true, null);
				}
			}
			else
			{
				this.NotifyPostCommentFinished(true, null);
			}

			(e.UserState as WebClient).Dispose();
		}

		private void TraceIpAddressAsync(IEnumerable<int> id)
		{
			this.NotifyStatusTextChanged(String.Format("書き込み元のトレース中...({0}～{1})", id.First() + 1, id.Last() + 1));

			var order = new Queue<int>(id);
			Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(3))
				.TakeWhile(_ => order.Any())
				.Select(_ => order.Dequeue())
				.ObserveOnDispatcher()
				.Subscribe(s => this.TraceIpAddressAsync(s));
		}

		private void TraceIpAddressAsync(int id)
		{
			if (this.LiveInfo.IsLoading || this.LiveInfo.Tags.Contains("traceip"))
			{
				var connector = new WebClient() { Encoding = Encoding.UTF8 };
				connector.DownloadStringCompleted += (s, e) =>
				{
					if (e.Error != null)
					{
						return;
					}

					if (Regex.IsMatch(e.Result, "alert\\( '(.*?):\\\\r\\\\n記録されていません\\(not recorded\\)'\\);"))
					{
						return;
					}

					var host = Regex.Match(e.Result, "alert\\( '(?:.*?):\\\\r\\\\nIPアドレス : (.*?)\\((.*?)\\)'\\);").Groups[1].Value;
					this.CommentContainer[id].Host = host;
					this.NotifyStatusTextChanged(String.Format("書き込み元のトレース完了({0})", id + 1));
					this.NotifyGetHostFinished(id, host);
				};

				connector.DownloadStringAsync(new Uri("http://livetube.cc/stream/" + this.StreamId + ".traceaddr." + id)); 
			}
		}

		protected virtual void NotifyLoaded(bool success)
		{
			if (this.OnLoaded != null)
			{
				this.OnLoaded(success);
			}
		}

		protected virtual void NotifyCaughtComment(IEnumerable<LivetubeCommentData> additionalComments)
		{
			if (this.OnCaughtComment != null)
			{
				this.OnCaughtComment(additionalComments);
			}
		}

		protected virtual void NotifyBanCommentFinished(int id, bool success, Exception error)
		{
			if (this.OnBanCommentFinished != null)
			{
				this.OnBanCommentFinished(id, success, error);
			}
		}

		protected virtual void NotifyUnBanCommentFinished(int id, bool success, Exception error)
		{
			if (this.OnUnBanCommentFinished != null)
			{
				this.OnUnBanCommentFinished(id, success, error);
			}
		}

		protected virtual void NotifyStatusTextChanged(string text)
		{
			if (this.OnStatusTextChanged != null)
			{
				this.OnStatusTextChanged(text);
			}
		}

		protected virtual void NotifyExceptionThrown(Exception exception, bool isCritical)
		{
			if (this.OnExceptionThrown != null)
			{
				this.OnExceptionThrown(exception, isCritical);
			}
		}

		protected virtual void NotifyLoginFinished(bool success, Exception reasonOfError)
		{
			if (this.OnLoginFinished != null)
			{
				this.OnLoginFinished(success, reasonOfError);
			}
		}

		protected virtual void NotifyPostCommentFinished(bool success, Exception reasonOfError)
		{
			if (this.OnPostCommentFinished != null)
			{
				this.OnPostCommentFinished(success, reasonOfError);
			}
		}

		protected virtual void NotifyBannedCommentsListFinished(IEnumerable<int> unBannedComments, IEnumerable<int> bannedComments)
		{
			if (this.OnBannedCommentsListRefreshed != null)
			{
				this.OnBannedCommentsListRefreshed(unBannedComments, bannedComments);
			}
		}

		protected virtual void NotifyGetHostFinished(int id, string host)
		{
			if (this.OnGetHostFinished != null)
			{
				this.OnGetHostFinished(id, host);
			}
		}
	}
}
