//------------------------------------------------------------------------------
// <copyright company="Telligent Systems">
//     Copyright (c) Telligent Systems Corporation.  All rights reserved.
// </copyright> 
//------------------------------------------------------------------------------

using System;
using System.Collections.Specialized;
using System.IO;
using System.Threading;
using System.Web;
using System.Collections;
using CommunityServer.Configuration;

namespace CommunityServer.Components 
{
    public delegate bool UrlReWriterDelegate(HttpContext context);

	/// <summary>
	/// The CSContext represents common properties and settings used through out of a Request. All data stored
	/// in the context will be cleared at the end of the request
	/// 
	/// This object should be safe to use outside of a web request, but querystring and other values should be prepopulated
	/// 
	/// Each CS thread must create an instance of the CSContext using one of the Three Create overloads. In some cases, 
	/// the CreateEmptyContext method may be used, but it is NOT recommended.
	/// </summary>
	public sealed class CSContext 
	{

		#region Private Containers

		//Generally expect 10 or less items
		private HybridDictionary _items = new HybridDictionary();
		private NameValueCollection _queryString = null;
		private string _siteUrl = null, _rewrittenUrlName = null;
		private Uri _currentUri;

		string rolesCacheKey = null;

		string authenticationType = "forms";
        
		bool _isUrlReWritten = false;
		bool _isEmpty = false;
		string _rawUrl;

		HttpContext _httpContext = null;
		DateTime requestStartTime = DateTime.Now;
		
		#endregion

		#region Initialize  and cnstr.'s

		/// <summary>
		/// Create/Instatiate items that will vary based on where this object 
		/// is first created
		/// 
		/// We could wire up Path, encoding, etc as necessary
		/// </summary>
		private void Initialize(NameValueCollection qs, Uri uri, string rawUrl, string siteUrl)
		{
			_queryString = qs;
			_siteUrl = siteUrl;
			_currentUri = uri;
			_rawUrl = rawUrl;
			
		}

		/// <summary>
		/// cntr called when no HttpContext is available
		/// </summary>
		private CSContext(Uri uri, string siteUrl)
		{
			Initialize(new NameValueCollection(), uri, uri.ToString(), siteUrl);
		}

		/// <summary>
		/// cnst called when HttpContext is avaiable
		/// </summary>
		/// <param name="context"></param>
		private CSContext(HttpContext context, bool includeQS)
		{
			this._httpContext = context;

            if(includeQS)
            {
			Initialize(new NameValueCollection(context.Request.QueryString), context.Request.Url, context.Request.RawUrl, GetSiteUrl());
		}
            else
            {
                   Initialize(null, context.Request.Url, context.Request.RawUrl, GetSiteUrl());
            }
		}

		#endregion

		#region Create

		/// <summary>
		/// Creates a empty context to be used for thread data storage. This instance of context will not be able to look up a SiteSettings or User.
		/// This method is NOT recommended since most of the API relies on a valid User and SiteSettings objects.
		/// </summary>
		/// <returns></returns>
		public static CSContext CreateEmptyContext()
		{
			CSContext csContext = new CSContext(new Uri("http://CreateEmptyContext"),"http://CreateEmptyContext");
			csContext._isEmpty = true;
			SaveContextToStore(csContext);
			return csContext;
		}

		/// <summary>
		/// Creates a new context and sets the SiteSettings to the specific SettingsID. It's primary usage will be for background threads/tasks/offline.
		/// </summary>
		/// <param name="settingsID">Settings to look up and bind to.</param>
		/// <returns></returns>
		public static CSContext Create(int settingsID)
		{
			SiteSettings siteSettings = SiteSettingsManager.GetSiteSettings(settingsID);
            CSContext csContext;
            try
            {
                csContext = new CSContext(new Uri(siteSettings.SiteDomainUrl), siteSettings.SiteDomainUrl);
            }
		    catch(Exception ex)
		    {
                CSException csException = new CSException(CSExceptionType.UnknownError, string.Format("CSContext failed to load a valid url for settingsid {0}. Url in database {1}. Reason {2}", settingsID, siteSettings.SiteDomainUrl, ex.Message));
                csException.Log(settingsID);
                
		        csContext = new CSContext(new Uri("http://CreateEmptyContext"), "http://CreateEmptyContext");
            }
			csContext.SiteSettings = siteSettings;
			SaveContextToStore(csContext);
			return csContext;
		}


		/// <summary>
		/// Creates a Context instance based on HttpContext. Generally, this
		/// method should be called via Begin_Request in an HttpModule
		/// </summary>
		public static CSContext Create(HttpContext context)
		{
			return Create(context,false);
		}

		/// <summary>
		/// Creates a Context instance based on HttpContext. Generally, this
		/// method should be called via Begin_Request in an HttpModule
		/// </summary>
		public static CSContext Create(HttpContext context, bool isReWritten)
		{
			
			CSContext csContext = new CSContext(context,true);
			csContext.IsUrlReWritten = isReWritten;
			SaveContextToStore(csContext);

			return csContext;
		}

		/// <summary>
        /// On occasion, it may be necessary to use the CSContext during UrlRewriting. By default, it is generally not
        /// in the correct state and should not be accessed. The signature below will enable the CSContext to be created,
        /// saved (so it is available globally) and then perform the UrlRewritng via a delegate. 
        /// 
        /// Except for QueryString values, the full CSContext should be availble during UrlRewriting.
        /// </summary>
        /// <param name="context"></param>
        /// <param name="rewriter"></param>
        /// <returns></returns>
        public static CSContext Create(HttpContext context, UrlReWriterDelegate rewriter)
        {
            CSContext csContext = new CSContext(context,false);
            SaveContextToStore(csContext);
            csContext.IsUrlReWritten = rewriter(context);
            csContext._queryString = new NameValueCollection(context.Request.QueryString);
            return csContext;
        }

		/// <summary>
		/// Creates a Context instance based. If the HttpContext is available it will be used.
		/// Generally this method should be used when working with CS outside of a valid Web Request
		/// </summary>
		public static CSContext Create(Uri uri, string appName)
		{
			CSContext csContext = new CSContext(uri,appName);
			SaveContextToStore(csContext);
			return csContext;
		}
		#endregion

		#region Core Properties
		/// <summary>
		/// Simulates Context.Items and provides a per request/instance storage bag
		/// </summary>
		public IDictionary Items
		{
			get{ return _items;}
		}

		/// <summary>
		/// Provides direct access to the .Items property
		/// </summary>
		public object this[string key]
		{
			get
			{
				return this.Items[key];
			}
			set
			{
				this.Items[key] = value;
			}
		}

		/// <summary>
		/// Allows access to QueryString values
		/// </summary>
		public NameValueCollection QueryString
		{
			get{return _queryString;}
		}

		/// <summary>
		/// Quick check to see if we have a valid web reqeust. Returns false if HttpContext == null
		/// </summary>
		public bool IsWebRequest
		{
			get{ return this.Context != null;}
		}

		public bool IsAuthenticated
		{
			get { return !User.IsAnonymous;}	
		}

		public string AuthenticationType
		{
			get { return authenticationType; }
			set { authenticationType = value.ToLower(); }
		}

        public string RewrittenUrlName
        {
            get{return _rewrittenUrlName;}
            set{_rewrittenUrlName = value;}
        }

		public HttpContext Context 
		{ 
			get 
			{ 
				return _httpContext;
			} 
		}

		public string SiteUrl
		{
			get { return _siteUrl; }
		}

        private Guid _anonUserId = Guid.Empty;
        public Guid AnonymousUserID
        {
            get
            {
                if(_anonUserId == Guid.Empty && this.IsWebRequest)
                    _anonUserId = Users.GetAnonyousUserTrackingID(this.Context);

                return _anonUserId;
            }
        }

		#endregion

		#region Helpers

		public bool IsEmpty
		{
			get { return _isEmpty;}
		}

        public bool IsUserTokenRequest
        {
            get {return !Globals.IsNullorEmpty(this.Token); }
        }

		// *********************************************************************
		//  GetGuidFromQueryString
		//
		/// <summary>
		/// Retrieves a value from the query string and returns it as an int.
		/// </summary>
		// ***********************************************************************/
		public Guid GetGuidFromQueryString(string key) 
		{
			Guid returnValue = Guid.Empty;
			string queryStringValue;

			// Attempt to get the value from the query string
			//
			queryStringValue = QueryString[key];

			// If we didn't find anything, just return
			//
			if (queryStringValue == null)
				return returnValue;

			// Found a value, attempt to conver to integer
			//
			try 
			{

				// Special case if we find a # in the value
				//
				if (queryStringValue.IndexOf("#") > 0)
					queryStringValue = queryStringValue.Substring(0, queryStringValue.IndexOf("#"));

				returnValue = new Guid(queryStringValue);
			} 
			catch {}

			return returnValue;

		}

		// *********************************************************************
		//  GetIntFromQueryString
		//
		/// <summary>
		/// Retrieves a value from the query string and returns it as an int.
		/// </summary>
		// ***********************************************************************/
		public int GetIntFromQueryString(string key, int defaultValue) 
		{
			string queryStringValue;


			// Attempt to get the value from the query string
			//
			queryStringValue = this.QueryString[key];

			// If we didn't find anything, just return
			//
			if (queryStringValue == null)
				return defaultValue;

			// Found a value, attempt to conver to integer
			//
			try 
			{

				// Special case if we find a # in the value
				//
				if (queryStringValue.IndexOf("#") > 0)
					queryStringValue = queryStringValue.Substring(0, queryStringValue.IndexOf("#"));

				defaultValue = Convert.ToInt32(queryStringValue);
			} 
			catch {}

			return defaultValue;

		}

		public string MapPath(string path)
		{
			if(_httpContext != null)
				return _httpContext.Server.MapPath(path);
			else
				// Returns System\WOW for non web // return Directory.GetCurrentDirectory() + path.Replace("/", @"\").Replace("~", "");
				return PhysicalPath(path.Replace("/", Path.DirectorySeparatorChar.ToString()).Replace("~", ""));
		}

		public string PhysicalPath(string path)
		{
			return string.Concat(RootPath().TrimEnd(Path.DirectorySeparatorChar), Path.DirectorySeparatorChar.ToString() , path.TrimStart(Path.DirectorySeparatorChar));
		}

		private string _rootPath = null;

		private string RootPath()
		{
			if(_rootPath == null)
			{
				_rootPath = AppDomain.CurrentDomain.BaseDirectory;
				string dirSep = Path.DirectorySeparatorChar.ToString();

				_rootPath = _rootPath.Replace("/", dirSep);

				string filePath = Config.FilesPath;
				
				if(filePath != null)
				{
					filePath = filePath.Replace("/", dirSep);

					if(filePath.Length > 0 &&  filePath.StartsWith(dirSep) && _rootPath.EndsWith(dirSep))
					{
						_rootPath = _rootPath + filePath.Substring(1);
					}
					else
					{
						_rootPath = _rootPath + filePath;
					}
				}
			}
				return _rootPath;
		}

		private string GetSiteUrl() 
		{
			string appOverride = this.Config.ApplicationOverride;
			if(appOverride != null)
				return appOverride;

			//NOTE: Watch this change. Should be safe, but not tested.
			//Virtualization means urls must be very precise.
			string hostName = _httpContext.Request.Url.Host.Replace("www.",string.Empty);
			string applicationPath = _httpContext.Request.ApplicationPath;

			if(applicationPath.EndsWith("/"))
				applicationPath = applicationPath.Remove(applicationPath.Length-1,1);

			return  hostName + applicationPath;
				
		}
		#endregion

		#region CS Data
		private User _currentUser = null;
		private SiteSettings _currentSettings = null;
		private CSConfiguration _config = null;
		private SiteStatistics _stats = null;
		private Section _section = null;
		private Group _group = null;
		private Post _post = null;
		private int _settingsID = -1;

		/// <summary>
		/// Return the current logged in User. This user value to be anonymous if no user is logged in.
		/// 
		/// If this context is IsEmpty (ie, SiteSettings == null) then null will be returned by default. 
		/// 
		/// This value can be set if necessary
		/// </summary>
		public User User
		{
			get
			{
				//We can not look up a user without SiteSettings
                if(_currentUser == null && SiteSettings != null)
                {
                    _currentUser = TokenUser();
                    if(_currentUser == null)
                        _currentUser= Users.GetUser(true);
                }
				return _currentUser;
			}
			set { _currentUser = value;}
		}

        private User TokenUser()
        {
            if(!Globals.IsNullorEmpty(this.Token) && !Globals.IsNullorEmpty(this.UserName))
            {
                Guid g = new Guid(this.Token);
                User user = Users.GetUser(0,this.UserName,true,true);
                if(user != null && !user.IsAnonymous && user.PublicToken == g)
                    return user;
            }

            return null;
        }

		/// <summary>
		/// Settings for the current site. This by default, it will use the value found in this.SiteUrl. 
		/// 
		/// This value is set by ID when using .Create(int settingsID). If IsEmpty == false, this method will
		/// return false unless the SiteSettings is explicitly set.
		/// </summary>
		public SiteSettings SiteSettings
		{
			get
			{
				//If this context is empty, we can not look up a SiteSetting.
				//We can still set the setting if necessary
				if(_currentSettings == null && !IsEmpty)
					_currentSettings = SiteSettingsManager.GetSiteSettings(this.SiteUrl);

				return _currentSettings;
			}
			set { _currentSettings = value;}
		}

		/// <summary>
		/// Returnt the current configuration found in the communityserver.config file
		/// </summary>
		public CSConfiguration Config
		{
			get
			{
				if(_config == null)
					_config = CSConfiguration.GetConfig();

				return _config;
			}		
		}

		/// <summary>
		/// Return the site statistics for the current SiteSettings. If SiteSettings is null (usually because of using the Emtpy setting)
		/// this propety will return null. In other words, there needs to be a valid SiteSettings first.
		/// </summary>
		public SiteStatistics Statistics 
		{
			get
			{	
				//Same as user. No SiteSettings, no Statistics
				if(_stats == null && SiteSettings != null)
					_stats = SiteStatistics.LoadSiteStatistics(this.SiteSettings.SettingsID,true,3);

				return _stats;

			}
		}

		/// <summary>
		/// Container for the current post. This must be explicitly or it will always bee null
		/// </summary>
		public Post Post
		{
			get{ return _post;}
			set {_post = value;}
		}

		/// <summary>
		/// Container for the current section. This must be explicitly or it will always bee null
		/// </summary>
		public Section Section
		{
			get{ return _section;}
			set {_section = value;}
		}

		/// <summary>
		/// Container for the current group. This must be explicitly or it will always bee null
		/// </summary>
		public Group Group
		{
			get{ return _group;}
			set {_group = value;}
		}

		/// <summary>
		/// Shortcut to SiteSettings.SettingsID. This proprty will return -1 if 
		/// the SiteSettings can not be found (or IsEmtpy == true)
		/// </summary>
		public int SettingsID
		{
			get
			{
				if(_settingsID == -1 && !IsEmpty)
					_settingsID = this.SiteSettings.SettingsID;

				return _settingsID;

			}
		}

		#endregion

		#region Status Properties
		public DateTime RequestStartTime { get { return requestStartTime; } }
		public string RolesCacheKey { get { return rolesCacheKey; } set { rolesCacheKey = value; } }
		public bool IsUrlReWritten { get { return _isUrlReWritten; } set { _isUrlReWritten = value; } }
		public string RawUrl { get { return _rawUrl; } set { _rawUrl = value; } }
		public ApplicationType ApplicationType { get {return Config.AppLocation.CurrentApplicationType;}}
		public Uri CurrentUri
		{
		    get
		    {
		        if(_currentUri == null) 
                    _currentUri = new Uri("http://localhost/cs");

                return _currentUri;

		    } set {_currentUri = value;}
		}
		private string _hostPath = null;
		public string HostPath
		{
			get
			{
				if(_hostPath == null)
				{
					string portInfo = CurrentUri.Port == 80 ? string.Empty : ":" + CurrentUri.Port.ToString();
					_hostPath = string.Format("{0}://{1}{2}",CurrentUri.Scheme,CurrentUri.Host, portInfo);
				}
				return _hostPath;
			}
		}

		private bool _isModal = false;
		public bool IsModal
		{
			get
			{
				return _isModal;
			}
			set
			{
				_isModal = value;
			}
		}

		#endregion

		#region Common QueryString Properties

		#region Private Members

		int sectionID =       -2;
		int categoryID =	-2;
		int messageID =     -2;
		int groupID =  -2;
		int postID =        -2;
		int threadID =      -2;
		int userID =        -2;
		string userName =   null;
		int pageIndex =     -2;
		int blogGroupID =   -2;
		string[] tags = null;
		Guid roleID =        Guid.Empty;
		string queryText =  null;
		string returnUrl =  null;
		string appKey = null;
		string url = null;
		string args = null;
		string postName = null;

		#endregion

		public int MessageID 
		{
			get
			{
				if(messageID == -2)
					messageID = this.GetIntFromQueryString("MessageID", -1);

				return messageID;
			} 
			set {messageID = value;}
		}

//		[Obsolete("ForumID is obsolete, use the SectionID property")]
//		public int ForumID 
//		{
//			get
//			{
//				if(sectionID == -2)
//					sectionID = this.GetIntFromQueryString("ForumID", -1);
//
//				return sectionID;
//			} 
//			set {sectionID = value;}
//		}

		public int SectionID 
		{
			get
			{
                if(sectionID == -2)
                {
                    //Phasing out calls to ForumID. For now, if SectioID fails to be found, we default to ForumID
                    sectionID = GetIntFromQueryString("SectionID", GetIntFromQueryString("ForumID",-1));
                }
				return sectionID;
			} 
			set {sectionID = value;}
		}



		public int GroupID 
		{
			get
			{
				if(groupID == -2)
					groupID = this.GetIntFromQueryString("GroupID", GetIntFromQueryString("ForumGroupID", -1));

				return groupID;
			} 
			set {groupID = value;}
		}


		public int CategoryID 
		{
			get
			{
				if(categoryID == -2)
					categoryID = this.GetIntFromQueryString("CategoryID", -1);

				return categoryID;
			} 
			set {categoryID = value;}
		}

		public int BlogGroupID 
		{
			get
			{
				if(blogGroupID == -2)
					blogGroupID = this.GetIntFromQueryString("BlogGroupID", -1);

				return blogGroupID;
			} 
			set {blogGroupID = value;}
		}


		public int PostID 
		{
			get
			{
				if(postID == -2)
					postID = this.GetIntFromQueryString("PostID", -1);

				return postID;
			} 
			set {postID = value;}
		}

		public string PostName
		{
			get
			{
				if (postName == null)
				{
					postName = QueryString["PostName"];
					if (postName != null)
						postName = Globals.UrlDecodeFileComponent(postName);
				}

				return postName;
			}
			set
			{
				postName = value;
			}
		}

		public int ThreadID 
		{
			get
			{
				if(threadID == -2)
					threadID = this.GetIntFromQueryString("ThreadID", -1);

				return threadID;
			} 
			set {threadID = value;}
		}

		public int UserID 
		{
			get
			{
				if(userID == -2)
					userID = this.GetIntFromQueryString("UserID", -1);

				return userID;
			} 
			set {userID = value;}
		}

		public string UserName 
		{
			get
			{
				if(userName == null)
				{
					userName = QueryString["UserName"];
				}

				return userName;
			}
			set
			{
				userName = value;
			}
		}

        public string Token
        {
            get { return QueryString["Token"];}
        }

		public Guid RoleID 
		{
			get
			{
				if(roleID == Guid.Empty)
					roleID = GetGuidFromQueryString("RoleID");

				return roleID;
			} 
			set {roleID = value;}
		}



		public string QueryText
		{
			get
			{
				if(queryText == null)
					queryText = QueryString["q"];

				return queryText;
			}
			set {queryText = value;}
		}

		public string ReturnUrl
		{
			get
			{
				if(returnUrl == null)
					returnUrl = QueryString["returnUrl"];

				return returnUrl;
			}
			set {returnUrl = value;}
		}

		public string[] Tags
		{
			get
			{
				if (tags == null && !Globals.IsNullorEmpty(QueryString["Tags"]))
				{
					tags = QueryString["Tags"].Split('/');

					for (int i = 0; i < tags.Length; i++)
					{
						tags[i] = Globals.UrlDecodePathComponent(tags[i]);
						if (tags[i].IndexOfAny(new char[] {'<', '>', '"'}) > -1)
							tags[i] = Globals.HtmlEncode(tags[i]);
					}
				}

				return tags;
			}
			set
			{
				tags = value;
			}
		}

		public string Url
		{
			get
			{
				if(url == null)
					url = QueryString["url"];

				return url;
			}
			set {url = value;}
		}

		public string Args
		{
			get
			{
				if(args == null)
					args = QueryString["args"];

				return args;
			}
			set {args = value;}
		}

		public int PageIndex 
		{
			get
			{
				if(pageIndex == -2)
				{
					// load page number from AJAX parameter first
					if (this.Context != null && AjaxManager.IsCallBack && AjaxManager.CallBackMethod == "GetPage" && !Globals.IsNullorEmpty(this.Context.Request.Form["Ajax_CallBackArgument0"]))
						pageIndex = int.Parse(this.Context.Request.Form["Ajax_CallBackArgument0"]) - 1;
					else
					{
						pageIndex = this.GetIntFromQueryString("PageIndex", GetIntFromQueryString("p",-1));
						if(pageIndex != -1)
							pageIndex = pageIndex - 1;
						else if(pageIndex < 0)
							pageIndex = 0;
					}
				}
				return pageIndex;
			} 
			set {pageIndex = value;}
		}


		public string ApplicationKey
		{
			get
			{
				if(appKey == null)
				{
					appKey = ApplicationKeyProvider.Instance().GetKey();
				}
				return appKey;
			}
			set {appKey = value;}
		}


		#endregion

		#region State

		private static readonly string dataKey = "CSContextStore";

		/// <summary>
		/// Returns the current instance of the CSContext from the ThreadData Slot. If one is not found and a valid HttpContext can be found,
		/// it will be used. Otherwise, an exception will be thrown. 
		/// </summary>
		public static CSContext Current 
		{
			get 
			{
                HttpContext httpContext = HttpContext.Current;
                CSContext context = null;
                if(httpContext != null)
                {
                    context = httpContext.Items[dataKey] as CSContext;
                }
                else
                {
                    context = Thread.GetData(GetSlot()) as CSContext;
                }

				if (context == null) 
				{
					
					if(httpContext == null)
						throw new Exception("No CSContext exists in the Current Application. AutoCreate fails since HttpContext.Current is not accessible.");

					context = new CSContext(httpContext,true);
					SaveContextToStore(context);
				}
				return context;
			}
		}

		private static LocalDataStoreSlot GetSlot()
		{
			return Thread.GetNamedDataSlot(dataKey);
		}

		private static void SaveContextToStore(CSContext context)
		{
            if(context.IsWebRequest)
            {
                context.Context.Items[dataKey] = context;
            }
            else
            {
                Thread.SetData(GetSlot(), context);    
            }
			
		}

		public static void Unload()
		{
			Thread.FreeNamedDataSlot(dataKey);
		}
		#endregion
	}
}
