// *** undefine this constant to remove the code that allows for
// *** optional script compression with the wwScriptingModule
// *** when MS Ajax is not available.
#define IncludewwScriptModuleSupport

using System;
using System.Web;
using System.Web.UI;
using System.Reflection;
using System.Text;

namespace Westwind.Web.Controls
{
    /// <summary>
    /// This is a proxy object for the Page.ClientScript and MS Ajax ScriptManager 
    /// object that can operate when MS Ajax when present otherwise falling back to
    ///  Page.ClientScript. Because MS Ajax may not be available accessing the 
    /// methods directly is not possible and we are required to indirectly 
    /// reference client script methods through this class.
    /// 
    /// This class should be invoked at the Control's start up and be used to 
    /// replace all calls Page.ClientScript. Scriptmanager calls are made through 
    /// Reflection indirectly so there's no dependency on the script manager.
    /// 
    /// This class also provides a few additional page injection helpers like the 
    /// abillity to load scripts in the page header (rather than in the body) and 
    /// to use script compression using wwScriptCompressionModule without using MS 
    /// Ajax.
    /// </summary>
    public class ClientScriptProxy
    {
        private const string STR_CONTEXTID = "__ClientScriptProxy";

        private static Type scriptManagerType = null;

        // *** Register proxied methods of ScriptManager
        private static MethodInfo RegisterClientScriptBlockMethod;
        private static MethodInfo RegisterStartupScriptMethod;
        private static MethodInfo RegisterClientScriptIncludeMethod;
        private static MethodInfo RegisterClientScriptResourceMethod;
        private static MethodInfo RegisterHiddenFieldMethod;
        private static MethodInfo GetCurrentMethod;

        //private static MethodInfo RegisterPostBackControlMethod;
        //private static MethodInfo GetWebResourceUrlMethod;


        /// <summary>
        /// Internal global static that gets set when IsMsAjax() is
        /// called. The result is cached once per application so 
        /// we don't have keep making reflection calls for each access
        /// </summary>
        private static bool _IsMsAjax = false;

        /// <summary>
        /// Flag that determines whether check was previously done
        /// </summary>
        private static bool _CheckedForMsAjax = false;

        /// <summary>
        /// Cached value to see whether the script manager is
        /// on the page. This value caches here once per page.
        /// </summary>
        private bool _IsScriptManagerOnPage = false;
        private bool _CheckedForScriptManager = false;

        /// <summary>
        /// Current instance of this class which should always be used to 
        /// access this object. There are no public constructors to
        /// ensure the reference is used as a Singleton to further
        /// ensure that all scripts are written to the same clientscript
        /// manager.
        /// </summary>
        public static ClientScriptProxy Current
        {
            get
            {
                if (HttpContext.Current != null &&
                     HttpContext.Current.Items.Contains(STR_CONTEXTID) )
                    return HttpContext.Current.Items[STR_CONTEXTID] as ClientScriptProxy;

                return new ClientScriptProxy();
            }
        }

        /// <summary>
        /// No public constructor - use ClientScriptProxy.Current to
        /// get an instance to ensure you once have one instance per
        /// page active.
        /// </summary>
        protected ClientScriptProxy()
        {
        }

        /// <summary>
        /// Checks to see if MS Ajax is registered with the current
        /// Web application.
        /// 
        /// Note: Method is static so it can be directly accessed from
        /// anywhere. If you use the IsMsAjax property to check the
        /// value this method fires only once per application.
        /// </summary>
        /// <returns></returns>
        public static bool IsMsAjax()
        {
            if (_CheckedForMsAjax)
                return _IsMsAjax;

            // *** Easiest but we don't want to hardcode the version here
            // scriptManagerType = Type.GetType("System.Web.UI.ScriptManager, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35", false);

            // *** To be safe and compliant we need to look through all loaded assemblies            
            Assembly ScriptAssembly = null; // Assembly.LoadWithPartialName("System.Web.Extensions");
            foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())
            {
                string fn = ass.FullName;
                if (fn.StartsWith("System.Web.Extensions"))
                {
                    ScriptAssembly = ass;
                    break;
                }
            }

            if (ScriptAssembly == null)
                return false;

            scriptManagerType = ScriptAssembly.GetType("System.Web.UI.ScriptManager");

            if (scriptManagerType == null)
            {
                _IsMsAjax = false;
                _CheckedForMsAjax = true;
                return false;

            }

            // *** Method to check for current instance on a page - cache
            // *** since we might call this frequently
            GetCurrentMethod = scriptManagerType.GetMethod("GetCurrent");

            _IsMsAjax = true;
            _CheckedForMsAjax = true;

            return true;
        }

        /// <summary>
        /// Checks to see if a script manager is on the page
        /// </summary>
        /// <param name="control"></param>
        /// <returns></returns>
        public bool IsScriptManagerOnPage(Page page)
        {
            // *** Check is done only once per page
            if (this._CheckedForScriptManager)
                return _IsScriptManagerOnPage;

            // *** Must check whether MS Ajax is available
            // *** at all first. Method sets up scriptManager
            // *** and GetCurrentMethod on success.
            if (!IsMsAjax())
            {
                this._CheckedForScriptManager = true;
                this._IsScriptManagerOnPage = false;
                return false;
            }

            // *** Now check and see if we can get a ref to the script manager
            object sm = GetCurrentMethod.Invoke(null, new object[1] { page });
            if (sm == null)
                this._IsScriptManagerOnPage = false;
            else
                this._IsScriptManagerOnPage = true;

            this._CheckedForScriptManager = true;
            return this._IsScriptManagerOnPage;
        }

        /// <summary>
        /// Returns a WebResource or ScriptResource URL for script resources that are to be
        /// embedded as script includes.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="resourceName"></param>
        public void RegisterClientScriptResource(Control control, Type type, string resourceName)
        {
            if (this.IsScriptManagerOnPage(control.Page))
            {
                // *** NOTE: If MS Ajax is referenced, but no scriptmanager is on the page
                //           script no compression will occur. With a script manager
                //           on the page compression will be handled by MS Ajax.
                if (RegisterClientScriptResourceMethod == null)
                    RegisterClientScriptResourceMethod = scriptManagerType.GetMethod("RegisterClientScriptResource",
                                                                 new Type[3] { typeof(Control), typeof(Type), typeof(string) });

                RegisterClientScriptResourceMethod.Invoke(null, new object[3] { control, type, resourceName });
                return;
            }

#if IncludewwScriptModuleSupport
            // *** If wwScriptCompression Module through Web.config is loaded use it to compress 
            // *** script resources by using wcSC.axd Url the module intercepts
            if (wwScriptCompressionModule.wwScriptCompressionModuleActive)
            {
                string resName = HttpUtility.UrlEncode(resourceName);
                if (type.Assembly == this.GetType().Assembly)

                    RegisterClientScriptInclude(control, type, resourceName, "wwSC.axd?r=" + resName );
                                                //Convert.ToBase64String(Encoding.ASCII.GetBytes()));
                else
                {
                    string typName = HttpUtility.UrlEncode(type.Assembly.FullName);
                    RegisterClientScriptInclude(control, type, resourceName, 
                                                string.Format("wwSC.axd?r={0}&t={1}",resName,typName) );
                                                //Convert.ToBase64String(Encoding.ASCII.GetBytes(resName)) +
                                                //"&t=" +
                                                //Convert.ToBase64String(Encoding.ASCII.GetBytes(type.Assembly.FullName)));
                }
            }
            else
#endif
                // *** Otherwise just embed a script reference into the page
                control.Page.ClientScript.RegisterClientScriptResource(type, resourceName);
        }

        /// <summary>
        /// Registers a client script block in the page.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="key"></param>
        /// <param name="script"></param>
        /// <param name="addScriptTags"></param>
        public void RegisterClientScriptBlock(Control control, Type type, string key, string script, bool addScriptTags)
        {
            if (IsMsAjax())
            {
                if (RegisterClientScriptBlockMethod == null)
                    RegisterClientScriptBlockMethod = scriptManagerType.GetMethod("RegisterClientScriptBlock", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) });

                RegisterClientScriptBlockMethod.Invoke(null, new object[5] { control, type, key, script, addScriptTags });
            }
            else
                control.Page.ClientScript.RegisterClientScriptBlock(type, key, script, addScriptTags);
        }

        /// <summary>
        /// Registers a startup code snippet that gets placed at the bottom of the page
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="key"></param>
        /// <param name="script"></param>
        /// <param name="addStartupTags"></param>
        public void RegisterStartupScript(Control control, Type type, string key, string script, bool addStartupTags)
        {
            if (IsMsAjax())
            {
                if (RegisterStartupScriptMethod == null)
                    RegisterStartupScriptMethod = scriptManagerType.GetMethod("RegisterStartupScript", new Type[5] { typeof(Control), typeof(Type), typeof(string), typeof(string), typeof(bool) });

                RegisterStartupScriptMethod.Invoke(null, new object[5] { control, type, key, script, addStartupTags });
            }
            else
                control.Page.ClientScript.RegisterStartupScript(type, key, script, addStartupTags);

        }

        /// <summary>
        /// Registers a script include tag into the page for an external script url
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="key"></param>
        /// <param name="url"></param>
        public void RegisterClientScriptInclude(Control control, Type type, string key, string url)
        {
            if (IsMsAjax())
            {
                if (RegisterClientScriptIncludeMethod == null)
                    RegisterClientScriptIncludeMethod = scriptManagerType.GetMethod("RegisterClientScriptInclude", new Type[4] { typeof(Control), typeof(Type), typeof(string), typeof(string) });

                RegisterClientScriptIncludeMethod.Invoke(null, new object[4] { control, type, key, url });
            }
            else
                control.Page.ClientScript.RegisterClientScriptInclude(type, key, url);
        }

        /// <summary>
        /// Returns a WebResource URL for non script resources
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="resourceName"></param>
        /// <returns></returns>
        public string GetWebResourceUrl(Control control, Type type, string resourceName)
        {
            return control.Page.ClientScript.GetWebResourceUrl(type, resourceName);
        }

        /// <summary>
        /// Works like GetWebResourceUrl but can be used with javascript resources
        /// to allow using of resource compression (if the module is loaded).
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="resourceName"></param>
        /// <returns></returns>
        public string GetClientScriptResourceUrl(Control control, Type type, string resourceName)
        {

            #if IncludewwScriptModuleSupport

            // *** If wwScriptCompression Module through Web.config is loaded use it to compress 
            // *** script resources by using wcSC.axd Url the module intercepts
            if (wwScriptCompressionModule.wwScriptCompressionModuleActive) 
            {
                string url = "wwSC.axd?r=" + Convert.ToBase64String(Encoding.ASCII.GetBytes(resourceName));
                if (type.Assembly != this.GetType().Assembly)
                    url += "&t=" + Convert.ToBase64String(Encoding.ASCII.GetBytes(type.FullName));
                
                return url;
            }

            
            #endif

            return control.Page.ClientScript.GetWebResourceUrl(type, resourceName);
        }

        /// <summary>
        /// Inserts a client script resource into the Html header of the page rather 
        /// than into the body as RegisterClientScriptInclude does.
        /// 
        /// Scripts references are embedded at the bottom of the Html Header after
        /// any manually added header scripts.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="resourceName"></param>
        /// <param name="comment"></param>
        public void RegisterClientScriptResourceInHeader(Control control, Type type,  string resourceName, string comment)
        {
            if (control.Page.Header == null)
            {
                this.RegisterClientScriptResource(control, type, resourceName);
                return;
            }

            // *** Keep duplicates from getting written
            const string identifier = "headerscript_";
            if (HttpContext.Current.Items.Contains(identifier + resourceName))
                return;
            else
                HttpContext.Current.Items.Add(identifier + resourceName, string.Empty);

            object val = HttpContext.Current.Items["__ScriptResourceIndex"];
            int index = 0;
            if (val != null)
                index = (int)val;

            // *** Retrieve the Resource URL adjusted for MS Ajax, wwScriptCompression or stock ClientScript
            string script = GetClientScriptResourceUrl(control.Page, typeof(ControlResources), resourceName);
            
            // *** Embed in header
            StringBuilder sb = new StringBuilder(200);
            if (comment != null)
                sb.AppendLine("<!-- " + comment + " -->");

            sb.AppendLine(@"<script src=""" + script + @""" type=""text/javascript""></script>");

            control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));
            
            index++;
            HttpContext.Current.Items["__ScriptResourceIndex"] = index;
        }

        /// <summary>
        /// Registers a client script reference in the header instead of inside the document
        /// before the form tag. 
        /// 
        /// The script tag is embedded at the bottom of the HTML header.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="type"></param>
        /// <param name="Url"></param>
        /// <param name="comment"></param>
        public void RegisterClientScriptIncludeInHeader(Control control, Type type, string Url, string comment)
        {
            if (control.Page.Header == null)
            {
                this.RegisterClientScriptInclude(control, type, Url, Url);
                return;
            }

            // *** Keep duplicates from getting written
            const string identifier = "headerscript_";
            if (HttpContext.Current.Items.Contains(identifier + Url.ToLower()))
                return;
            else
                HttpContext.Current.Items.Add(identifier + Url.ToLower(), string.Empty);

            // *** Retrieve script index in header
            object val = HttpContext.Current.Items["__ScriptResourceIndex"];
            int index = 0;
            if (val != null)
                index = (int)val;

            // *** Embed in header
            StringBuilder sb = new StringBuilder(200);
            if (comment != null)
                sb.AppendLine("<!-- " + comment + " -->");

            sb.AppendLine(@"<script src=""" + Url + @""" type=""text/javascript""></script>");

            control.Page.Header.Controls.AddAt(index, new LiteralControl(sb.ToString()));

            index++;
            HttpContext.Current.Items["__ScriptResourceIndex"] = index;
        }



        /// <summary>
        /// Injects a hidden field into the page
        /// </summary>
        /// <param name="control"></param>
        /// <param name="hiddenFieldName"></param>
        /// <param name="hiddenFieldInitialValue"></param>
        public void RegisterHiddenField(Control control, string hiddenFieldName, string hiddenFieldInitialValue)
        {
            if (IsMsAjax())
            {
                if (RegisterHiddenFieldMethod == null)
                    RegisterHiddenFieldMethod = scriptManagerType.GetMethod("RegisterHiddenField", new Type[3] { typeof(Control), typeof(string), typeof(string) });

                RegisterHiddenFieldMethod.Invoke(null, new object[3] { control, hiddenFieldName, hiddenFieldInitialValue });
            }
            else
                control.Page.ClientScript.RegisterHiddenField(hiddenFieldName, hiddenFieldInitialValue);
        }

    }
}