/*
 * general global functions for the YAHOO.applegate namespace.
 * Assumes that the namespace is already created (see global-layout.js)
 */

(function(){
  YAHOO.namespace("applegate.global");

  var Dom = YAHOO.util.Dom,
    Selector = YAHOO.util.Selector,
    global = YAHOO.applegate.global,
    lang = YAHOO.lang;


  /* constants for general keycodes copied from prototype.js */
  /* additional keycodes can be found in YAHOO.util.KeyListener.KEY */
  global.key = {
    BACKSPACE: 8,
    TAB:       9,
    RETURN:   13,
    ESC:      27,
    LEFT:     37,
    UP:       38,
    RIGHT:    39,
    DOWN:     40,
    DELETE:   46,
    HOME:     36,
    END:      35,
    PAGEUP:   33,
    PAGEDOWN: 34,
    INSERT:   45
  };

  /**
   * Are we running in IE 7 or below.
   */
  global.ie = YAHOO.env.ua.ie > 0 && YAHOO.env.ua.ie < 8 ;

  // use to stop iterations in forEach() and selectEach()
  global.$break = {} ;
  // use to force an forEach or selectEach to continue to next iteration
  global.$continue = {} ;

  /**
   * Removes a node from the DOM if attached.
   * @param str | el ref
   * @return returns the HTMLElement
   */
  global.removeNode = function(node){
    var n = Dom.get(node);
    if( n && n.parentNode){
      n.parentNode.removeChild(n);
    }
    return n;
  };

  /**
   * @param elm str|HTMLElement
   * @return map of node attributes
   */
  global.getAttributes = function(elm){
    elm = Dom.get(elm);
    var attributes = new Array();
    Dom.batch(elm.attributes,function(attr){ attributes[attr.name] = attr.value;});
    return attributes;
  };

  /**
   * @return the value of request.contextPath in jsp.
   */
  global.contextPath = function(){
    // note the .value is set in the jsp that includes this js (yuiMain.jsp).
    return global.contextPath.value ;
  };

  /**
   * Builds a YUI tree based on the source html and inserted into
   * the target html element.
   * @param source
   * @param target
   */
  global.buildTree = function(source, target){
    var toolTipIds = new Array();

    // private methods for buildTree()
    var menuLabel = function(elm){
      var labelNode = Selector.query('label',elm, true);
      return labelNode ? labelNode.textContent : '';
    };
    var menuTitle = function(elm){ return elm.getAttribute("title"); };
    var menuOpen = function(elm){ return elm.getAttribute("menuOpen") == "true"; };
    /* recursive function to attach an 'ul' list as a subtree to a given tree*/
    var attachChildNodes = function(listElm, treeNode ){
      var children = Dom.getChildren(listElm);
      // for each child of the ul list.. ie each 'li'
      Dom.batch( children, function(child){
        var childAttr = global.getAttributes(child);
        var node = new YAHOO.widget.TextNode(childAttr,treeNode,childAttr.menuopen);
        if( childAttr.title ){
          toolTipIds.push(node.labelElId);
        }
        var childMenuList = Selector.query('ul',child, true);
        if( childMenuList ){
          attachChildNodes(childMenuList, node);
        }
      }, this, true);
    };
    // actual code
    var sourceRoot = Dom.get(source);
    var tree  = new YAHOO.widget.TreeView(target);
    var treeRoot = tree.getRoot();

    // start the recursive tree build
    attachChildNodes(sourceRoot,treeRoot);

    if( toolTipIds.length > 0){
      new YAHOO.widget.Tooltip("tt", {context:toolTipIds});
    }
    return tree ;
  };

  // general functions ========================================================

  /**
   * returns a function that will call the given method with the given scope.
   * used to force certain event handlers to call methods with the correct this
   * pointer.
   * @param method - method reference
   * @param scope  - value of *this* when method is invoked
   * @return function
   */
  global.bind = function(method, scope){
    return function(){
      return method.apply(scope,arguments);
    };
  };

  /**
   * Applies the given arguments to the method and returns
   * a function that prepends them to the invoke.
   * @see http://en.wikipedia.org/wiki/Currying
   * @param method function to curry
   * @param args... any additional parameters will be prepended (or curried) to the given method
   */
  global.curry = function(method){
    if( arguments.length < 2){
      return method ;
    }
    // arguments isn't a real array, see:
    // http://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Functions_and_function_scope/arguments
    var args = Array.prototype.slice.call(arguments,1);
    return function(){
      var callArgs = [];
      global.addAll(args, callArgs);
      global.addAll(Array.prototype.slice.call(arguments), callArgs);
      return method.apply(this,callArgs);
    };
  };

  global.joinArray = function( array , joiner){
    if( lang.isArray(array)){
      var result = '';
      for(var i=0; i< array.length; i++){
        result = result + (i == 0 ? '' : joiner) + array[i];
      }
      return result;
    }
    return '' + array;
  };

  /**
   * util to allow loading of
   * @param script
   * @param options
   * @param scope
   */
  global.loadScript = function(script, options, scope){
    var scripts = global.loadScript._scripts;
    var invokeOnSuccess = function(){
      if(lang.isFunction(options.onSuccess)){
        options.onSuccess.apply(scope || window);
      }
    };
    if(scripts[script] == global.loadScript.LOADED){
      invokeOnSuccess();
    }else if (scripts[script] == global.loadScript.LOADING ){
      var loadListener = function(loaded_script){
        if( loaded_script == script){
          invokeOnSuccess();
          setTimeout(function(){global.loadScript._scriptLoadedEvent.unsubscribe(loadListener);},0);
        }
      };
    }else{
      scripts[script] = global.loadScript.LOADING ;
      YAHOO.util.Get.script(global.contextPath()
          + '/javascript/'+ script + (YAHOO.env.ua.ie > 0 ? '?' + Math.random() : ''),{
        onSuccess:function(){
          YAHOO.log("loaded " + script,"info", "global.js");
          scripts[script] = global.loadScript.LOADED;
          global.loadScript._scriptLoadedEvent.fire(script);
          invokeOnSuccess();
        }
      });
    }
  };

  global.loadScript._scripts = {};
  global.loadScript.LOADED = "LOADED";
  global.loadScript.LOADING = "LOADING";
  global.loadScript._scriptLoadedEvent = new YAHOO.util.CustomEvent("scriptLoadedEvent");

  /**
   * Insert a bit of HTML into the given node (using innerHTML) and
   * optionally execute any included script elements.
   * @param node required HTMLElement
   * @param html html to insert
   * @param evalscripts optional (default: false) execute any included scripts
   */
  global.insertHtml = function(node, html, evalscripts){
    node.innerHTML = global.stripScripts(html);
    // defer scripts out of this event loop
    if( evalscripts ){
      lang.later(0, window, function(){global.evalScripts(html);});
    }
    return node;
  };

  global.ScriptFragment = '<script[^>]*>([\\S\\s]*?)<\/script>';

  /**
   * Extracts an array of scripts that are contained in the given string.
   * @param 'html' required string
   * @return array of the contents of any script tags in the 'html' string
   */
  global.extractScripts = function(html) {
    // taken from prototype.js and adapted to work without all the prototype goodness
    var matchAll = new RegExp(global.ScriptFragment, 'img');
    var matchOne = new RegExp(global.ScriptFragment, 'im');
    return global.forEach((html.match(matchAll) || []), function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  };
  /**
   * @param html required string
   * @return html minus any script elements that may have been present
   */
  global.stripScripts = function(html) {
    return html.replace(new RegExp(global.ScriptFragment, 'img'), '');
  };

  global.evalScripts = function(html){
    return global.forEach(global.extractScripts(html),function(script){
      eval(script);
    });
  };

  /**
   * Iterates over a collections and returns the maped values.
   * Kinda similar to prototype.js Enumerable.map() and the Dom.batch
   * method, which doesn't allow none HTMLElement iterations.
   * <p>
   * If you need to stop the iteration, throwing global.$break will cause the
   * function to terminate and return the result up to that point.
   * </p>
   * @param a required array or object literal
   * @param method function(V,K) to be invoked on every element of a,
   *         passing the element as the first parameter and the key/index as the second
   * @param scope optional second parameter to pass
   * @return array of the results of 'method'
   */
  global.forEach = function(a, method, scope){
    var s = (lang.isUndefined(scope)) ? window : scope;

    var collection = new Array();

    try{
      for (var k in a) {
        try{
          collection[collection.length] = method.call(s, a[k], k);
        }catch(e){
          if( e != global.$continue ) throw e;
        }
      }
    }catch(e){
      if( e != global.$break ) throw e;
    }

    return collection;
  };

  /**
   * Allows selection of elements from an array or object literal.
   * <p>
   * If you need to stop the iteration, throwing global.$break will cause the
   * function to terminate and return the result up to that point.
   * </p>
   * @param 'a' required array or object literal
   * @param 'method' function(V,K) to be invoked on every element of a,
   *         passing the element as the first parameter and the key/index as the second
   * @param 'scope' optional second parameter to pass
   * @return array of elements where method returned a true value
   */
  global.selectEach = function(a, method, scope) {
    var scope = (lang.isUndefined(scope)) ? window : scope;
    var collection = [];
    try {
      for (var k in a) {
        try {
          if (method.call(scope, a[k], k)) {
            collection[k] = a[k];
          }
        } catch(e) {
          if (e != global.$continue) throw e;
        }
      }
    } catch(e) {
      if (e != global.$break) throw e;
    }
    return collection;
  };

  /**
   * Opposite of the global.selectEach method.
   * Simply returns an array of elements selected from a where 'method' returned
   * false.
   * @param 'a' required array or object literal
   * @param 'method' function(V,K) to be invoked on every element of a,
   *         passing the element as the first parameter and the key/index as the second
   * @param 'scope' optional second parameter to pass
   * @return array of elements where method returned a true value
   */
  global.excludeEach = function(a, method, scope){
    // implement as a simple inverse of selectEach(a,method,scope);
    return global.selectEach(a, function(){ return !method.apply(this,arguments);}, scope);
  };

  /**
   * Iterates over 'source' and adds each element of it to 'target' at the
   * last index position.
   * @param source iterable object (array, object)
   * @param target required Array, elements of source will be appended to the end of target
   */
  global.addAll = function (source , target){
    global.forEach(source, function(v){ target[target.length] = v;});
  };

  /**
   * copyArray, target is optional and if not provided
   * a new array is created and returned with the contents of
   * source.
   * Note keys will be preversed during the copy.
   * e.g.
   * @param source Array to copy
   * @param target optional Array to copy the elements into, unlike addAll elements
   *         will be overwriten in target
   */
  global.copyArray = function(source, target){
    var target = (target ? target : new Array());
    for(var i = 0, len = source.length ; i < len ; i ++ ){
      target[i] = source[i];
    }
    return target;
  };

  /**
   * Iterates over 'source' and returns a normal (indexed) array of
   * values.
   * @param source iterable object
   * @return Array of the values
   */
  global.toArray = function(source){
    return global.forEach(source, function(v){return v;});
  };

  /**
   * Tests the array contents for the existance of a value.
   * Note this will use Array.indexOf if present.
   * Comparsions are done using strict '===' so 'value' must be equal and
   * of the same type as the element in 'array'
   * @param array Array to search
   * @param value to look for.
   */
  global.contains = function(array, value){
    if( lang.isFunction(array.indexOf)){
      return array.indexOf(value) > -1;
    }else{
      for(var i = 0, len = array.length; i < len ; ++i){
        if( array[i] === value) return true;
      }
      return false;
    }
  };

  /**
   * Determines if there are any shared elements
   * between a1 and a2.
   * Comparsions are done using strict '===' so 'value' must be equal and
   * of the same type as the element in 'array'
   * @param a1 required array
   * @param a2 required array
   * @return true if 'a1' shares any elements with 'a2'
   */
  global.containsAny = function(a1, a2){
    for(var i = 0, len = a1.length; i < len ; ++i){
      if( global.contains(a2, a1[i])) return true;
    }
    return false
  };

  /**
   * Flattern a list of arrays into a single array.
   * eg:
   *  global.flattern(["happy","Happy"], ["joy","joy"])
   * will return:
   *  ["happy","Happy","joy","joy"]
   *
   * @param arguments ... list of arrays to flattern
   */
  global.flattern = function(){
    var result = [];
    for(var i= 0 ; i < arguments.length; i++ ){
      global.addAll(arguments[i], result);
    }
    return result;
  }
  /**
   * Kinda like Prototype Element.up, allows upwards navigation in the Dom
   * by CSS selector.
   * Note: only use this if you actually need to use a css selector,
   * Dom.getAncestorByClassName and Dom.getAncestorByTagName cover most
   * usecases.
   *
   * @param oElm element id or string
   * @param sQuery css selector to match
   * @return first HTMLElement that matches the query, starting at oElm
   */
  global.up = function(oElm, sQuery){
    if( lang.isString(oElm)){
      oElm = Dom.get(oElm);
    }
    // end recursaiton if null requested
    if( oElm == null){
      return null;
    }
    // end if found a matching element
    if( Selector.test(oElm, sQuery)){
      return oElm;
    }
    // recurse up the tree
    return global.up(oElm.parentNode, sQuery);
  };

  /**
   * Enchances the given object by curry'ing the methods
   * from the source object and assigning them to the
   * object
   * @param object object to 'enchance'
   * @param source required methods to include
   */
  global.enchanceObject = function(object, source){
    if(object) {
      global.forEach(source, function(f,n){
        if( lang.isUndefined(object[n]) && lang.isFunction(f)){
          object[n] = global.curry(f, object);
        }
      });
    }
    return object;
  };

  global.isEmailValid = function(str){
    // not exactly the most complete email address check ever
    // taken from jQuery.formvalidation plugin.
    var filter  = /^([\w\.])+\@(([\w])+\.)+[a-zA-Z0-9]{2,}$/ ;
    return filter.test(str);
  };

  /**
   * Copied from http://snipplr.com/view/5949/format-humanize-file-byte-size-presentation-in-javascript/
   * @param filesize
   */
  global.formatSize = function(filesize){
    if (filesize >= 1073741824) {
         filesize = global.formatNumber(filesize / 1073741824, 2, '.', '') + ' Gb';
    } else {
      if (filesize >= 1048576) {
           filesize = global.formatNumber(filesize / 1048576, 2, '.', '') + ' Mb';
       } else {
        if (filesize >= 1024) {
          filesize = global.formatNumber(filesize / 1024, 0) + ' Kb';
        } else {
          filesize = global.formatNumber(filesize, 0) + ' bytes';
        };
       };
    };
    return filesize;
  };

  global.formatNumber = function(number, decimals, dec_point, thousands_sep){
    // http://kevin.vanzonneveld.net
     // +   original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
     // +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
     // +     bugfix by: Michael White (http://crestidg.com)
     // +     bugfix by: Benjamin Lupton
     // +     bugfix by: Allan Jensen (http://www.winternet.no)
     // +    revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
     // *     example 1: number_format(1234.5678, 2, '.', '');
     // *     returns 1: 1234.57

     var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
     var d = dec_point == undefined ? "," : dec_point;
     var t = thousands_sep == undefined ? "." : thousands_sep, s = n < 0 ? "-" : "";
     var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0;

     return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
  };

  /**
   * @description This method takes in a form object and returns a sanitized version, with all non-form
   * elements excluded.
   *
   * @param {object} Form object.
   * @return {object} Sanitized form object.
   */
  global.sanitizeForm = function(oForm) {
    var oElement, oName, oDisabled,
            i,len, count = 0;

    var returnForm = {
      nodeName: oForm.nodeName,
      tagName: oForm.tagName,
      localName: oForm.localName,
      elements: []
    };

    // Iterate over the form elements collection to construct the
		// label-value pairs.
		for (i=0,len=oForm.elements.length; i<len; ++i){
			oElement  = oForm.elements[i];
			oDisabled = oElement.disabled;
      oName     = oElement.name;

			// Do not submit fields that are disabled or
			// do not have a name attribute value.
			if(!oDisabled && oName) {
				switch(oElement.type) {
					case 'submit':
            // stub case for submit
          case 'select-one':
            // stub case for select-one
					case 'select-multiple':
            // stub case for select-multiple
					case 'radio':
            // stub case for radio
          case 'text':
            // stub case for text
          case 'hidden':
            // stub case for text
          case 'checkbox':
            returnForm.elements[count] = oElement;
            count++;
          default:
            break;
        }
			}
		}
    return returnForm;
  };

  /**
   * Short cut to YAHOO.util.Date.
   * Note that YAHOO.util.Date is specified in datasource.js.
   * see http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html
   * and http://uk.php.net/strftime for format documentation.
   *
   * Properties of oConfig:
   *  format: format string default is "%d-%b-%Y"
   *  nullValue: string outputted if the date parameter is null
   *  locale: defaults to en-GB
   *
   * @param oDate js date instance, if null '-' is returned
   * @param oConfig config attributes.
   */
  global.formatDate = function(oDate, oConfig){
    oConfig = lang.merge({
      format:"%d-%b-%Y",
      nullValue:"-",
      locale:"en-GB"
    }, oConfig || {});

    if(oDate){
      return YAHOO.util.Date.format(oDate, {format:oConfig.format}, oConfig.locale);
    }
    return oConfig.nullValue;
  };

  /**
   * Parses a string into a Date object.
   * Note this function isn't very clever, it doesn't understand format strings
   * like formatDate and only currently understands 'dd-mmm-yyyy' input.
   *
   * properties of oConfig:
   *  locale: locale to translate the month default is "en-GB"
   *
   * @param sValue string to parse into a Date
   * @param oConfig config attributes
   */
  global.parseDate = function(sValue, oConfig){
    var value = lang.trim(sValue);
    oConfig = lang.merge({
      locale:"en-GB"
    }, oConfig || {});

    if( value.match(/\d{1,2}-\w{3}-\d{4}/)){
      var parts = value.split("-");
      var month = -1;
      var strMonth = parts[1].toLowerCase();
      var localeMonths = YAHOO.util.DateLocale['en-GB'].b; // %b is the token
      for( var i = 0; i< localeMonths.length ; i ++){
        if( localeMonths[i].toLowerCase() === strMonth){
          month = i ;
          break;
        }
      }
      if( month === -1)
        return null;

      var year = parseInt(parts[2],10);
      var day = parseInt(parts[0],10);
      return new Date(year, month, day);
    }else{
      return null;
    }
  }

  /**
   * @param elm HTMLElement|str input element to change to date field
   */
  global.makeDateField = function(elm,zIndex){
    var field = Dom.get(elm);
    var parentNode = field.parentNode ;
    Dom.setStyle(parentNode, "position", "relative");
    if(zIndex!=null){
      Dom.setStyle(parentNode, "zIndex", zIndex);
    }

    var popupLink = document.createElement("A");
    popupLink.href = '#';
    popupLink.id = field.name + '_up';
    popupLink.className = 'calendar-popup';
    popupLink.innerHTML = "...";
    parentNode.appendChild(popupLink);

    var calendarDiv = document.createElement("DIV");
    calendarDiv.className = 'calendar';
    calendarDiv.id = field.name + '_cal';
    Dom.setStyle(calendarDiv, "display", "none");
    Dom.setStyle(calendarDiv, "position", "absolute");
    Dom.setStyle(calendarDiv, "z-index", "1");
    parentNode.appendChild(calendarDiv);

    var caljs = new YAHOO.widget.Calendar(calendarDiv, {
      title:"Choose a date:", close:true
    });

    caljs.selectEvent.subscribe(function(sType,aArgs){
      var dates = aArgs[0];
      var date = dates[0];
      var year = date[0], month = date[1], day = date[2];
      field.value =  global.formatDate(new Date(year,month-1, day), {nullValue:""});
      caljs.hide();
    });
    caljs.render();
    YAHOO.util.Event.on(popupLink,"click", function(event){
      var date = global.parseDate(field.value);
      if( date ){
        caljs.select(date);
        caljs.cfg.setProperty("pagedate", (date.getMonth()+1) + "/" + date.getFullYear());
        caljs.render();
      }else{
        // clear selection
        caljs.select();
      }
      // reflect changes
      caljs.render();
      caljs.show();
      YAHOO.util.Event.stopEvent(event);
    });
  };

  /**
   * Returns either the value as a string or if the value is null it returns
   * defaultValue.
   * The default defaultValue is an empty string.
   * @param value value to return
   * @param defaultValue optional default to use
   */
  global.defaultString = function(value, defaultValue){
    var def = lang.isUndefined(defaultValue) ? "" : defaultValue;
    return value ? "" + value : "" + def ;
  };

  /**
   * Populates a form using the supplied Object
   * @param obj the object to use to populate the form
   * @param form The form to populate
   */
  global.populateForm = function(obj, form){
    var field = null;
    var idFields = ['unverifiedUserDescriptor.companyId', 'unverifiedUserDescriptor.cityTown', 'unverifiedUserDescriptor.county'];
    for (var propertyName in obj) {
      // Loop through all the properties in the object
      if (!(obj[propertyName] instanceof Function)) {
        // Get the input field, or if autocompleter these have 'Input' postfix, else try and get the select
        field = Selector.query('input#' + propertyName, form, true) ||
                Selector.query('input#' + propertyName + 'Input', form, true) ||
                Selector.query('select#' + propertyName, form, true);
        if(field) {
          var text = '';
          if(obj[propertyName] instanceof Object) {
            text = obj[propertyName].id;
          } else {
            text = obj[propertyName];
          }

          if(text !== null) {
            field.value = text;
          } else {
            field.value = (global.contains(idFields, field.name)) ? 0 : '';
          }
        }
      }
    }
  };

  /**
   * Clears all inputs and selects in a form/container.
   * @param form The form/container (div|span etc.) to clear
   */
  global.clearForm = function(form){
    var idFields = ['unverifiedUserDescriptor.companyId', 'unverifiedUserDescriptor.cityTown', 'unverifiedUserDescriptor.county'];
    // Get all the child inputs
    var childrenInputs = form.getElementsByTagName('input');

    global.forEach(childrenInputs, function(c) {
      c.value = (global.contains(idFields, c.name)) ? 0 : '';
    });

    // Get all the child selects
    var childrenSelects = form.getElementsByTagName('select');

    global.forEach(childrenSelects, function(s) {
      // Reset to the first element selected
      s.selectedIndex = 0;
    });
  };

  /**
   * Test all inputs in a form/container for input
   * @param form The form/container (div|span etc.) to clear
   */
  global.hasTextBeenEntered = function(form){
    // Get all the child inputs
    var childrenInputs = form.getElementsByTagName('input');

    var success = false;
    global.forEach(childrenInputs, function(c) {
      if(c.type === 'text' && lang.trim(c.value) != '') {
        success = true;
        throw global.$break;
      }
    });

    return success;
  };

  /**
   * Disable all inputs/selects in a form/container
   * @param form The form/container (div|span etc.)
   */
  global.disableForm = function(form){
    // Get all the child inputs
    var childrenInputs = form.getElementsByTagName('input');

    global.forEach(childrenInputs, function(c) {
      if(c.type === 'text') {
        // This is a bit of a hack as I didn't want to disabled the accountManager field, but it's not very generic,
        // would be best to pass in an array of fields to ignore
        if(c.name !== 'unverifiedUserDescriptor.accountManager') {
          c.disabled = 'disabled';
        }
      }
    });

    // Get all the child selects
    var childrenSelects = form.getElementsByTagName('select');

    global.forEach(childrenSelects, function(s) {
      s.disabled = 'disabled';
    });
  };

  /**
   * Enable all inputs/selects in a form/container
   * @param form The form/container (div|span etc.)
   */
  global.enableForm = function(form){
    // Get all the child inputs
    var childrenInputs = form.getElementsByTagName('input');

    global.forEach(childrenInputs, function(c) {
      if(c.type === 'text') {
        c.disabled = '';
      }
    });

    // Get all the child selects
    var childrenSelects = form.getElementsByTagName('select');

    global.forEach(childrenSelects, function(s) {
      s.disabled = '';
    });
  };

  // ~ generalised functions ==================================================
  global.imposeMaxLength = function(obj, maxLength)
  {
    return (obj.value.length <= maxLength);
  }

})();