using System;
using System.Collections.Generic;
using System.Web.UI;
using System.ComponentModel;
using System.Web.UI.HtmlControls;
using System.Web;
using System.Web.UI.Design;
using Westwind.Tools;
using System.IO;
using System.Diagnostics;

namespace Westwind.Web.Controls
{
    /// <summary>
    /// Class that provides script embedding functionality to ASP.NET pages. 
    /// Features include the ability to use ResolveUrl style src urls to place 
    /// script includes inline (to the control), in the Header or using standard 
    /// ASP.NET ClientScript or ScriptManager. The control can also optimize 
    /// scripts if a .min.js script file is available by using the AllowMinScript 
    /// property on the script.
    /// 
    /// The purpose of this control is to provide Intellisense compatible script 
    /// embedding without requiring the ASP.NET AJAX ScriptManager control since 
    /// that control automatically includes MS AJAX scripts even if none of the MS 
    /// AJAX features are otherwise used.
    /// 
    /// Using markup the control can embed scripts like this:
    /// 
    /// &lt;&lt;code lang="HTML"&gt;&gt;&lt;ww:wwScriptContainer ID="scripts" 
    /// runat="server" RenderMode="Header"&gt;
    ///     &lt;Scripts&gt;
    ///         &lt;Script Src="Scripts/jquery.js"  
    /// Resource="jquery"&gt;&lt;/Script&gt;
    ///         &lt;Script Src="Scripts/ui.core.js"  
    /// AllowMinScript="true"&gt;&lt;/Script&gt;
    ///         &lt;Script Src="Scripts/ui.draggable.js"  &gt;&lt;/Script&gt;
    ///         &lt;Script Src="Scripts/wwscriptlibrary.js"  
    /// Resource="wwscriptlibrary"&gt;&lt;/Script&gt;
    ///         &lt;Script Resource="ControlResources.Menu.js" 
    /// ResourceControl="txtInput" /&gt;
    ///     &lt;/Scripts&gt;
    /// &lt;/ww:wwScriptContainer&gt;
    /// &lt;&lt;/code&gt;&gt;
    /// 
    /// The options on the &lt;Script&gt; tag can be found in the 
    /// <see cref="_2EU16Y1L1">ScriptItem class</see>. Unfortunately due to the requirement for an 
    /// HtmlGeneric control (so Intellisense still works for scripts) there's no 
    /// Intellisense for the properties of Script elements in markup. They do work 
    /// however.
    /// 
    /// Using CodeBehind .NET code, the static Singleton ScriptContainer.Current 
    /// can be used to add scripts (even if no script instance pre-exists):
    /// 
    /// &lt;&lt;code lang="C#"&gt;&gt;
    /// ScriptContainer script = ScriptContainer.Current;
    /// script.AddScript("~/scripts/wwEditable.js","jquery");  // as  known 
    /// resource
    /// script.AddScript("~/scripts/wwEditable.js", true);  // as .min.js
    /// &lt;&lt;/code&gt;&gt;
    /// 
    /// Markup scripts always have precendence over scripts embedded in code in 
    /// terms of rendering order, but you can choose where scripts are rendered to 
    /// individually - Header, Script, Inline or the default of InheritFromControl.
    ///  This allows some control over where scripts are loaded.
    /// <seealso>Class ScriptItem</seealso>
    /// </summary>
    /// <remarks>
    /// Only one instance of this component can exist on the page otherwise an 
    /// exception is thrown.
    /// </remarks>
    [NonVisualControl, Designer(typeof(wwScriptContainerDesigner))]
    [ParseChildren(true, "Scripts")]
    [PersistChildren(false)]
    [DefaultProperty("Scripts")]
    [ToolboxData("<{0}:ScriptContainer runat=\"server\">\r\n\t<Scripts>\r\n\t</Scripts></{0}:ScriptContainer>")]
    public class ScriptContainer : Control
    {
        private const string STR_CONTEXTID = "wwScriptContainer";

        public ScriptContainer()
        {
            this._Scripts = new List<HtmlGenericControl>();
            this._InternalScripts = new List<ScriptItem>();

        }

        

        public override void Dispose()
        {
            base.Dispose();
            if (HttpContext.Current != null)
                HttpContext.Current.Items.Remove(STR_CONTEXTID);
        }

        /// <summary>
        /// Returns a current instance of this control if an instance
        /// is already loaded on the page. Otherwise a new instance is
        /// created, added to the Form and returned.
        /// 
        /// It's important this function is not called too early in the
        /// page cycle - it should not be called before Page.OnInit().
        /// 
        /// This property is the preferred way to get a reference to a
        /// wwScriptContainer control that is either already on a page
        /// or needs to be created. Controls in particular should always
        /// use this property.
        /// </summary>
        public static ScriptContainer Current
        {
            get
            {
                // *** We need a context for this to work!
                if (HttpContext.Current == null)
                    return null;

                Page page = HttpContext.Current.Handler as Page;
                if (page == null)
                    throw new InvalidOperationException("wwScriptContainer.Current only works with Page based handlers."); 

                ScriptContainer ctl = null;

                // *** Retrieve the current instance
                ctl = page.Items[STR_CONTEXTID] as ScriptContainer;
                if (ctl != null)
                    return ctl;
                
                ctl = new ScriptContainer();
                
                page.Form.Controls.Add(ctl);

                return ctl;
            }
        }
 

        /// <summary>
        /// Collection of ScriptItems
        /// </summary>
        [Description("Collection of ScriptItems")]
        [Category("Script")]        
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        public List<HtmlGenericControl> Scripts
        {
            get { return this._Scripts; }
            // *** IMPORTANT: If set method exists error occurs during designer loading.
            // set { _ScriptItems = value; }  
        }
        private List<HtmlGenericControl> _Scripts = null;

        /// <summary>
        /// Internally stored list of parsed or manually added Scripts
        /// </summary>
        protected List<ScriptItem> InternalScripts
        {
            get { return _InternalScripts; }
            //set { _InternalScripts = value; }
        }
        private List<ScriptItem> _InternalScripts = null;

        /// <summary>
        /// Determines where scripts are rendered by default. Defaults to script which renders using ClientScript or ScriptManager.
        /// </summary>
        [Category("Script")]
        [DefaultValue(typeof(ScriptRenderModes),"Script")]
        public ScriptRenderModes RenderMode
        {
            get { return _RenderMode; }
            set { _RenderMode = value; }
        }
        private ScriptRenderModes _RenderMode = ScriptRenderModes.Script;

        /// <summary>
        /// Internally tracked client script proxy that goes either
        /// to MS Ajax ScriptManager if available or to Page.ClientScript 
        /// </summary>
        private ClientScriptProxy scriptProxy = null;



        /// <summary>
        /// Script extension for minimized or packed scripts. Used only
        /// for entries that AllowMinScript=True.
        /// </summary>
        [Description("Script extension for minimized or packed scripts. Used on entries that have AllowMinScript=True")]
        [Category("Script"), DefaultValue(".min.js")]
        public string MinScriptExtension
        {
            get { return _MinScriptExtension; }
            set { _MinScriptExtension = value; }
        }
        private string _MinScriptExtension = ".min.js";


        /// <summary>
        /// Read the HtmlGeneric Script controls and parse them into
        /// Internal scripts at page load time
        /// </summary>
        /// <param name="e"></param>
        protected override void OnInit(EventArgs e)
        {

            // *** Save a Per Request instance in Context.Items so we can retrieve it
            //     generically from code with wwScriptContainer.Current
            if (this.Page.Items.Contains(STR_CONTEXTID))
                throw new InvalidOperationException("Only one ScriptContainer is allowed per page.");

            this.Page.Items[STR_CONTEXTID] = this;

            base.OnInit(e);

            // *** Pick up HtmlGeneric controls parse into Script objects
            // *** and add to Internal scripts for final processing
            foreach (HtmlGenericControl ctl in this.Scripts)
            {
                // *** Take generic control and parse into Script object
                ScriptItem script = this.ParseScriptProperties(ctl);

                // *** Insert into the list at the head of the list before 'manually'
                // *** added script entries
                this.AddScript(script);                
            }
        }

        /// <summary>
        /// Handle writing out scripts to header or 'ASP.NET script body'
        /// </summary>
        /// <param name="e"></param>
        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);

            // *** Use ClientScript Proxy for all script injection except inline
            scriptProxy = ClientScriptProxy.Current;

            // *** Now process all but inline scripts - 
            // *** inline scripts must be rendered in Render()
            foreach (ScriptItem script in this.InternalScripts)
            {                
                // *** Allow inheriting from control which is the default behavior
                if (script.RenderMode == ScriptRenderModes.InheritFromControl)
                    script.RenderMode = this.RenderMode;

                // *** Skip over inline scripts
                if (script.RenderMode == ScriptRenderModes.Inline)
                    continue;

                string src = string.Empty;
                Type ctlType = typeof(ControlResources);
                
                if (!string.IsNullOrEmpty(script.Resource))
                {
                    // *** Allow resource overrides which take priority
                    switch (script.Resource.ToLower())
                    {
                        // *** Handle 'known' Resources 
                        case "wwscriptlibrary":
                            if (script.RenderMode == ScriptRenderModes.Script)
                                ControlResources.LoadwwScriptLibrary(this);
                            else
                                src = scriptProxy.GetClientScriptResourceUrl(this, ctlType, ControlResources.SCRIPTLIBRARY_SCRIPT_RESOURCE);
                            break;
                        case "jquery":
                            if (script.RenderMode == ScriptRenderModes.Script)
                                ControlResources.LoadjQuery(this);
                            else
                                src = scriptProxy.GetClientScriptResourceUrl(this, ctlType, ControlResources.JQUERY_SCRIPT_RESOURCE);
                            break;
                    }
                }
                else if (!string.IsNullOrEmpty(script.ResourceControl))
                {                                  
                    Control ctl = wwWebUtils.FindControlRecursive(this.Page,script.ResourceControl,false);
                    if (ctl != null)
                    {
                        ctlType = ctl.GetType();
                        src = scriptProxy.GetClientScriptResourceUrl(this.Page, ctlType, script.Resource);
                    }
                    break;
                }
                else
                {
                    src = this.ResolveUrl(script.Src);

                    // *** Fix up the URL so we can allow ~/script syntax                    
                    if (script.AllowMinScript && !HttpContext.Current.IsDebuggingEnabled)
                        src = src.ToLower().Replace(".js", this.MinScriptExtension);
                }

                if (!string.IsNullOrEmpty(src))
                {

                    switch (script.RenderMode)
                    {
                        case ScriptRenderModes.Script:
                            scriptProxy.RegisterClientScriptInclude(this.Page, ctlType, src.ToLower(), src);
                            break;
                        case ScriptRenderModes.Header:
                            scriptProxy.RegisterClientScriptIncludeInHeader(this.Page, ctlType, src, null);
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Renders the scripts contained in this control
        /// </summary>
        /// <param name="writer"></param>
        protected override void Render(HtmlTextWriter writer)
        {
            // *** Use ClientScript Proxy for all script injection except inline
            ClientScriptProxy scriptProxy = ClientScriptProxy.Current;

            writer.WriteLine(); // empty line
            
            foreach (ScriptItem script in this.InternalScripts)
            {
                // *** We only need to handle inline here - others have been handled in OnPreRender
                if (script.RenderMode != ScriptRenderModes.Inline)
                    continue;

                // *** Allow inheriting from control which is the default behavior
                if (script.RenderMode == ScriptRenderModes.InheritFromControl)
                    script.RenderMode = this.RenderMode;

                // *** Fix up url to allow ~/ syntax
                string src = this.ResolveUrl(script.Src);

                if (script.AllowMinScript && !HttpContext.Current.IsDebuggingEnabled)
                    src = src.ToLower().Replace(".js", this.MinScriptExtension);                

                writer.Write("<script src=\"" + src + "\" type=\"text/javascript\"></script>\r\n");
            }
        }


        /// <summary>
        /// Adds a script item to the page with full options
        /// </summary>
        /// <param name="scriptUrl">The Url to load script from. Can include ~/ syntax</param>
        /// <param name="renderMode">Determines where the script is rendered</param>
        /// <param name="allowMinScript">Determines if in non-debug mode .min.js files are used</param>
        public void AddScript(ScriptItem script)
        {
            ScriptItem match = null;
                 
            if (!string.IsNullOrEmpty(script.Src))
            {
                // *** Grab just the path
                script.FileId = Path.GetFileName(script.Src).ToLower();

                // *** Check if the script was already added to the page 
                match = this.InternalScripts.Find( // item => item.FileId == script.FileId );
                    delegate(ScriptItem item)
                    {
                        return script.Src == item.Src;
                    });
            }

            if (match == null)                
                this.InternalScripts.Add(script);
            else
                match = script;
        }



        /// <summary>
        /// Adds a script to the collection of embedded scripts
        /// </summary>
        /// <param name="scriptUrl"></param>
        /// <param name="renderMode"></param>
        /// <param name="allowMinScript"></param>
        public void AddScript(string scriptUrl, ScriptRenderModes renderMode, bool allowMinScript)
        {
            ScriptItem script = new ScriptItem();
            script.Src = scriptUrl;
            script.RenderMode = renderMode;
            script.AllowMinScript = allowMinScript;

            this.AddScript(script);
        }

        /// <summary>
        /// Adds a script to the page using the control's rendermode
        /// </summary>
        /// <param name="scriptUrl"></param>
        public void AddScript(string scriptUrl)
        {
            this.AddScript(scriptUrl, ScriptRenderModes.InheritFromControl, false);            
        }
        /// <summary>
        /// Adds a script to the page using the control's rendermode and allows specifying of min.js script files in non-debug mode.
        /// </summary>
        /// <param name="scriptUrl"></param>
        /// <param name="allowMinScript"></param>
        public void AddScript(string scriptUrl, bool allowMinScript)
        {
            this.AddScript(scriptUrl, ScriptRenderModes.InheritFromControl, allowMinScript);
        }

        /// <summary>
        /// Parses HtmlgenericControl attributes into a script object
        /// </summary>
        /// <param name="ctl"></param>
        /// <returns></returns>
        private ScriptItem ParseScriptProperties(HtmlGenericControl ctl)
        {
            ScriptItem script = new ScriptItem();

            script.Src = ctl.Attributes["Src"];

            string val = ctl.Attributes["RenderMode"] ?? "";
            switch (val.ToLower())
            {
                case "header":
                    script.RenderMode = ScriptRenderModes.Header;
                    break;
                case "script":
                    script.RenderMode = ScriptRenderModes.Script;
                    break;
                case "inline":
                    script.RenderMode = ScriptRenderModes.Inline;
                    break;
                default:
                    script.RenderMode = ScriptRenderModes.InheritFromControl;
                    break;
            }

            val = ctl.Attributes["AllowMinScript"] ?? "";
            if (val.ToLower() == "true")
                script.AllowMinScript = true;

            val = ctl.Attributes["Resource"] ?? "";
            if (!string.IsNullOrEmpty(val))
                script.Resource = val;

            val = ctl.Attributes["ResourceControl"] ?? "";
            if (!string.IsNullOrEmpty(val))
                script.ResourceControl = val;

            val = ctl.Attributes["Assembly"] ?? "";
            if (!string.IsNullOrEmpty(val))
                script.ResourceAssembly = val;            

            return script;
        }
    }


    /// <summary>
    /// Individual Script Item used inside of a script container.
    /// This control maps syntax of the standard HTML script tag
    /// and ads a number of additional properties that are specific
    /// to script generation.
    /// 
    /// Note there's no Intellisense on this child item as it is
    /// rendered as an HtmlGenericControl and parsed into this
    /// object. Hence the properties below must be manually typed
    /// in and are not visible to Intellisense.
    /// </summary>
    //[ToolboxItem(false)]
    [ToolboxData("<Script src=\"\" type=\"text/javascript\" />")]
    [DefaultProperty("Src")]
    [Serializable]
    public class ScriptItem
    {
        /// <summary>
        /// The src location of the file. This path can include ~ pathing
        /// </summary>
        [Description("The src location of the file. This path can include ~ pathing")]
        [UrlProperty("*.js")]
        [Category("Script"), DefaultValue("")]
        public string Src
        {
            get { return _Src; }
            set { _Src = value; }
        }
        private string _Src = "";

        /// <summary>
        /// Determines whether script looks for optimized .min.js file in non-debug mode
        /// </summary>
        [Description("Determines whether script looks for optimized .min.js file in non-debug mode")]
        [Category("Script"), DefaultValue(false)]
        public bool AllowMinScript
        {
            get { return _AllowMinScript; }
            set { _AllowMinScript = value; }
        }
        private bool _AllowMinScript = false;

        /// <summary>
        /// Determines how the script is rendered
        /// </summary>
        [Description("Determines how the script is rendered")]
        [Category("Script"), DefaultValue(typeof(ScriptRenderModes), "InheritFromControl")]
        public ScriptRenderModes RenderMode
        {
            get { return _ScriptRenderMode; }
            set { _ScriptRenderMode = value; }
        }
        private ScriptRenderModes _ScriptRenderMode = ScriptRenderModes.InheritFromControl;


        /// <summary>
        /// If specified loads a 'known' resource by name from resources rather
        /// than the script source. This allows using a src url for getting
        /// debugging but using a WebResource for ensuring the latest version
        /// is always used at runtime.
        /// 
        /// Known resources are:
        /// wwscriptlibrary
        /// jquery
        /// calendar
        /// </summary>
        [Description("Specifies a known resource: wwScriptLibrary, jQuery, Calendar")]
        [Category("Script"), DefaultValue(typeof(ScriptRenderModes), "InheritFromControl")]
        public string Resource
        {
            get { return _Resource; }
            set { _Resource = value; }
        }
        private string _Resource = "";

        /// <summary>
        /// A reference to a control in the assembly that holds the resource.
        /// This is used in lieu of a type so you can specify a control that
        /// is in the same assembly. Specify an ID in markup or a reference
        /// in code.
        /// </summary>
        public string ResourceControl
        {
            get { return _ResourceControl; }
            set { _ResourceControl = value; }
        }
        private string _ResourceControl = null;

        
        /// <summary>
        /// The ID for this file when stored in the collection
        /// of script items. Based on the filename of the script
        /// (ie. somescript.js). Used to avoid duplication
        /// </summary>
        protected internal string FileId
        {
            get { return _FileId; }
            set { _FileId = value; }
        }
        private string _FileId = "";


        /// <summary>
        /// Resource assembly - maps to Assembly keyword
        /// </summary>
        public string ResourceAssembly
        {
            get { return _ResourceAssembly; }
            set { _ResourceAssembly = value; }
        }
        private string _ResourceAssembly = "";
    }

    /// <summary>
    /// Determines how scripts are included into the page
    /// </summary>
    public enum ScriptRenderModes
    {

        /// <summary>
        /// Inherits the setting from the control
        /// </summary>
        InheritFromControl,

        /// <summary>
        /// Uses ClientScript or ScriptManager to embed the script include
        /// </summary>
        Script,

        /// <summary>
        /// Renders the script include at the location of the control
        /// </summary>
        Inline,

        /// <summary>
        /// Renders the script include into the header of the page
        /// </summary>
        Header        
    }


    /// <summary>
    /// Control designer used so we get a grey button display instead of the 
    /// default label display for the control.
    /// </summary>
    internal class wwScriptContainerDesigner : ControlDesigner
    {
        public override string GetDesignTimeHtml()
        {
            return base.CreatePlaceHolderDesignTimeHtml();
        }
    }

}
