using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using FLocal.Core;
using FLocal.Core.DB;
using FLocal.Core.DB.conditions;
using FLocal.Common.actions;

namespace FLocal.Common.dataobjects {
	public class Thread : SqlObject<Thread> {

		public const int FORMALREADMIN = 10000001;

		public class TableSpec : ISqlObjectTableSpec {
			public const string TABLE = "Threads";
			public const string FIELD_ID = "Id";
			public const string FIELD_BOARDID = "BoardId";
			public const string FIELD_FIRSTPOSTID = "FirstPostId";
			public const string FIELD_TOPICSTARTERID = "TopicstarterId";
			public const string FIELD_TITLE = "Title";
			public const string FIELD_LASTPOSTID = "LastPostId";
			public const string FIELD_LASTPOSTDATE = "LastPostDate";
			public const string FIELD_ISANNOUNCEMENT = "IsAnnouncement";
			public const string FIELD_ISLOCKED = "IsLocked";
			public const string FIELD_TOTALPOSTS = "TotalPosts";
			public const string FIELD_TOTALVIEWS = "TotalViews";
			public static readonly TableSpec instance = new TableSpec();
			public string name { get { return TABLE; } }
			public string idName { get { return FIELD_ID; } }
			public void refreshSqlObject(int id) { Refresh(id); }
		}

		public class ReadMarkerTableSpec : ISqlObjectTableSpec {
			public const string TABLE = "Threads_ReadMarkers";
			public const string FIELD_ID = "Id";
			public const string FIELD_THREADID = "ThreadId";
			public const string FIELD_ACCOUNTID = "AccountId";
			public const string FIELD_POSTID = "PostId";
			public static readonly ReadMarkerTableSpec instance = new ReadMarkerTableSpec();
			public string name { get { return TABLE; } }
			public string idName { get { return FIELD_ID; } }
			public void refreshSqlObject(int id) {  }
		}

		protected override ISqlObjectTableSpec table { get { return TableSpec.instance; } }

		private AbstractCondition getReadmarkerSearchCondition(Account account) {
			return new ComplexCondition(
				ConditionsJoinType.AND,
				new ComparisonCondition(
					ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_THREADID),
					ComparisonType.EQUAL,
					this.id.ToString()
				),
				new ComparisonCondition(
					ReadMarkerTableSpec.instance.getColumnSpec(ReadMarkerTableSpec.FIELD_ACCOUNTID),
					ComparisonType.EQUAL,
					account.id.ToString()
				)
			);
		}

		private int _boardId;
		public int boardId {
			get {
				this.LoadIfNotLoaded();
				return this._boardId;
			}
		}
		public Board board {
			get {
				return Board.LoadById(this.boardId);
			}
		}

		private int _firstPostId;
		public int firstPostId {
			get {
				this.LoadIfNotLoaded();
				return this._firstPostId;
			}
		}
		public Post firstPost {
			get {
				return Post.LoadById(this.firstPostId);
			}
		}

		private int _topicstarterId;
		public int topicstarterId {
			get {
				this.LoadIfNotLoaded();
				return this._topicstarterId;
			}
		}
		public User topicstarter {
			get {
				return User.LoadById(this.topicstarterId);
			}
		}

		private string _title;
		public string title {
			get {
				this.LoadIfNotLoaded();
				return this._title;
			}
		}
		public string titleTranslit {
			get {
				return TranslitManager.Translit(this.title).PHPSubstring(0, 50);
			}
		}
		
		private int _lastPostId;
		public int lastPostId {
			get {
				this.LoadIfNotLoaded();
				return this._lastPostId;
			}
		}
		public Post lastPost {
			get {
				return Post.LoadById(this.lastPostId);
			}
		}

		private DateTime _lastPostDate;
		public DateTime lastPostDate {
			get {
				this.LoadIfNotLoaded();
				return this._lastPostDate;
			}
		}

		private bool _isAnnouncement;
		public bool isAnnouncement {
			get {
				this.LoadIfNotLoaded();
				return this._isAnnouncement;
			}
		}

		private bool _isLocked;
		public bool isLocked {
			get {
				this.LoadIfNotLoaded();
				return this._isLocked;
			}
		}

		private int _totalPosts;
		public int totalPosts {
			get {
				this.LoadIfNotLoaded();
				return this._totalPosts;
			}
		}

		private int _totalViews;
		public int totalViews {
			get {
				this.LoadIfNotLoaded();
				return this._totalViews;
			}
		}

		protected override void doFromHash(Dictionary<string, string> data) {
			this._boardId = int.Parse(data[TableSpec.FIELD_BOARDID]);
			this._firstPostId = int.Parse(data[TableSpec.FIELD_FIRSTPOSTID]);
			this._topicstarterId = int.Parse(data[TableSpec.FIELD_TOPICSTARTERID]);
			this._title = data[TableSpec.FIELD_TITLE];
			this._lastPostId = int.Parse(data[TableSpec.FIELD_LASTPOSTID]);
			this._lastPostDate = new DateTime(long.Parse(data[TableSpec.FIELD_LASTPOSTDATE]));
			this._isAnnouncement = FLocal.Core.Util.string2bool(data[TableSpec.FIELD_ISANNOUNCEMENT]);
			this._isLocked = FLocal.Core.Util.string2bool(data[TableSpec.FIELD_ISLOCKED]);
			this._totalPosts = int.Parse(data[TableSpec.FIELD_TOTALPOSTS]);
			this._totalViews = int.Parse(data[TableSpec.FIELD_TOTALVIEWS]);
		}

		public XElement exportToXmlSimpleWithParent(UserContext context) {
			return new XElement("thread",
				new XElement("id", this.id),
				new XElement("name", this.title),
				new XElement("nameTranslit", this.titleTranslit),
				new XElement("parent", this.board.exportToXmlSimpleWithParent(context))
			);
		}

		public XElement exportToXml(UserContext context, params XElement[] additional) {

			if(!context.isPostVisible(this.firstPost)) return null;

			XElement result = new XElement("thread",
				new XElement("id", this.id),
				new XElement("firstPost", this.firstPost.exportToXml(context)),
				new XElement("title", this.title),
				new XElement("lastPostId", this.lastPostId),
				new XElement("lastPostDate", this.lastPostDate.ToXml()),
				new XElement("isAnnouncement", this.isAnnouncement),
				new XElement("isLocked", this.isLocked),
				new XElement("totalPosts", this.totalPosts),
				new XElement("totalViews", this.totalViews),
				context.formatTotalPosts(this.totalPosts)
			);
			if(context.account != null) {
				int lastReadId = this.getLastReadId(context.account);
				result.Add(
					new XElement("afterLastRead", lastReadId + 1),
					new XElement(
						"totalNewPosts",
						Config.instance.mainConnection.GetCountByConditions(
							Post.TableSpec.instance,
							new ComplexCondition(
								ConditionsJoinType.AND,
								new ComparisonCondition(
									Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID),
									ComparisonType.EQUAL,
									this.id.ToString()
								),
								new ComparisonCondition(
									Post.TableSpec.instance.getIdSpec(),
									ComparisonType.GREATERTHAN,
									lastReadId.ToString()
								)
							)
						)
					)
				);
			}
			if(additional.Length > 0) {
				result.Add(additional);
			}
			return result;
		}

		public IEnumerable<Post> getPosts(Diapasone diapasone, bool isAscending) {
			return Post.LoadByIds(
				from stringId in Config.instance.mainConnection.LoadIdsByConditions(
					Post.TableSpec.instance,
					new ComparisonCondition(
						Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID),
						ComparisonType.EQUAL,
						this.id.ToString()
					),
					diapasone,
					new JoinSpec[0],
					new SortSpec[] {
						new SortSpec(
							Post.TableSpec.instance.getIdSpec(),
							isAscending
						),
					}
				) select int.Parse(stringId)
			);
		}

		public void incrementViewsCounter() {
			ChangeSetUtil.ApplyChanges(new AbstractChange[] {
				new UpdateChange(
					TableSpec.instance,
					new Dictionary<string,AbstractFieldValue>() {
						{
							TableSpec.FIELD_TOTALVIEWS,
							new IncrementFieldValue()
						}
					},
					this.id
				)
			});
		}

		public int getLastReadId(Account account) {
			List<string> stringIds = Config.instance.mainConnection.LoadIdsByConditions(
				ReadMarkerTableSpec.instance,
				this.getReadmarkerSearchCondition(account),
				Diapasone.unlimited
			);
			if(stringIds.Count > 1) {
				throw new CriticalException("more than one row");
			}
			if(stringIds.Count < 1) {
				return (this.lastPostId >= FORMALREADMIN) ? 0 : this.lastPostId;
			}
			Dictionary<string, string> data = Config.instance.mainConnection.LoadById(ReadMarkerTableSpec.instance, stringIds[0]);
			if((data[ReadMarkerTableSpec.FIELD_POSTID] == "") || (data[ReadMarkerTableSpec.FIELD_POSTID] == null)) {
				return 0;
			}
			return int.Parse(data[ReadMarkerTableSpec.FIELD_POSTID]);
		}

		public void forceMarkAsRead(Account account, Post maxPost) {
			ChangeSetUtil.ApplyChanges(
				new InsertOrUpdateChange(
					ReadMarkerTableSpec.instance,
					new Dictionary<string,AbstractFieldValue> {
						{ ReadMarkerTableSpec.FIELD_ACCOUNTID, new ScalarFieldValue(account.id.ToString()) },
						{ ReadMarkerTableSpec.FIELD_THREADID, new ScalarFieldValue(this.id.ToString()) },
						{ ReadMarkerTableSpec.FIELD_POSTID, new ScalarFieldValue(maxPost.id.ToString()) },
					},
					new Dictionary<string,AbstractFieldValue> {
						{ ReadMarkerTableSpec.FIELD_POSTID, new IncrementFieldValue(IncrementFieldValue.GREATEST(maxPost.id)) },
					},
					this.getReadmarkerSearchCondition(account)
				)
			);
		}

		public void markAsRead(Account account, Post minPost, Post maxPost) {
			ChangeSetUtil.ApplyChanges(
				new InsertOrUpdateChange(
					ReadMarkerTableSpec.instance,
					new Dictionary<string,AbstractFieldValue> {
						{
							ReadMarkerTableSpec.FIELD_THREADID,
							new ScalarFieldValue(this.id.ToString())
						},
						{
							ReadMarkerTableSpec.FIELD_ACCOUNTID,
							new ScalarFieldValue(account.id.ToString())
						},
						{
							ReadMarkerTableSpec.FIELD_POSTID,
							new ScalarFieldValue(
								(minPost.id <= this.firstPostId)
								?
								maxPost.id.ToString()
								:
								null
							) },
					},
					new Dictionary<string,AbstractFieldValue> {
						{
							ReadMarkerTableSpec.FIELD_POSTID,
							new IncrementFieldValue(
								s => {
									if((s == null) || (s == "")) {
										s = "0"; //workaround
									}
									if(maxPost.id < int.Parse(s)) {
										return (s == "0") ? null : s; //if some newer posts were already read
									}
									long count = Config.instance.mainConnection.GetCountByConditions(
										Post.TableSpec.instance,
										new ComplexCondition(
											ConditionsJoinType.AND,
											new ComparisonCondition(
												Post.TableSpec.instance.getColumnSpec(Post.TableSpec.FIELD_THREADID),
												ComparisonType.EQUAL,
												this.id.ToString()
											),
											new ComparisonCondition(
												Post.TableSpec.instance.getIdSpec(),
												ComparisonType.GREATERTHAN,
												s
											),
											new ComparisonCondition(
												Post.TableSpec.instance.getIdSpec(),
												ComparisonType.LESSTHAN,
												minPost.id.ToString()
											)
										)
									);
									if(count > 0) {
										return (s == "0") ? null : s; //if there are some unread posts earlier than minPost
									} else {
										return maxPost.id.ToString();
									}
								}
							)
						}
					},
					this.getReadmarkerSearchCondition(account)
				)
			);
		}

		internal static KeyValuePair<AbstractChange, IEnumerable<AbstractChange>> getNewPostChanges(Board board, int threadId, Post parentPost, User poster, PostLayer layer, string title, string body, DateTime date, int? forcedPostId) {
			string parentPostId = null;
			if(parentPost != null) parentPostId = parentPost.id.ToString();
			bool isNewThread = (parentPost == null);
			string bodyIntermediate;
			if(forcedPostId.HasValue) {
				//dirty hack
				bodyIntermediate = body;
			} else {
				bodyIntermediate = UBBParser.UBBToIntermediate(body);
			}
			var postInsertData = new Dictionary<string,AbstractFieldValue> {
				{ Post.TableSpec.FIELD_THREADID, new ScalarFieldValue(threadId.ToString()) },
				{ Post.TableSpec.FIELD_PARENTPOSTID, new ScalarFieldValue(parentPostId) },
				{ Post.TableSpec.FIELD_POSTERID, new ScalarFieldValue(poster.id.ToString()) },
				{ Post.TableSpec.FIELD_POSTDATE, new ScalarFieldValue(date.ToUTCString()) },
				{ Post.TableSpec.FIELD_REVISION, new ScalarFieldValue("0") },
				{ Post.TableSpec.FIELD_LASTCHANGEDATE, new ScalarFieldValue(date.ToUTCString()) },
				{ Post.TableSpec.FIELD_LAYERID, new ScalarFieldValue(layer.id.ToString()) },
				{ Post.TableSpec.FIELD_TITLE, new ScalarFieldValue(title) },
				{ Post.TableSpec.FIELD_BODY, new ScalarFieldValue(bodyIntermediate) },
				{ Post.TableSpec.FIELD_TOTALPUNISHMENTS, new ScalarFieldValue("0") },
			};
			if(forcedPostId.HasValue) {
				postInsertData[Post.TableSpec.FIELD_ID] = new ScalarFieldValue(forcedPostId.Value.ToString());
			}
			AbstractChange postInsert = new InsertChange(
				Post.TableSpec.instance,
				postInsertData
			);
			AbstractFieldValue postReference = new ReferenceFieldValue(postInsert);
			AbstractFieldValue postIndexReference = new TwoWayReferenceFieldValue(postInsert, TwoWayReferenceFieldValue.GREATEST);
			AbstractChange revisionInsert = new InsertChange(
				Revision.TableSpec.instance,
				new Dictionary<string,AbstractFieldValue> {
					{ Revision.TableSpec.FIELD_POSTID, postReference },
					{ Revision.TableSpec.FIELD_NUMBER, new ScalarFieldValue("0") },
					{ Revision.TableSpec.FIELD_CHANGEDATE, new ScalarFieldValue(date.ToUTCString()) },
					{ Revision.TableSpec.FIELD_TITLE, new ScalarFieldValue(title) },
					{ Revision.TableSpec.FIELD_BODY, new ScalarFieldValue(body) },
				}
			);
			Dictionary<string, AbstractFieldValue> threadData = new Dictionary<string,AbstractFieldValue> {
				{ Thread.TableSpec.FIELD_LASTPOSTDATE, new ScalarFieldValue(date.ToUTCString()) },
				{ Thread.TableSpec.FIELD_TOTALPOSTS, new IncrementFieldValue() },
				{
					Thread.TableSpec.FIELD_LASTPOSTID,
					postIndexReference
				}
			};
			if(isNewThread) {
				threadData[Thread.TableSpec.FIELD_FIRSTPOSTID] = postReference;
			}
			AbstractChange threadUpdate = new UpdateChange(
				Thread.TableSpec.instance,
				threadData,
				threadId
			);
			AbstractChange userUpdate = new UpdateChange(
				User.TableSpec.instance,
				new Dictionary<string,AbstractFieldValue> {
					{ User.TableSpec.FIELD_TOTALPOSTS, new IncrementFieldValue() },
				},
				poster.id
			);
			List<AbstractChange> changes = new List<AbstractChange> {
				postInsert,
				revisionInsert,
				threadUpdate,
				userUpdate,
			};


			Dictionary<string, AbstractFieldValue> boardData = new Dictionary<string,AbstractFieldValue> {
				{ Board.TableSpec.FIELD_TOTALPOSTS, new IncrementFieldValue() },
				{
					Board.TableSpec.FIELD_LASTPOSTID,
					postIndexReference
				}
			};
			if(isNewThread) {
				boardData[Board.TableSpec.FIELD_TOTALTHREADS] = new IncrementFieldValue();
			}
			int? boardId = board.id;
			do {
				Board _board = Board.LoadById(boardId.Value);
				changes.Add(
					new UpdateChange(
						Board.TableSpec.instance,
						boardData,
						_board.id
					)
				);
				boardId = _board.parentBoardId;
			} while(boardId.HasValue);

			return new KeyValuePair<AbstractChange,IEnumerable<AbstractChange>>(postInsert, changes);
		}

		public readonly object locker = new object();

		public void Synchronized(Action action) {
			lock(this.locker) {
				this.board.Synchronized(action);
			}
		}

	}

}
