/*
	$Id: MainFormTimelinePanel.cs 25 2010-01-17 19:55:07Z 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.Utilities;

namespace Hiyoko.Forms{
	using WinForms = System.Windows.Forms;
	using Gdi = System.Drawing;
	
	public partial class MainFormTimelinePanel : UserControl{
		private ObservableCollection<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;
			
			this.timeline = new ObservableCollection<Status>();
			this.timelineList.DataContext = this.timeline;
			this.timelineStatusEditRow.Height = Program.Settings.TimelineStatusEditRowHeight;
			
			this.RefreshStatusEditCount();
		}
		
		#region コマンド
		
		private void Refresh_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void Refresh_Executed(object target, ExecutedRoutedEventArgs e){
			this.RefreshTimeline();
		}
		
		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>()
			                                                     .Select(status => status.User.Url)
			                                                     .Where(url => !String.IsNullOrEmpty(url))
			                                                     .Distinct()){
				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 = (this.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();
		}
		
		#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, page, 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));
						}
						
						// 新着ステータス検索
						Status[] newStatuses = timeline.Where(status => (status.CreatedAt > latest))
						                               .OrderByDescending(status => status.CreatedAt).ToArray();
						
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							if(newStatuses.Length > 0){
								this.LatestTimelineStatusDateTime = newStatuses[0].CreatedAt;
							}
							
							// タイムライン更新
							Status diff = timeline.Except(this.timeline).FirstOrDefault();
							if(diff != null){
								this.timelineList.DataContext = null;
								this.timeline.Clear();
								foreach(Status stat in timeline){
									this.timeline.Add(stat);
								}
								this.timelineList.DataContext = this.timeline;
								if(this.timelineList.Items.Count > 0){
									this.timelineList.ScrollIntoView(this.timelineList.Items[0]);
								}
							}
							
							this.OnTimelineRefreshed(new TimelineRefreshedEventArgs(newStatuses));
						}));
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
							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)){
							this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
								Program.MainForm.Outputs.Add(new SuccessOutputItem("ステータス送信成功"));
								this.statusEdit.Text = "";
							}));
						}
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
						}));
					}
				},
				delegate{
					V(this.updateStatusSemaphore);
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.statusEdit.IsReadOnly = false;
					}));
				}
			);
		}
		
		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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
							}));
						}
					}
				);
			}
		}
		
		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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
							}));
						}
					}
				);
			}
		}
		
		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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
							}));
						}
					}
				);
			}
		}
		
		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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
							}));
						}
					}
				);
			}
		}
		
		private void ShortenUrlStatusEdit(){
			UrlShorter urlShorter = this.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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
								}));
							}
						},
						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)){
							url = uploader.Upload(res);
						}
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							if(callback != null){
								callback(url);
							}
							if(url != null){
								Program.MainForm.Outputs.Add(new SuccessOutputItem("画像アップロード成功"));
							}else{
								Program.MainForm.Outputs.Add(new ErrorOutputItem("画像アップロード失敗"));
							}
						}));
					}catch(WebException ex){
						this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
							Program.MainForm.Outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, 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(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
				}));
			}
		}
		
		#endregion
		
		#region ステータス
		
		private void StatusEdit_TextChanged(object sender, TextChangedEventArgs e){
			this.RefreshStatusEditCount();
		}
		
		private void RefreshStatusEditCount(){
			int count = this.statusEdit.Text.Length;
			int dispCount = 140 - count;
			if(dispCount < 10){
				this.statusEditCount.Foreground = (SolidColorBrush)this.Resources["StatusEditCountHighWarningColor"];
			}else if(dispCount < 20){
				this.statusEditCount.Foreground = (SolidColorBrush)this.Resources["StatusEditCountWarningColor"];
			}else{
				this.statusEditCount.Foreground = (SolidColorBrush)this.Resources["StatusEditCountNormalColor"];
			}
			this.statusEditCount.Content = dispCount.ToString().PadLeft(3);
		}
		
		#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 UrlShorterProperty = DependencyProperty.Register("UrlShorter", typeof(UrlShorter), typeof(MainFormTimelinePanel));
		public UrlShorter UrlShorter{
			get{
				return (UrlShorter)this.GetValue(UrlShorterProperty);
			}
			set{
				this.SetValue(UrlShorterProperty, 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;
		}
	}
}