/*
	$Id: MainForm.cs 9 2009-12-26 03:15:14Z 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.Net;
using System.Text;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
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.Twitter;
using Hiyoko.Utilities;

namespace Hiyoko.Forms{
	using WinForms = System.Windows.Forms;
	using Gdi = System.Drawing;
	
	public partial class MainForm : Window{
		private DateTime latestTimelineStatusDate = Program.Settings.LatestTimelineStatusDate;
		private DispatcherTimer autoRefreshTimelineTimer = new DispatcherTimer(DispatcherPriority.Background);
		private Gdi.Icon emptyIcon;
		private Gdi.Icon letterIcon;
		private ObservableCollection<OutputItem> outputs;
		private ObservableCollection<Status> timeline;
		private Account currentAccount = new Account("", "");
		private WinForms.ContextMenu notifyIconContextMenu = new WinForms.ContextMenu();
		private WinForms.NotifyIcon notifyIcon = new WinForms.NotifyIcon();
		private WindowSettings windowSettings = new WindowSettings("MainForm");
		private WindowState restoreState;
		private XElement timelineXml = null;
		private List<Status> newTimelineStatusesNotShown = new List<Status>();
		private double autoRefreshTimelineWeight = 1;
		private Semaphore refreshTimelineSemaphore = new Semaphore(1, 1);
		private Semaphore updateStatusSemaphore = new Semaphore(1, 1);
		private Semaphore networkSemaphore = new Semaphore(1, 1);
		private OutputPanelTraceListener traceListener;
		
		public MainForm(){
			this.InitializeComponent();
			
			this.currentAccount.Username = Program.Settings.AccountUsername;
			this.currentAccount.Password = Program.Settings.AccountPassword;
			
			this.outputs = new ObservableCollection<OutputItem>(new SkipList<OutputItem>());
			this.outputs.CollectionChanged += this.Outputs_CollectionChanged;
			this.outputList.DataContext = this.outputs;
			
			this.timeline = new ObservableCollection<Status>();
			this.timelineList.DataContext = this.timeline;
			
			// 通知アイコン
			System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
			this.letterIcon = new Gdi.Icon(asm.GetManifestResourceStream("Resources.Letter.ico"));
			this.emptyIcon = new Gdi.Icon(asm.GetManifestResourceStream("Resources.Empty.ico"));
			this.notifyIcon.Icon = this.emptyIcon;
			this.notifyIcon.MouseClick += this.NotifyIcon_MouseClick;
			this.notifyIcon.Text = "Hiyoko - A Twitter Client";
			
			// 通知アイコンのコンテキストメニュー。
			this.notifyIconContextMenu.MenuItems.Add(new WinForms.MenuItem("表示(&V)", delegate{
				this.ShowWindow();
			}));
			this.notifyIconContextMenu.MenuItems.Add(new WinForms.MenuItem("-"));
			this.notifyIconContextMenu.MenuItems.Add(new WinForms.MenuItem("終了(&V)", delegate{
				Application.Current.Shutdown();
			}));
			this.notifyIcon.ContextMenu = this.notifyIconContextMenu;
			
			// ウインドウ復元
			this.windowSettings.UpgradeOnce();
			this.Left = ValidPositionValue(this.windowSettings.RestoreBounds.Left);
			this.Top = ValidPositionValue(this.windowSettings.RestoreBounds.Top);
			this.Width = ValidSizeValue(this.windowSettings.RestoreBounds.Width);
			this.Height = ValidSizeValue(this.windowSettings.RestoreBounds.Height);
			this.WindowState = this.windowSettings.WindowState;
			this.outputListRow.Height = Program.Settings.OutputListRowHeight;
			
			// イベント
			Program.Settings.PropertyChanged += this.ApplicationSettings_Changed;
			
			// TraceListener
			this.traceListener = new OutputPanelTraceListener(this);
			Debug.Listeners.Add(this.traceListener);
			
			// タイマー
			this.autoRefreshTimelineTimer.Tick += this.AutoRefreshTimelineTimer_Tick;
			this.autoRefreshTimelineTimer.Interval = TimeSpan.FromSeconds(60);
			this.CalculateAutoRefreshTimelineInterval();
			this.autoRefreshTimelineTimer.IsEnabled = Program.Settings.IsAutoRefreshTimeline;
			
			// 初期処理
			this.RefreshStatusEditCount();
			this.CurrentTimelinePage = 1;
		}
		
		private static double ValidSizeValue(double v){
			return ValidPositionValue(v);
		}
		
		private static double ValidPositionValue(double v){
			return ((Double.NegativeInfinity < v) && (v < Double.PositiveInfinity)) ? v : Double.NaN;
		}
		
		#region イベント
		
		protected override void OnInitialized(EventArgs e){
			base.OnInitialized(e);
			if(Program.Settings.IsResident){
				this.notifyIcon.Visible = true;
			}
			this.restoreState = this.WindowState;
		}
		
		protected override void OnClosing(CancelEventArgs e){
			base.OnClosing(e);
			this.windowSettings.RestoreBounds = this.RestoreBounds;
			if(this.WindowState != WindowState.Minimized){
				this.windowSettings.WindowState = this.WindowState;
			}
			Program.Settings.OutputListRowHeight = this.outputListRow.Height;
			if(Program.Settings.IsResident){
				e.Cancel = true;
				this.Hide();
			}
		}
		
		protected override void OnClosed(EventArgs e){
			base.OnClosed(e);
			Program.Settings.LatestTimelineStatusDate = this.latestTimelineStatusDate;
			this.windowSettings.Save();
			this.notifyIcon.Visible = false;
			this.notifyIcon.Dispose();
			this.letterIcon.Dispose();
			this.emptyIcon.Dispose();
			this.autoRefreshTimelineTimer.Stop();
			Program.Settings.PropertyChanged -= this.ApplicationSettings_Changed;
			Debug.Listeners.Remove(this.traceListener);
		}
		
		protected override void OnActivated(EventArgs e){
			this.notifyIcon.Icon = this.emptyIcon;
			this.newTimelineStatusesNotShown.Clear();
			this.notifyIcon.Text = "Hiyoko - A Twitter Client";
			this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(delegate{
				CommandManager.InvalidateRequerySuggested();
			}));
			base.OnActivated(e);
		}
		
		protected override void OnStateChanged(EventArgs e){
			if(this.WindowState == WindowState.Minimized){
				if(Program.Settings.IsHideOnMinimized){
					this.Hide();
				}
			}else{
				this.restoreState = this.WindowState;
			}
			base.OnStateChanged(e);
		}
		
		private void NotifyIcon_MouseClick(object sender, WinForms.MouseEventArgs e){
			if((e.Button & WinForms.MouseButtons.Left) > 0){
				this.ShowWindow();
			}
		}
		
		private void ShowWindow(){
			this.Show();
			this.Activate();
			this.WindowState = this.restoreState;
		}
		
		private void ApplicationSettings_Changed(object sender, PropertyChangedEventArgs e){
			ApplicationSettings settings = (ApplicationSettings)sender;
			switch(e.PropertyName){
				case "IsResident":{
					this.notifyIcon.Visible = settings.IsResident;
					break;
				}
				case "IsAutoRefreshTimeline":{
					this.autoRefreshTimelineTimer.IsEnabled = settings.IsAutoRefreshTimeline;
					this.CalculateAutoRefreshTimelineInterval();
					break;
				}
				/*case "AutoRefreshTimelineInterval":{
					this.autoRefreshTimelineTimer.Interval = TimeSpan.FromSeconds(settings.AutoRefreshTimelineInterval);
					break;
				}*/
				case "AccountUsername":{
					this.currentAccount.Username = settings.AccountUsername;
					break;
				}
				case "AccountPassword":{
					this.currentAccount.Password = settings.AccountPassword;
					break;
				}
			}
		}
		
		private void AutoRefreshTimelineTimer_Tick(object sender, EventArgs e){
			if(IsAvailable(this.networkSemaphore)){
				if(this.IsActive){
					if(this.CurrentTimelinePage == 1){
						this.RefreshTimeline(true);
					}
				}else{
					this.RefreshTimeline(true);
				}
			}
		}
		
		private void TimelineItem_MouseDown(object sender, MouseButtonEventArgs e){
			if((e.ClickCount > 1) && HiyokoCommands.OpenUrls.CanExecute(this.timelineTabItem, null)){
				HiyokoCommands.OpenUrls.Execute(this.timelineTabItem, null);
			}
		}
		
		#endregion
		
		#region コマンド
		
		private void About_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void About_Executed(object target, ExecutedRoutedEventArgs e){
			AboutForm about = new AboutForm();
			about.Owner = this;
			about.ShowDialog();
		}
		
		private void Settings_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void Settings_Executed(object target, ExecutedRoutedEventArgs e){
			SettingsForm settings = new SettingsForm();
			settings.Owner = this;
			settings.ShowDialog();
		}
		
		private void Close_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void Close_Executed(object target, ExecutedRoutedEventArgs e){
			Application.Current.Shutdown();
		}
		
		private void SaveTimeline_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = (this.timelineXml != null);
		}
		
		private void SaveTimeline_Executed(object target, ExecutedRoutedEventArgs e){
			WinForms.SaveFileDialog dlg = new WinForms.SaveFileDialog();
			dlg.Filter = "Xmlファイル|*.xml";
			dlg.DefaultExt = "xml";
			if(dlg.ShowDialog() == WinForms.DialogResult.OK){
				this.timelineXml.Save(dlg.FileName);
			}
		}
		
		private void Refresh_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void Refresh_Executed(object target, ExecutedRoutedEventArgs e){
			this.RefreshTimeline(false);
		}
		
		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 OpenTwitterCom_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = true;
		}
		
		private void OpenTwitterCom_Executed(object target, ExecutedRoutedEventArgs e){
			Process.Start("http://twitter.com");
		}
		
		private void OpenMyHome_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = !(String.IsNullOrEmpty(this.currentAccount.Username));
		}
		
		private void OpenMyHome_Executed(object target, ExecutedRoutedEventArgs e){
			Process.Start("http://twitter.com/" + this.currentAccount.Username);
		}
		
		#endregion
		
		#region コマンド - Timeline
		
		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 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.LineUp();
			}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.LineDown();
			}else{
				Debug.WriteLine(new ErrorOutputItem("can`t find sv"));
			}
		}
		
		#endregion
		
		#region コマンド - Pager
		
		private void FirstPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = this.timelineTabItem.IsSelected && (this.CurrentTimelinePage != 1) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void FirstPage_Executed(object target, ExecutedRoutedEventArgs e){
			if(this.timelineTabItem.IsSelected){
				this.CurrentTimelinePage = 1;
				this.RefreshTimeline(false);
			}
		}
		
		private void NextPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = this.timelineTabItem.IsSelected && (this.timeline != null) && (this.timeline.Count > 0) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void NextPage_Executed(object target, ExecutedRoutedEventArgs e){
			if(this.timelineTabItem.IsSelected){
				this.CurrentTimelinePage++;
				this.RefreshTimeline(false);
			}
		}
		
		private void PreviousPage_CanExecute(object target, CanExecuteRoutedEventArgs e){
			e.CanExecute = this.timelineTabItem.IsSelected && (this.CurrentTimelinePage > 1) && IsAvailable(this.refreshTimelineSemaphore);
		}
		
		private void PreviousPage_Executed(object target, ExecutedRoutedEventArgs e){
			if(this.timelineTabItem.IsSelected){
				this.CurrentTimelinePage--;
				this.RefreshTimeline(false);
			}
		}
		
		#endregion
		
		#region タイムライン
		
		private void RefreshTimeline(){
			this.RefreshTimeline(false);
		}
		
		private void RefreshTimeline(bool isAuto){
			P(this.refreshTimelineSemaphore);
			if(this.autoRefreshTimelineTimer.IsEnabled){
				this.autoRefreshTimelineTimer.Stop();
				this.autoRefreshTimelineTimer.Start();
			}
			int page = this.CurrentTimelinePage;
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					this.outputs.Add(new MessageOutputItem("タイムライン取得中..."));
				}));
				try{
					P(this.networkSemaphore);
					XElement xml;
					Collection<Status> timeline = this.currentAccount.GetHomeTimeline(Program.Settings.TimelineCount, page, 0, 0, out xml);
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						
						// 新着ステータス取得
						var query = from status in timeline
						            where status.CreatedAt > this.latestTimelineStatusDate
						            orderby status.CreatedAt descending
						            select status;
						Status[] newStatuses = query.ToArray();
						if(newStatuses.Length > 0){
							this.latestTimelineStatusDate = newStatuses[0].CreatedAt;
						}
						
						// バルーンチップ
						if((newStatuses.Length > 0) && this.notifyIcon.Visible && !this.IsActive){
							StringBuilder sb = new StringBuilder();
							foreach(Status status in newStatuses){
								sb.AppendFormat("{0} / {1}\n", status.User.Name, status.User.ScreenName);
								sb.AppendFormat("{0}\n", status.Text);
							}
							this.newTimelineStatusesNotShown.AddRange(newStatuses);
							this.notifyIcon.ShowBalloonTip(
								10000,
								String.Format("新着ステータス {0}件 / 未読 {1}件", newStatuses.Length, this.newTimelineStatusesNotShown.Count),
								sb.ToString(),
								WinForms.ToolTipIcon.None);
							this.notifyIcon.Icon = this.letterIcon;
							this.notifyIcon.Text = String.Format("Hiyoko - A Twitter Client\n新着{0}件", this.newTimelineStatusesNotShown.Count);
						}
						
						// アウトプットメッセージ
						if(newStatuses.Length > 0){
							this.outputs.Add(new SuccessOutputItem(String.Format("タイムライン取得成功 新着{0}件", newStatuses.Length)));
						}else{
							this.outputs.Add(new SuccessOutputItem("タイムライン取得成功"));
						}
						
						// タイムライン更新
						this.timeline.Clear();
						foreach(Status stat in timeline){
							this.timeline.Add(stat);
						}
						if(this.timeline.Count > 0){
							this.timelineList.ScrollIntoView(this.timeline[0]);
						}
						this.timelineXml = xml;
						
						// インターバル
						if(isAuto){
							if(newStatuses.Length > 0){
								this.autoRefreshTimelineWeight = 1;
							}else{
								this.autoRefreshTimelineWeight *= 0.5;
							}
						}
						if(this.autoRefreshTimelineTimer.IsEnabled){
							this.CalculateAutoRefreshTimelineInterval();
							this.outputs.Add(new MessageOutputItem(String.Format(
								"次の自動取得は {0} 分後",
								Math.Round(this.autoRefreshTimelineTimer.Interval.TotalMinutes))));
						}
						
						CommandManager.InvalidateRequerySuggested();
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
						this.timeline.Clear();
					}));
				}finally{
					V(this.networkSemaphore);
				}
				V(this.refreshTimelineSemaphore);
			}));
		}
		
		private void UpdateStatus(){
			P(this.updateStatusSemaphore);
			string status = this.statusEdit.Text;
			this.statusEdit.IsReadOnly = true;
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					this.outputs.Add(new MessageOutputItem("ステータス送信中..."));
				}));
				
				try{
					P(this.networkSemaphore);
					this.currentAccount.UpdateStatus(status, 0, "Hiyoko");
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new SuccessOutputItem("ステータス送信成功"));
						this.statusEdit.Text = "";
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
					}));
				}finally{
					V(this.networkSemaphore);
				}
				
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					this.statusEdit.IsReadOnly = false;
					if(IsAvailable(this.refreshTimelineSemaphore)){
						this.RefreshTimeline(false);
					}
				}));
				
				V(this.updateStatusSemaphore);
			}));
		}
		
		private void DestroyStatus(IEnumerable<Status> statuses){
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				try{
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new MessageOutputItem("ステータス削除中..."));
					}));
					P(this.networkSemaphore);
					foreach(Status status in statuses){
						this.currentAccount.DestroyStatus(status.Id);
					}
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new SuccessOutputItem("ステータス削除成功"));
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
					}));
				}finally{
					V(this.networkSemaphore);
				}
				
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					if(IsAvailable(this.refreshTimelineSemaphore)){
						this.RefreshTimeline(false);
					}
				}));
			}));
		}
		
		private void DestroyFriendship(IEnumerable<Status> statuses){
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				try{
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new MessageOutputItem("フォロー削除中..."));
					}));
					P(this.networkSemaphore);
					foreach(Status status in statuses){
						this.currentAccount.DestroyFriendship(status.User.Id);
					}
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new SuccessOutputItem("フォロー削除成功"));
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
					}));
				}finally{
					V(this.networkSemaphore);
				}
				
				this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
					if(IsAvailable(this.refreshTimelineSemaphore)){
						this.RefreshTimeline(false);
					}
				}));
			}));
		}
		
		private void CreateFavorite(IEnumerable<Status> statuses){
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				try{
					P(this.networkSemaphore);
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new MessageOutputItem("お気に入りに追加中..."));
					}));
					foreach(Status status in statuses){
						this.currentAccount.CreateFavorite(status.Id);
					}
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new SuccessOutputItem("お気に入りに追加成功"));
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
					}));
				}finally{
					V(this.networkSemaphore);
				}
			}));
		}
		
		private void CreateBlock(IEnumerable<Status> statuses){
			ThreadPool.QueueUserWorkItem(new WaitCallback(delegate{
				try{
					P(this.networkSemaphore);
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new MessageOutputItem("ブロックに追加中..."));
					}));
					foreach(Status status in statuses){
						this.currentAccount.CreateBlock(status.Id);
					}
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new SuccessOutputItem("ブロックに追加成功"));
					}));
				}catch(WebException ex){
					this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(delegate{
						this.outputs.Add(new ErrorOutputItem(String.Format("{0} :{1}", (int)ex.Status, ex.Message)));
					}));
				}finally{
					V(this.networkSemaphore);
				}
			}));
		}
		
		public static readonly DependencyProperty CurrentTimelinePageProperty = DependencyProperty.Register("CurrentTimelinePage", typeof(int), typeof(MainForm));
		public int CurrentTimelinePage{
			get{
				return (int)this.GetValue(CurrentTimelinePageProperty);
			}
			set{
				if(value <= 0){
					throw new ArgumentOutOfRangeException();
				}
				this.SetValue(CurrentTimelinePageProperty, value);
			}
		}
		
		/// <summery>
		/// ステータス間の投稿日時の差の加重平均からインターバルを計算する。
		/// </summery>
		private void CalculateAutoRefreshTimelineInterval(){
			if(this.CurrentTimelinePage != 0){
				TimeSpan span = TimeSpan.FromSeconds(60);
				if(this.timeline.Count > 1){
					double a = 1 - this.autoRefreshTimelineWeight;
					double w = 1;
					double wsum = 0;
					decimal sum = 0;
					DateTime[] times = (new DateTime[]{DateTime.Now}).Concat(
						this.timeline.Select(s => s.CreatedAt)).ToArray();
					for(int i = 1; i < times.Length; i++, w *= a){
						TimeSpan t = times[i - 1] - times[i];
						sum += (decimal)(t.TotalSeconds * w);
						wsum += w;
					}
					sum /= (decimal)wsum;
					sum /= 2;
					if(sum > 60){
						span = TimeSpan.FromSeconds((double)sum);
					}
				}
				this.autoRefreshTimelineTimer.Stop();
				this.autoRefreshTimelineTimer.Interval = span;
				this.autoRefreshTimelineTimer.Start();
			}
		}
		
		#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 void Outputs_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e){
			this.outputList.Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(delegate{
				if(this.outputs.Count > Program.Settings.OutputItemLimitCount){
					this.outputs.CollectionChanged -= this.Outputs_CollectionChanged;
					for(int i = Program.Settings.OutputItemLimitCount; i <= this.outputs.Count; i++){
						this.outputs.RemoveAt(0);
					}
					this.outputs.CollectionChanged += this.Outputs_CollectionChanged;
				}
				if(this.outputList.HasItems){
					this.outputList.ScrollIntoView(this.outputList.Items[this.outputList.Items.Count - 1]);
				}
			}));
		}
		
		public Collection<OutputItem> Outputs{
			get{
				return this.outputs;
			}
		}
		
		#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 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
		
		#region Debug
		
		private class OutputPanelTraceListener : TraceListener{
			private MainForm mainForm;
			
			public OutputPanelTraceListener(MainForm mainForm) : base(){
				this.mainForm = mainForm;
			}
			
			private void _Write(string message){
				if(this.NeedIndent){
					this.WriteIndent();
				}
				if(this.mainForm.Outputs.Count > 0){
					OutputItem item = this.mainForm.Outputs[this.mainForm.Outputs.Count - 1];
					item.Text += message;
					item.DateTime = DateTime.Now;
				}else{
					this.mainForm.Outputs.Add(new MessageOutputItem(message));
				}
			}
			
			private void _WriteLine(string message){
				if(this.NeedIndent){
					this.WriteIndent();
				}
				this.mainForm.Outputs.Add(new MessageOutputItem(message));
			}
			
			public override void Write(object o){
				this._Write(o.ToString());
			}
			
			public override void Write(string message){
				this._Write(message);
			}
			
			public override void Write(object o, string category){
				this._Write(category + " : " + o.ToString());
			}
			
			public override void Write(string message, string category){
				this._Write(category + " : " + message);
			}
			
			public override void WriteLine(object o){
				this._Write(o.ToString());
			}
			
			public override void WriteLine(string message){
				this._WriteLine(message);
			}
			
			public override void WriteLine(object o, string category){
				this._WriteLine(category + " : " + o.ToString());
			}
			
			public override void WriteLine(string message, string category){
				this._WriteLine(category + " : " + message);
			}
		}
		
		#endregion
		
		#region プロパティ
		
		
		
		#endregion
	}
}