/*
	$Id: MainFormTimelinePanel.cs 84 2010-05-21 17:37:35Z catwalk $
*/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Xml.Linq;
using Hiyoko.Net;
using Hiyoko.Net.Twitter;
using Hiyoko.Collections;
using Hiyoko.Utilities;

namespace Hiyoko.Forms{
	using WinForms = System.Windows.Forms;
	using Gdi = System.Drawing;
	
	public partial class MainFormTimelinePanel : UserControl{
		private ObservableOrderedSkipList<Status> timeline;
		private Semaphore refreshTimelineSemaphore = new Semaphore(1, 1);
		private Semaphore updateStatusSemaphore = new Semaphore(1, 1);
		private Semaphore shortenUrlStatusEditSemaphore = new Semaphore(1, 1);
		private Semaphore uploadImageSemaphore = new Semaphore(1, 1);
		
		public MainFormTimelinePanel(){
			this.InitializeComponent();
			this.LatestTimelineStatusDateTime = Program.Settings.LatestTimelineStatusDateTime;
			this.CurrentTimelinePage = 1;
			Program.ThemeManager.Attach(this.Resources);
			
			this.timeline = new ObservableOrderedSkipList<Status>(
				new CustomComparer<Status>(delegate(Status x, Status y){
					int d = y.CreatedAt.CompareTo(x.CreatedAt);
					return (d != 0) ? d : y.Id.CompareTo(x.Id);}), false);
			this.timelineList.DataContext = this.timeline;
			this.timelineStatusEditRow.Height = Program.Settings.TimelineStatusEditRowHeight;
			
			if(AutoComplete.GetIsEnabled(this.statusEdit)){
				var dict = AutoComplete.GetCandidates(this.statusEdit);
				foreach(string hash in Program.Settings.Hashes){
					dict["#" + hash] = new Hash(hash);
				}
			}
			
			AutoComplete.SetIsEnabled(this.statusEdit, Program.Settings.IsUseAutoComplete);
			AutoComplete.SetTokenPattern(this.statusEdit, Program.Settings.AutoCompleteTokenPattern);
			AutoComplete.SetInsertWordTypes(this.statusEdit, new Type[]{typeof(string)});
			this.RefreshAutoCompleteDictionary();
			
			Program.Settings.PropertyChanged += this.ApplicationSettings_Changed;
		}
		
		private bool abortLoadingDictionary = false;
		private bool abortLoadingHash = false;
		private bool abortLoadingUser = false;
		private Semaphore loadingDictionarySemaphore = new Semaphore(1, 1);
		private Semaphore loadingHashSemaphore = new Semaphore(1, 1);
		private Semaphore loadingUserSemaphore = new Semaphore(1, 1);
		
		public void RefreshAutoCompleteDictionary(){
			if(AutoComplete.GetIsEnabled(this.statusEdit)){
				var dict = AutoComplete.GetCandidates(this.statusEdit);
				// 処理中止
				if(!IsAvailable(this.loadingDictionarySemaphore)){
					this.abortLoadingDictionary = true;
				}
				if(!IsAvailable(this.loadingHashSemaphore)){
					this.abortLoadingHash = true;
				}
				if(!IsAvailable(this.loadingUserSemaphore)){
					this.abortLoadingUser = true;
				}
				P(this.loadingDictionarySemaphore);
				this.abortLoadingDictionary = false;
				P(this.loadingHashSemaphore);
				this.abortLoadingHash = false;
				P(this.loadingUserSemaphore);
				this.abortLoadingUser = false;
				dict.Clear();
				
				var account = this.Account;
				if(account != null){
					// 追加辞書読み込み
					string path = Program.Settings.AutoCompleteAddtionalDictionaryFile;
					if(!String.IsNullOrEmpty(path) && File.Exists(path)){
						ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
							this.Dispatcher.BeginInvoke(new Action(delegate{
								Program.MainForm.Outputs.Add(new MessageOutputItem("自動補完の追加辞書を読み込み中..."));
							}));
							try{
								using(var stream = File.Open(path, FileMode.Open, FileAccess.Read))
								using(var reader = new StreamReader(stream)){
									string line;
									while((line = reader.ReadLine()) != null){
										foreach(var word in line.Split(new char[]{' ', '\t'})){
											if(!String.IsNullOrEmpty(word)){
												lock(dict){
													dict[word] = word;
												}
											}
										}
										if(this.abortLoadingDictionary){
											this.Dispatcher.BeginInvoke(new Action(delegate{
												Program.MainForm.Outputs.Add(new WarningOutputItem("自動補完の追加辞書を読み込みを中止しました。"));
											}));
											return;
										}
									}
								}
								this.Dispatcher.BeginInvoke(new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("自動補完の追加辞書を読み込み完了"));
								}));
							}catch(Exception ex){
								this.Dispatcher.BeginInvoke(new Action(delegate{
									Program.MainForm.Outputs.Add(new ErrorOutputItem("自動補完の追加辞書を読み込み中にエラーが発生しました。\n" + ex.Message));
								}));
							}finally{
								GC.Collect();
								V(this.loadingDictionarySemaphore);
							}
						}));
					}else{
						V(this.loadingDictionarySemaphore);
					}
					
					// ハッシュ読み込み
					string[] hashes = Program.Settings.Hashes;
					if(hashes != null){
						ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
							try{
								foreach(string hash in hashes){
									lock(dict){
										dict.Add("#" + hash, new Hash(hash));
									}
									if(this.abortLoadingDictionary){
										return;
									}
									if(this.abortLoadingHash){
										return;
									}
								}
							}finally{
								GC.Collect();
								V(this.loadingHashSemaphore);
							}
						}));
					}else{
						V(this.loadingHashSemaphore);
					}
					
					// ユーザー読み込み
					ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
						try{
							foreach(var user in account.GetFriends()){
								lock(dict){
									dict.Add("@" + user.ScreenName, user);
								}
								if(this.abortLoadingUser){
									return;
								}
							}
						}catch(WebException){
						}finally{
							GC.Collect();
							V(this.loadingUserSemaphore);
						}
					}));
				}else{
					V(this.loadingDictionarySemaphore);
					V(this.loadingHashSemaphore);
					V(this.loadingUserSemaphore);
				}
			}
		}
		
		#region イベント処理
		
		private void ApplicationSettings_Changed(object sender, PropertyChangedEventArgs e){
			ApplicationSettings settings = (ApplicationSettings)sender;
			switch(e.PropertyName){
				case "IsUseAutoComplete":
					AutoComplete.SetIsEnabled(this.statusEdit, settings.IsUseAutoComplete);
					this.RefreshAutoCompleteDictionary();
					break;
				case "AutoCompleteAddtionalDictionaryFile":{
					this.RefreshAutoCompleteDictionary();
					break;
				}
				case "AutoCompleteTokenPattern":{
					AutoComplete.SetTokenPattern(this.statusEdit, Program.Settings.AutoCompleteTokenPattern);
					break;
				}
				case "IsExtractImagesInStatus":{
					if(IsAvailable(this.refreshTimelineSemaphore)){
						this.timeline.Clear();
						this.RefreshTimeline();
					}
					break;
				}
			}
		}
		
		private void AutoCompleteDeleteCandidate_MouseUp(object sender, MouseButtonEventArgs e){
			if(e.ChangedButton == MouseButton.Left){
				if(this.statusEditCandidatesListBox.SelectedValue != null){
					var values = this.statusEditCandidatesListBox.SelectedItems.Cast<KeyValuePair<string, object>>()
					                                                           .Select(p => p.Value)
					                                                           .Cast<Hash>()
					                                                           .Select(h => h.Name);
					Program.Settings.Hashes = Program.Settings.Hashes.Except(values).ToArray();
					this.RefreshAutoCompleteDictionary();
					AutoComplete.RefreshList(this.statusEdit);
				}
			}
		}
		
		#endregion
		
		#region コマンド
		
		private void Refresh_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void Refresh_Executed(object target, ExecutedRoutedEventArgs e){
			this.RefreshTimeline();
			//MessageBox.Show(((SolidColorBrush)this.Resources["ControlBrush"]).Color.ToString());
		}
		
		private void UpdateStatus_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (0 < this.statusEdit.Text.Length) && (this.statusEdit.Text.Length <= 140) && IsAvailable(this.updateStatusSemaphore);
		}
		
		private void UpdateStatus_Executed(object target, ExecutedRoutedEventArgs e){
			this.UpdateStatus();
		}
		

		private void OpenUrlsTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void OpenUrlsTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			foreach(string url in this.timelineList.SelectedItems.Cast<Status>()
			                                                     .SelectMany(status => status.Text.ExtractUrls())
			                                                     .Distinct()){
				Process.Start(url);
			}
		}
		
		private void OpenHomeTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void OpenHomeTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			foreach(string url in this.timelineList.SelectedItems.Cast<Status>()
			                                                     .Select(status => "http://twitter.com/" + status.User.ScreenName)
			                                                     .Distinct()){
				Process.Start(url);
			}
		}
		
		private void OpenSiteTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void OpenSiteTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			foreach(string url in this.timelineList.SelectedItems.Cast<Status>()
			                                                     .Select(status => status.User.Url)
			                                                     .Where(url => !String.IsNullOrEmpty(url))
			                                                     .Distinct()){
				Process.Start(url);
			}
		}
		
		private void OpenStatusTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void OpenStatusTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			foreach(string url in this.timelineList.SelectedItems.Cast<Status>()
			                                                     .Distinct(new SelectEqualityComparer<Status>(s => s.Id))
			                                                     .Where(s => !s.User.ScreenName.IsNullOrEmpty())
			                                                     .Select(s => "http://twitter.com/" + s.User.ScreenName + "/status/" + s.Id)){
				Process.Start(url);
			}
		}
		
		private void CopyStatus_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void CopyStatus_Executed(object target, ExecutedRoutedEventArgs e){
			StringBuilder sb = new StringBuilder();
			foreach(Status status in this.timelineList.SelectedItems){
				sb.AppendFormat("@{0} {1}\n", status.User.ScreenName, status.Text);
			}
			Clipboard.SetData(DataFormats.Text, sb.ToString());
		}
		
		private void CreateFavoriteTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void CreateFavoriteTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			if(this.timelineList.SelectedItems.Count > 0){
				this.CreateFavorite(this.timelineList.SelectedItems.Cast<Status>());
			}
		}
		
		private void CreateBlockTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void CreateBlockTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			if(this.timelineList.SelectedItems.Count > 0){
				this.CreateBlock(this.timelineList.SelectedItems.Cast<Status>());
			}
		}
		
		private void DestroyStatus_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void DestroyStatus_Executed(object target, ExecutedRoutedEventArgs e){
			if(MessageBox.Show("選択したステータスを削除します。\nよろしいですか？", "確認", MessageBoxButton.YesNo) == MessageBoxResult.Yes){
				if(this.timelineList.SelectedItems.Count > 0){
					this.DestroyStatus(this.timelineList.SelectedItems.Cast<Status>());
				}
			}
		}
		
		private void DestroyFriendshipTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null);
		}
		
		private void DestroyFriendshipTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			if(MessageBox.Show("選択したユーザーのフォローを削除します。\nよろしいですか？", "確認", MessageBoxButton.YesNo) == MessageBoxResult.Yes){
				if(this.timelineList.SelectedItems.Count > 0){
					this.DestroyFriendship(this.timelineList.SelectedItems.Cast<Status>());
				}
			}
		}
		
		private void ScrollPageUpTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void ScrollPageUpTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			ScrollViewer sv = GetScrollViewer(this.timelineList);
			if(sv != null){
				sv.LineLeft();
			}else{
				Debug.WriteLine(new ErrorOutputItem("can`t find sv"));
			}
		}
		
		private void ScrollPageDownTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void ScrollPageDownTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			ScrollViewer sv = GetScrollViewer(this.timelineList);
			if(sv != null){
				sv.LineRight();
			}else{
				Debug.WriteLine(new ErrorOutputItem("can`t find sv"));
			}
		}
		
		private void ShortenUrlStatusEdit_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (Program.UrlShorter != null) && IsAvailable(this.shortenUrlStatusEditSemaphore);
		}
		
		private void ShortenUrlStatusEdit_Executed(object target, ExecutedRoutedEventArgs e){
			this.ShortenUrlStatusEdit();
		}
		
		private void UploadImageStatusEdit_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = !(String.IsNullOrEmpty(Program.Settings.TwitpicUsername)) && IsAvailable(this.uploadImageSemaphore);
		}
		
		private void UploadImageStatusEdit_Executed(object target, ExecutedRoutedEventArgs e){
			WinForms.OpenFileDialog dlg = new WinForms.OpenFileDialog();
			dlg.Filter = "画像ファイル|*.jpg;*.jpeg;*.png;*.gif";
			if(dlg.ShowDialog() == WinForms.DialogResult.OK){
				this.statusEdit.IsReadOnly = true;
				this.UploadImage(dlg.FileName, delegate(string url){
					this.statusEdit.IsReadOnly = false;
					if(url != null){
						this.statusEdit.SelectedText = url;
					}
				});
			}
		}
		
		private void FirstPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.CurrentTimelinePage != 1) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void FirstPage_Executed(object target, ExecutedRoutedEventArgs e){
			this.CurrentTimelinePage = 1;
			this.RefreshTimeline();
		}
		
		private void NextPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timeline != null) && (this.timeline.Count > 0) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void NextPage_Executed(object target, ExecutedRoutedEventArgs e){
			this.CurrentTimelinePage++;
			this.RefreshTimeline();
		}
		
		private void PreviousPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.CurrentTimelinePage > 1) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void PreviousPage_Executed(object target, ExecutedRoutedEventArgs e){
			this.CurrentTimelinePage--;
			this.RefreshTimeline();
		}
		
		private void ReplyTo_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null) && IsAvailable(this.updateStatusSemaphore);
		}
		
		private void ReplyTo_Executed(object target, ExecutedRoutedEventArgs e){
			Status status = (Status)this.timelineList.SelectedValue;
			this.StatusToReply = status;
			this.statusEdit.Text = String.Format("@{0} {1}", status.User.ScreenName, this.statusEdit.Text);
			this.statusEdit.SelectionStart = status.User.ScreenName.Length + 2;
			this.statusEdit.SelectionLength = 0;
			this.statusEdit.Focus();
		}
		
		private void ClearReplyTo_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.StatusToReply != Status.Empty);
		}
		
		private void ClearReplyTo_Executed(object target, ExecutedRoutedEventArgs e){
			this.StatusToReply = Status.Empty;
		}
		
		private void Retweet_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null) && IsAvailable(this.updateStatusSemaphore);
		}
		
		private void Retweet_Executed(object target, ExecutedRoutedEventArgs e){
			Status status = (Status)this.timelineList.SelectedValue;
			//this.StatusToReply = status;
			int idx = this.statusEdit.Text.Length;
			this.statusEdit.Text = String.Format("{2} RT @{0} {1}", status.User.ScreenName, status.Text, this.statusEdit.Text);
			this.statusEdit.SelectionStart = idx;
			this.statusEdit.SelectionLength = 0;
			this.statusEdit.Focus();
		}
		
		private void OfficialRetweet_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineList.SelectedValue != null) && IsAvailable(this.updateStatusSemaphore);
		}
		
		private void OfficialRetweet_Executed(object target, ExecutedRoutedEventArgs e){
			Status status = (Status)this.timelineList.SelectedValue;
			Account account = this.Account;
			Program.NetworkJobServer.EnqueueJob(
				delegate(object arg){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						Program.MainForm.Outputs.Add(new MessageOutputItem("Retweet中..."));
					}));
					return TwitterAPI.Retweet(account.AccessToken, status.Id);
				},
				this.WriteRequestDataCallback,
				delegate(NetworkJobData data){
					try{
						using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
						}
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new SuccessOutputItem("Retweet成功"));
						}));
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
						}));
					}
				},
				null
			);
		}
		
		#endregion
		
		#region 通信
		
		public void RefreshTimeline(){
			P(this.refreshTimelineSemaphore);
			Account account = this.Account;
			int page = this.CurrentTimelinePage;
			DateTime latest = this.LatestTimelineStatusDateTime;
			Program.NetworkJobServer.EnqueueJob(
				delegate{
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						Program.MainForm.Outputs.Add(new MessageOutputItem("タイムライン取得中..."));
					}));
					return TwitterAPI.GetHomeTimeline(account.AccessToken, Program.Settings.TimelineCount, 0, 0, 0);
				},
				null,
				delegate(NetworkJobData data){
					try{
						// タイムライン取得
						XDocument xml;
						Collection<Status> timeline = new Collection<Status>();
						using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult))
						using(Stream stream = res.GetResponseStream())
						using(StreamReader reader = new StreamReader(stream, Encoding.UTF8)){
							xml = XDocument.Parse(reader.ReadToEnd());
						}
						foreach(XElement status in xml.Element("statuses").Elements("status")){
							timeline.Add(Status.FromXElement(status));
						}
						
						// 新着ステータス検索
						var newStatuses = timeline.Where(status => (status.CreatedAt > latest))
						                          .OrderByDescending(status => status.CreatedAt).ToArray();
						var removedStatuses = this.timeline.Except(timeline).ToArray();
						var addedStatuses = timeline.Except(this.timeline).ToArray();
						
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{

							if(newStatuses.Length > 0){
								this.LatestTimelineStatusDateTime = newStatuses[0].CreatedAt;
							}
							
							// タイムライン更新
							foreach(var item in removedStatuses){
								this.timeline.Remove(item);
							}
							foreach(var item in addedStatuses){
								this.timeline.Add(item);
							}
							
							if((this.timelineList.SelectedValue == null) && (this.timelineList.Items.Count > 0)){
								ScrollViewer sv = GetScrollViewer(this.timelineList);
								sv.ScrollToLeftEnd();
							}
							this.OnTimelineRefreshed(new TimelineRefreshedEventArgs(newStatuses));

						}));
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
							this.timeline.Clear();
						}));
					}
				},
				delegate{
					V(this.refreshTimelineSemaphore);
				}
			); 
		}
		
		private void UpdateStatus(){
			P(this.updateStatusSemaphore);
			string status = this.statusEdit.Text;
			this.statusEdit.IsReadOnly = true;
			Account account = this.Account;
			decimal replyTo = this.StatusToReply.Id;
			this.StatusToReply = Status.Empty;
			Program.NetworkJobServer.EnqueueJob(
				delegate(object arg){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						Program.MainForm.Outputs.Add(new MessageOutputItem("ステータス送信中..."));
					}));
					return TwitterAPI.UpdateStatus(account.AccessToken, status, replyTo, "Hiyoko");
				},
				this.WriteRequestDataCallback,
				delegate(NetworkJobData data){
					try{
						using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
							// ハッシュ登録
							if(Program.Settings.Hashes == null){
								Program.Settings.Hashes = new string[0];
							}
							var hashes = Regex.Matches(status, "#([a-zA-Z0-9_]+|[^ \t]+_)")
							                  .Cast<Match>()
							                  .Select(m => m.Groups[1].Value)
							                  .Union(Program.Settings.Hashes);
							Program.Settings.Hashes = hashes.ToArray();
							
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new SuccessOutputItem("ステータス送信成功"));
								this.statusEdit.Text = "";
								if(AutoComplete.GetIsEnabled(this.statusEdit)){
									var dict = AutoComplete.GetCandidates(this.statusEdit);
									foreach(string hash in Program.Settings.Hashes){
										dict["#" + hash] = new Hash(hash);
									}
								}
							}));
						}
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
						}));
					}
				},
				delegate{
					V(this.updateStatusSemaphore);
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.statusEdit.IsReadOnly = false;
						if(IsAvailable(refreshTimelineSemaphore)){
							this.RefreshTimeline();
						}
					}));
				}
			);
		}
		
		private void DestroyStatus(IEnumerable<Status> statuses){
			Account account = this.Account;
			foreach(Status stat in statuses){
				Status status = stat;
				Program.NetworkJobServer.EnqueueJob(
					delegate{
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new MessageOutputItem("ステータス削除中..."));
						}));
						return TwitterAPI.DestroyStatus(account.AccessToken, status.Id);
					},
					this.WriteRequestDataCallback,
					delegate(NetworkJobData data){
						try{
							using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("ステータス削除成功"));
								}));
							}
						}catch(WebException ex){
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
							}));
						}
					}
				);
			}
		}
		
		private void DestroyFriendship(IEnumerable<Status> statuses){
			Account account = this.Account;
			foreach(Status stat in statuses){
				Status status = stat;
				Program.NetworkJobServer.EnqueueJob(
					delegate{
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new MessageOutputItem("フォロー削除中..."));
						}));
						return TwitterAPI.DestroyFriendship(account.AccessToken, status.User.Id);
					},
					this.WriteRequestDataCallback,
					delegate(NetworkJobData data){
						try{
							using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("フォロー削除成功"));
								}));
							}
						}catch(WebException ex){
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
							}));
						}
					}
				);
			}
		}
		
		private void CreateFavorite(IEnumerable<Status> statuses){
			Account account = this.Account;
			foreach(Status stat in statuses){
				Status status = stat;
				Program.NetworkJobServer.EnqueueJob(
					delegate{
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new MessageOutputItem("お気に入り追加中..."));
						}));
						return TwitterAPI.CreateFavorite(account.AccessToken, status.Id);
					},
					this.WriteRequestDataCallback,
					delegate(NetworkJobData data){
						try{
							using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("お気に入り追加成功"));
								}));
							}
						}catch(WebException ex){
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
							}));
						}
					}
				);
			}
		}
		
		private void CreateBlock(IEnumerable<Status> statuses){
			Account account = this.Account;
			foreach(Status stat in statuses){
				Status status = stat;
				Program.NetworkJobServer.EnqueueJob(
					delegate{
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new MessageOutputItem("ブロック追加中..."));
						}));
						return TwitterAPI.CreateBlock(account.AccessToken, status.User.Id);
					},
					this.WriteRequestDataCallback,
					delegate(NetworkJobData data){
						try{
							using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("ブロック追加成功"));
								}));
							}
						}catch(WebException ex){
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
							}));
						}
					}
				);
			}
		}
		
		private void ShortenUrlStatusEdit(){
			UrlShorter urlShorter = Program.UrlShorter;
			if(urlShorter == null){
				throw new InvalidOperationException();
			}
			
			string status = this.statusEdit.Text;
			this.statusEdit.IsReadOnly = true;
			
			P(this.shortenUrlStatusEditSemaphore);
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				string shortStatus = StringEx.UrlRegex.Replace(status, new MatchEvaluator(delegate(Match match){
					string shortUrl = null;
					ManualResetEvent wait = new ManualResetEvent(false);
					Program.NetworkJobServer.EnqueueJob(
						delegate{
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new MessageOutputItem("短縮URL取得中..."));
							}));
							return urlShorter.RequireShorten(match.Value);
						},
						null,
						delegate(NetworkJobData data){
							try{
								using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
									shortUrl = urlShorter.Shorten(res);
								}
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new SuccessOutputItem("短縮URL取得成功"));
								}));
							}catch(WebException ex){
								this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
									Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
								}));
							}
						},
						delegate{
							wait.Set();
						}
					);
					wait.WaitOne();
					return shortUrl;
				}));
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					this.statusEdit.Text = shortStatus;
					this.statusEdit.IsReadOnly = false;
				}));
				V(this.shortenUrlStatusEditSemaphore);
			}));
		}
		
		private void UploadImage(string filename, Action<string> callback){
			WebUploader uploader = new Twitpic(Program.Settings.TwitpicUsername, Program.Settings.TwitpicPassword);
			P(this.uploadImageSemaphore);
			Program.NetworkJobServer.EnqueueJob(
				delegate{
					try{
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new MessageOutputItem("画像アップロード中..."));
						}));
						return uploader.RequestUpload(filename);
					}catch(Exception ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(ex.Message));
							if(callback != null){
								callback(null);
							}
						}));
					}
					return null;
				},
				this.WriteRequestDataCallback,
				delegate(NetworkJobData data){
					try{
						string url = null;
						using(HttpWebResponse res = (HttpWebResponse)data.WebRequestData.WebRequest.EndGetResponse(data.AsyncResult)){
							try{
								url = uploader.Upload(res);
							}catch(WebException ex){
								Program.MainForm.Outputs.Add(new ErrorOutputItem("画像アップロード失敗\n" + ex.Message));
							}
						}
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							if(callback != null){
								callback(url);
							}
							Program.MainForm.Outputs.Add(new SuccessOutputItem("画像アップロード成功"));
						}));
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(ex.Message));
						}));
					}
				},
				delegate{
					V(this.uploadImageSemaphore);
				}
			);
		}
		
		private void WriteRequestDataCallback(NetworkJobData data){
			try{
				using(Stream stream = data.WebRequestData.WebRequest.EndGetRequestStream(data.AsyncResult)){
					data.WebRequestData.WriteRequestData(stream);
				}
			}catch(WebException ex){
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					Program.MainForm.Outputs.Add(new ErrorOutputItem(TwitterAPI.GetErrorMessage(ex)));
				}));
			}
		}
		
		#endregion
		
		#region セマフォ
		
		private static bool IsAvailable(Semaphore sem){
			bool signal = sem.WaitOne(0, false);
			if(signal){
				sem.Release();
			}
			return signal;
		}
		
		private static void P(Semaphore sem){
			sem.WaitOne();
			Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new Action(delegate{
				CommandManager.InvalidateRequerySuggested();
			}));
		}
		
		private static void V(Semaphore sem){
			sem.Release();
			Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, new Action(delegate{
				CommandManager.InvalidateRequerySuggested();
			}));
		}
		
		#endregion
		
		#region プロパティ
		
		public static readonly DependencyProperty AccountProperty = DependencyProperty.Register("Account", typeof(Account), typeof(MainFormTimelinePanel));
		public Account Account{
			get{
				return (Account)this.GetValue(AccountProperty);
			}
			set{
				this.SetValue(AccountProperty, value);
			}
		}
		
		public static readonly DependencyProperty CurrentTimelinePageProperty = DependencyProperty.Register("CurrentTimelinePage", typeof(int), typeof(MainFormTimelinePanel));
		public int CurrentTimelinePage{
			get{
				return (int)this.GetValue(CurrentTimelinePageProperty);
			}
			set{
				if(value <= 0){
					throw new ArgumentOutOfRangeException();
				}
				this.SetValue(CurrentTimelinePageProperty, value);
			}
		}
		
		public static readonly DependencyProperty LatestTimelineStatusDateTimeProperty = DependencyProperty.Register("LatestTimelineStatusDateTime", typeof(DateTime), typeof(MainFormTimelinePanel));
		public DateTime LatestTimelineStatusDateTime{
			get{
				return (DateTime)this.GetValue(LatestTimelineStatusDateTimeProperty);
			}
			set{
				this.SetValue(LatestTimelineStatusDateTimeProperty, value);
			}
		}
		
		public static readonly DependencyProperty StatusToReplyProperty = DependencyProperty.Register("StatusToReply", typeof(Status), typeof(MainFormTimelinePanel));
		public Status StatusToReply{
			get{
				return (Status)this.GetValue(StatusToReplyProperty);
			}
			set{
				this.SetValue(StatusToReplyProperty, value);
			}
		}
		
		public bool IsRefreshingTimeline{
			get{
				return !IsAvailable(this.refreshTimelineSemaphore);
			}
		}
		
		public ReadOnlyCollection<Status> Timeline{
			get{
				return new ReadOnlyCollection<Status>(this.timeline);
			}
		}
		
		public TextBox StatusEdit{
			get{
				return this.statusEdit;
			}
		}
		
		public ListBox TimelineList{
			get{
				return this.timelineList;
			}
		}
		
		public GridLength StatusEditRowHeight{
			get{
				return this.timelineStatusEditRow.Height;
			}
		}
		
		#endregion
		
		#region イベント
		
		public event TimelineRefreshedEventHandler TimelineRefreshed;
		
		protected virtual void OnTimelineRefreshed(TimelineRefreshedEventArgs e){
			if(this.TimelineRefreshed != null){
				this.TimelineRefreshed(this, e);
			}
		}
		
		#endregion
		
		#region その他
		
		private static ScrollViewer GetScrollViewer(DependencyObject dobj){
			if(dobj is ScrollViewer){
				return dobj as ScrollViewer;
			}
			int count = VisualTreeHelper.GetChildrenCount(dobj);
			for(int i = 0; i < count; i++){
				var ret = GetScrollViewer(VisualTreeHelper.GetChild(dobj, i));
				if(ret != null){
					return ret;
				}
			}
			return null;
		}
		
		#endregion
	}
	
	public delegate void TimelineRefreshedEventHandler(object sender, TimelineRefreshedEventArgs e);
	
	public class TimelineRefreshedEventArgs : EventArgs{
		public Status[] NewStatuses{get; private set;}
		
		public TimelineRefreshedEventArgs(Status[] newStatuses){
			this.NewStatuses = newStatuses;
		}
	}
	
	public struct Hash{
		private string name;
		
		public string Name{
			get{
				return this.name;
			}
		}
		
		public Hash(string name){
			this.name = name;
		}
		
		public override string ToString(){
			return "#" + this.name;
		}
	}
	
	public class AutoCompleteCandidateTemplateSelector : DataTemplateSelector{
		public override DataTemplate SelectTemplate(object item, DependencyObject container){
			var frm = (FrameworkElement)container;
			var pair = (KeyValuePair<string, object>)item;
			if(pair.Value is User){
				return (DataTemplate)frm.FindResource("AutoCompleteUserCandidateTemplate");
			}else if(pair.Value is Hash){
				return (DataTemplate)frm.FindResource("AutoCompleteHashCandidateTemplate");
			}else{
				return (DataTemplate)frm.FindResource("AutoCompleteCandidateTemplate");
			}
		}
	}
}