using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web;
using System.Reflection;
using Westwind.Tools;

namespace Westwind.Web.Controls
{
    /// <summary>
    /// Provides an easy way for server code to publish strings into client script code.
    /// This object basically provides a mechanism for adding key value pairs and embedding
    /// those values into an object that is hosted on the client.        
    /// </summary>
    public class ScriptVariables
    {

        /// <summary>
        /// Internally holds all script variables declared
        /// </summary>
        Dictionary<string, object> ScriptVars = new Dictionary<string, object>();


        /// <summary>
        /// Internally tracked reference to the Page object
        /// </summary>
        Page Page = null;


        /// <summary>
        /// The name of the object generated in client script code
        /// </summary>
        public string ClientObjectName
        {
            get { return _ClientObjectName; }
            set { _ClientObjectName = value; }
        }
        private string _ClientObjectName = "serverVars";

        /// <summary>
        /// Determines whether the output object script is rendered
        /// automatically as part of Page PreRenderComplete. If false
        /// you can manually call the RenderClientScript() method to
        /// retrieve the script and embed it yourself.
        /// </summary>        
        public bool AutoRenderClientScript
        {
            get { return _AutoRenderClientScript; }
            set { _AutoRenderClientScript = value; }
        }
        private bool _AutoRenderClientScript = true;


        /// <summary>
        /// Determines how updates to the server from the client are performed.
        /// If enabled changes to the client side properties post back to the
        /// server on a full Postback. 
        /// 
        /// Options allow for none, updating the properties only or updating
        /// only the Items collection (use .add() on the client to add new items)
        /// </summary>
        public AllowUpdateTypes UpdateMode
        {
            get { return _UpdateMode; }
            set { _UpdateMode = value; }
        }
        private AllowUpdateTypes _UpdateMode = AllowUpdateTypes.None;
                

        /// <summary>
        /// Internal string of the postback value for the field values
        /// if AllowUpdates is true
        /// </summary>
        private string PostBackValue
        {
            get
            {
                if (this._PostBackValue == null)
                    this._PostBackValue = this.Page.Request.Form["__" + this.ClientObjectName];

                return this._PostBackValue;
            }
        }
        private string _PostBackValue = null;


        /// <summary>
        /// Internal instance of the Json Serializer used to serialize
        /// the object and deserialize the updateable fields
        /// </summary>
        private JSONSerializer JsonSerializer = new JSONSerializer();


        /// <summary>
        /// Internally tracked prefix code
        /// </summary>
        private StringBuilder sbPrefixScriptCode = new StringBuilder();

        private StringBuilder sbPostFixScriptCode = new StringBuilder();


        /// <summary>
        /// Internal counter for submit script embedded
        /// </summary>
        private int SubmitCounter = 0;


        /// <summary>
        /// Full constructor that receives an instance of any control object
        /// and the client name of the generated script object that contains
        /// the specified properties.
        /// </summary>
        /// <param name="control"></param>
        /// <param name="clientObjectName"></param>
        public ScriptVariables(Control control, string clientObjectName)
        {
            if (control == null)
                // Note: this will fail if called from Page Contstructor
                //       ie. wwScriptVariables scripVars = new wwScriptVariables();
                this.Page = HttpContext.Current.Handler as Page;
            else
                this.Page = control.Page;

            if (this.Page == null)
                throw new InvalidOperationException("Could not retrieve a Page instance in wwScriptVariables.\r\n Either provide a Control or Page reference to the wwScriptVariables constructor or intialize the class in OnInit() of the page or later.");

            if (!string.IsNullOrEmpty(clientObjectName))
                this.ClientObjectName = clientObjectName;

            // *** Force RenderClientScript to be called before the page renders
            this.Page.PreRenderComplete += new EventHandler(Page_PreRenderComplete);
        }
        /// <summary>
        /// This constructor only takes an instance of a Control. The name of the
        /// client object will be serverVars.
        /// </summary>
        /// <param name="control"></param>
        public ScriptVariables(Control control)
            : this(control, "serverVars")
        {
        }
        /// <summary>
        /// This constructor can only be called AFTER a page instance has been created.
        /// This means OnInit() or later, but not in the constructor of the page.
        /// 
        /// The name of the client object will be serverVars.
        /// </summary>
        public ScriptVariables()
            : this(null, "serverVars")
        {
        }

        private void Page_PreRenderComplete(object sender, EventArgs e)
        {
            this.RenderClientScript();
        }

        /// <summary>
        /// Adds a property and value to the client side object
        /// to be rendered into JavaScript code. VariableName 
        /// becomes a property on the object and the value will
        /// be properly converted into JavaScript Compatible text.
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="value"></param>
        public void Add(string variableName, object value)
        {
            this.ScriptVars.Add(variableName, value);
        }

        /// <summary>
        /// Adds the dynamic value of a control or any object's property
        /// that is picked up just before rendering. 
        /// 
        /// This allows you to specify a given control or object's value to 
        /// added to the client object with the specified property value 
        /// set on the JavaScript object and have that value be picked
        /// up just before rendering. This is useful so you can do all
        /// client object declarations up front in one place regardless
        /// of where the values are actually set.
        /// 
        /// Dynamic values are retrieved with Reflection so this method
        /// is necessarily slower than direct assignment.
        /// </summary>
        /// <param name="variableName"></param>
        /// <param name="control"></param>
        /// <param name="property"></param>
        public void AddDynamicValue(string variableName, object control, string property)
        {
            // *** Special key syntax: .varName.Property syntax to be picked up by parser
            this.ScriptVars.Add("." + variableName + "." + property, control);
        }

        /// <summary>
        /// Adds all the client ids for a container as properties of the 
        /// client object. The name of the property is the ID + "Id"
        /// Example: txtNameId
        /// 
        /// Note that there's no attempt made to  resolve naming conflicts
        /// in different naming containers. If there's a naming conflict
        /// last one wins.
        /// </summary>
        /// <param name="control"></param>
        public void AddClientIds(Control container, bool recursive)
        {
            foreach (Control control in container.Controls)
            {
                string id = control.ID + "Id";
                if (string.IsNullOrEmpty(id))
                    continue;

                if (!this.ScriptVars.ContainsKey(id ))
                    this.ScriptVars.Add(id, control.ClientID);
                else
                    this.ScriptVars[id] = control.ClientID;

                // *** Drill into the hierarchy
                if (recursive)
                    this.AddClientIds(control, true);
            }
        }

        /// <summary>
        /// Adds all the client ids for a container as properties of the 
        /// client object. The name of the property is the ID + "Id"
        /// Example: txtNameId
        /// This version only retrieves ids for the specified container
        /// level - no hierarchical recursion of controls is performed.
        /// </summary>
        /// <param name="container"></param>
        public void AddClientIds(Control container)
        {
            this.AddClientIds(container, false);
        }

        /// <summary>
        /// Any custom JavaScript code that is to immediately preceed the
        /// client object declaration. This allows setting up of namespaces
        /// if necesary for scoping.
        /// </summary>
        /// <param name="scriptCode"></param>
        public void AddScriptBefore(string scriptCode)
        {
            this.sbPrefixScriptCode.AppendLine(scriptCode);
        }

        /// <summary>
        /// Any custom JavaScript code that is to immediately follow the
        /// client object declaration. This allows setting up of namespaces
        /// if necesary for scoping.
        /// </summary>
        /// <param name="scriptCode"></param>
        public void AddScriptAfter(string scriptCode)
        {
            this.sbPostFixScriptCode.AppendLine(scriptCode);
        }

        /// <summary>
        /// Returns a value that has been updated on the client 
        /// 
        /// Note this method will throw if it is not called
        /// during PostBack or if AllowUpdates is false.
        /// </summary>
        /// <typeparam name="TType"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public TType GetValue<TType>(string key)
        {
            if (this.UpdateMode == AllowUpdateTypes.None || this.UpdateMode == AllowUpdateTypes.ItemsOnly)
                throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true");
            if (!this.Page.IsPostBack)
                throw new InvalidOperationException("GetValue can only be called during postback");

            // *** Get the postback value which is __ + ClientObjectName
            string textValue = this.PostBackValue;
            if (textValue == null)
                return default(TType);

            // *** Retrieve individual Url encoded value from the bufer
            textValue = wwWebUtils.GetUrlEncodedKey(textValue, key);
            if (textValue == null)
                return default(TType);
            
            // *** And deserialize as JSON
            object value = this.JsonSerializer.Deserialize(textValue, typeof(TType));
            
            return (TType) value;
        }

        /// <summary>
        /// Returns a value from the client Items collection
        /// </summary>
        /// <typeparam name="TType"></typeparam>
        /// <param name="key"></param>
        /// <returns></returns>
        public TType GetItemValue<TType>(string key)
        {
            if (this.UpdateMode == AllowUpdateTypes.None || this.UpdateMode == AllowUpdateTypes.PropertiesOnly)
                throw new InvalidOperationException("Can't get values if AllowUpdates is not set to true");
            if (!this.Page.IsPostBack)
                throw new InvalidOperationException("GetValue can only be called during postback");

            // *** Get the postback value which is __ + ClientObjectName
            string textValue = this.PostBackValue;
            if (textValue == null)
                return default(TType);

            // *** Retrieve individual Url encoded value from the bufer
            textValue = wwWebUtils.GetUrlEncodedKey(textValue, "_Items");
            if (textValue == null)
                return default(TType);

            textValue = wwWebUtils.GetUrlEncodedKey(textValue, key);
            if (textValue == null)
                return default(TType);

            // *** And deserialize as JSON
            object value = this.JsonSerializer.Deserialize(textValue, typeof(TType));

            return (TType)value;
        }


        /// <summary>
        /// Explicitly forces the client script to be rendered into the page.
        /// This code is called automatically by the configured event handler that
        /// is hooked to Page_PreRenderComplete
        /// </summary>
        private void RenderClientScript()
        {            
            if (!this.AutoRenderClientScript || this.ScriptVars.Count == 0)
                return;

            ClientScriptProxy scriptProxy = ClientScriptProxy.Current;

            StringBuilder sb = new StringBuilder();

            // *** Check for any prefix code and inject it
            if (this.sbPrefixScriptCode.Length > 0)
                sb.Append(sbPrefixScriptCode.ToString());

            // *** If the name includes a . assignment is made to an existing
            // *** object or namespaced reference - don't create var instance.
            if (!this.ClientObjectName.Contains("."))
                sb.Append("var ");
            
            sb.AppendLine( this.ClientObjectName + " = {");

            // *** We'll serialize single values into the client
            JSONSerializer ser = new JSONSerializer();
            ser.SerializeDateAsString = false;  // use new Date() output

            foreach(KeyValuePair<string,object> entry in this.ScriptVars)
            {
                if (entry.Key.StartsWith("."))
                {
                    // *** It's a dynamic key
                    string[] tokens = entry.Key.Split(new char[1] { '.' }, StringSplitOptions.RemoveEmptyEntries);
                    string varName = tokens[0];
                    string property = tokens[1];


                    object propertyValue = null;
                    if (entry.Value != null)
                        propertyValue = wwUtils.GetPropertyEx(entry.Value, "AdminMode");

                    sb.AppendLine("\t" + varName + ": " + ser.Serialize(propertyValue) + ",");                
                }
                else
                    sb.AppendLine("\t" + entry.Key + ": " + ser.Serialize(entry.Value) + ",");
            }

            if (this.UpdateMode != AllowUpdateTypes.None)
            {
                sb.AppendLine("\t_Items:{},");
                sb.AppendLine("\tadd: function(key,value) { this._Items[key] = value; },");
            }

            // *** Strip off last comma plus CRLF
            if (sb.Length > 0)
                sb.Length -= 3;                

            sb.AppendLine("\r\n};");

            if (this.UpdateMode != AllowUpdateTypes.None)
            {
                // *** Requires wwScritpLibrary
                ControlResources.LoadwwScriptLibrary(this.Page);
                
                scriptProxy.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "submitServerVars", STR_SUBMITSCRIPT, true);

                scriptProxy.RegisterHiddenField(this.Page, "__" + this.ClientObjectName, "");
                
                string script = @"wwEvent.addEventListener(document.forms['{1}'],'submit',function() {{ __submitServerVars({0},'__{0}'); }},true);";
                sb.Append(string.Format(script, this.ClientObjectName, this.Page.Form.ClientID,this.SubmitCounter++));                
            }

            if (this.sbPostFixScriptCode.Length > 0)
                sb.AppendLine(this.sbPostFixScriptCode.ToString());
            
            if (this.Page == null)
                this.Page = HttpContext.Current.Handler as Page;
            
            // *** Use ClientScriptProxy from West Wind Ajax Toolkit to be MS Ajax compatible - otherwise use ClientScript
            scriptProxy.RegisterClientScriptBlock(this.Page, typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
            
            //this.Page.ClientScript.RegisterClientScriptBlock(typeof(ControlResources), "_ClientScriptStrings", sb.ToString(), true);
        }

        const string STR_SUBMITSCRIPT =            
@"
function __submitServerVars(inst,hiddenId)
 {
     var output = '';
     for(var prop in inst)
     {        
        if (prop == '_Items') 
        {
            var out = '';
            for(var p in inst._Items)            
                out += p + '=' + encodeURIComponent(JSON.serialize(inst._Items[p]) ) + '&';
            output += '_Items=' + encodeURIComponent(out) + '&';
        } else
        output += prop + '=' + encodeURIComponent(JSON.serialize(inst[prop])) + '&';
     }  
     $w(hiddenId).value = output;
 };
";
            
    }

    public enum AllowUpdateTypes
    {
        None,
        ItemsOnly,
        PropertiesOnly,
        All
    }
}
