(function(w) {
  if (w.fastXDM) return;

  var handlers  = {};
  var proxyToServer = {};
  var onEnvLoad = [];
  var env       = {};

  // Key generation
  function genKey() {
    var key = '';
    for (var i = 0; i < 5; i++) {
      key += Math.ceil(Math.random() * 15).toString(16);
    }
    return key;
  }

  function waitFor(obj, prop, func, self,  count) {
    if (obj[prop]) {
      func.apply(self);
    } else {
      count = count || 0;
      if (count < 1000) {
        setTimeout(function() {
          waitFor(obj, prop, func, self, count + 1);
        }, 0);
      }
    }
  }

  function attachScript(url) {
    setTimeout(function() {
      var newScript  = document.createElement('script');
      newScript.type = 'text/javascript';
      newScript.src  = url || w.fastXDM.helperUrl;
      waitFor(document, 'body', function() {
        document.getElementsByTagName('HEAD')[0].appendChild(newScript);
      });
    }, 0);
  }

  function walkVar(value, clean) {
    var newValue;

    switch (typeof value) {
      case 'string':
        if (clean) {
          newValue = value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
        } else {
          newValue = value.replace(/&#039;/g, '\'').replace(/&quot;/g, '"').replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
        }
        break;
      case 'object':
        if (Object.prototype.toString.apply(value) === '[object Array]') {
          newValue = [];
          for (var i = 0, len = value.length; i < len; i++) {
            newValue[i] = walkVar(value[i], clean);
          }
        } else {
          newValue = {};
          for (var k in value) {
            if (Object.hasOwnProperty.call(value, k)) {
              newValue[k] = walkVar(value[k], clean);
            }
          }
        }
        break;
      default:
        newValue = value;
        break;
    }

    return newValue;
  }

  // Env functions
  function getEnv(callback, self) {
    if (env.loaded) {
      callback.apply(self, [env]);
    } else {
      onEnvLoad.push([self, callback]);
    }
  }

  function envLoaded() {
    env.loaded = true;

    for (var i = 0, len = onEnvLoad.length; i < len; i++) {
      onEnvLoad[i][1].apply(onEnvLoad[i][0], [env]);
    }
  }

  function applyMethod(strData, self, origin) {
    getEnv(function(env) {
      var data = env.json.parse(strData);
      var method = data[0];

      if (method) {
        if (
          env.protocol === 'p' &&
          self.options.sameOrigin &&
          origin !== w.origin &&
          !self.isUnsafeMethod(method)
        ) {
          throw Error('Insecure method call.');
        }

        if (!data[1]) data[1] = [];

        for (var i = 0, len = data[1].length; i < len; i++) {
          if (data[1][i] && data[1][i]._func) {
            var funcNum = data[1][i]._func;
            data[1][i] = function() {
              var args = Array.prototype.slice.call(arguments);
              args.unshift('_func' + funcNum);
              self.callMethod.apply(self, args);
            }
          } else if (self.options.safe) {
            data[1][i] = walkVar(data[1][i], true);
          }
        }

        setTimeout(function() {
          if (!self.methods[method]) {
            throw Error('fastXDM: Method ' + method + ' is undefined');
          }
          self.methods[method].apply(self, data[1]);
        }, 0);
      }
    });
  }

  function extend(obj1, obj2) {
    for (var i in obj2) {
      if (obj1[i] && typeof(obj1[i]) === 'object') {
        extend(obj1[i], obj2[i])
      } else {
        obj1[i] = obj2[i];
      }
    }
  }

  // XDM object
  w.fastXDM = {
    _id: 0,
    helperUrl: 'https://vk.com/js/api/xdmHelper.js',

    Server: function(methods, filter, options) {
      this.methods   = methods || {};
      this.filter    = filter;
      this.options   = options || {};
      this.id        = w.fastXDM._id++;
      this.key       = genKey();
      this.frameName = 'fXD' + this.key;
      this.server    = true;

      this.methods['%init%'] = this.methods.__fxdm_i = function() {
        getEnv(function(env) {
          for (var method in this.methods) {
            if (this.methods[method] instanceof w.fastXDM.Server) {
              var proxyFromServer = this;
              var proxyToServer = this.methods[method];

              if (env.protocol === 'p') {
                env.send(proxyFromServer, env.json.stringify(['%proxy%', [method, proxyToServer.key]]));
              }

              this.methods[method] = function() {
                this.callMethod.apply(this, arguments);
              }.bind(proxyToServer)
            }
          }
        }, this);

        w.fastXDM.run(this.id);
        if (this.methods.onInit) {
          this.methods.onInit();
        }
      };

      handlers[this.key] = [applyMethod, this];
    },

    Client: function(methods, options) {
      this.methods = methods || {};
      this.options = options || {};
      this.id      = w.fastXDM._id++;
      this.client  = true;

      w.fastXDM.run(this.id);

      if (window.name.indexOf('fXD') === 0) {
        this.key = window.name.substr(3);
      } else {
        throw Error('Wrong window.name property.');
      }

      this.caller = window.parent;

      if (!proxyToServer[this.key]) {
        proxyToServer[this.key] = {};
      }
      this.methods['%proxy%'] = function(method, key) {
        if (method && key && this.caller.frames['fXD' + key]) {
          proxyToServer[this.key][method] = {
            key: key,
            frame: {
              contentWindow: this.caller.frames['fXD' + key]
            }
          };
        }
      }.bind(this);

      handlers[this.key] = [applyMethod, this];

      w.fastXDM.on('helper', function() {
        w.fastXDM.onClientStart(this);
      }, this);

      getEnv(function(env) {
        env.send(this, env.json.stringify(['%init%']));

        var methods = this.methods;
        setTimeout(function() {
          if (methods.onInit) {
            methods.onInit();
          }
        }, 0);
      }, this);
    },

    onMessage: function(e) {
      var data = e.data;
      if (!data) {
        return false;
      }
      if (typeof data !== 'string' && !(data instanceof String)) {
        return false;
      }

      var key = data.substr(0, 5);
      if (handlers[key]) {
        var self = handlers[key][1];
        if (self && (!self.filter || self.filter(e.origin))) {
          handlers[key][0](data.substr(6), self, e.origin);
        }
      }
    },

    setJSON: function(json) {
      env.json = json;
    },

    getJSON: function(callback) {
      if (!callback) {
        return env.json;
      }

      getEnv(function(env) {
        callback(env.json);
      });
    },

    getProxyToServer: function(key, method) {
      return proxyToServer[key] && proxyToServer[key][method];
    },

    setEnv: function(exEnv) {
      for (var i in exEnv) {
        env[i] = exEnv[i];
      }

      envLoaded();
    },

    _q: {},

    on: function(key, act, self) {
      if (!this._q[key]) this._q[key] = [];

      if (this._q[key] == -1) {
        act.apply(self);
      } else {
        this._q[key].push([act, self]);
      }
    },

    run: function(key) {
      var len = (this._q[key] || []).length;
      for (var i = 0; i < len; i++) {
        this._q[key][i][0].apply(this._q[key][i][1]);
      }

      this._q[key] = -1;
    },

    waitFor: waitFor
  }

  w.fastXDM.Server.prototype.start = function(obj, count) {
    if (obj.contentWindow) {
      this.caller = obj.contentWindow;
      this.frame  = obj;

      w.fastXDM.on('helper', function() {
        w.fastXDM.onServerStart(this);
      }, this);
    } else { // Opera old versions
      var self = this;
      count = count || 0;
      if (count < 50) {
        setTimeout(function() {
          self.start.apply(self, [obj, count + 1]);
        }, 100);
      }
    }
  }

  w.fastXDM.Server.prototype.destroy = function() {
    delete handlers[this.key];
  }

  w.fastXDM.Server.prototype.append = function(obj, options, attrs) {
    var div       = document.createElement('DIV');
    div.innerHTML = '<iframe name="' + this.frameName + '" ' + (attrs || '') + '></iframe>';
    var frame     = div.firstChild;
    var self      = this;

    setTimeout(function() {
      frame.frameBorder = '0';
      if (options) extend(frame, options);
      obj.insertBefore(frame, obj.firstChild);
      self.start(frame);
    }, 0);

    return frame;
  }

  w.fastXDM.Client.prototype.callMethod = w.fastXDM.Server.prototype.callMethod = function() {
    var args   = Array.prototype.slice.call(arguments);
    var method = args.shift();

    for (var i = 0, len = args.length; i < len; i++) {
      if (typeof(args[i]) === 'function') {
        this.funcsCount = (this.funcsCount || 0) + 1;
        var func        = args[i];
        var funcName    = '_func' + this.funcsCount;

        this.methods[funcName] = function() {
          func.apply(this, arguments);
          delete this.methods[funcName];
        }

        args[i] = {_func: this.funcsCount};
      } else if (this.options.safe) {
        args[i] = walkVar(args[i], false);
      }
    }

    waitFor(this, 'caller', function() {
      w.fastXDM.on(this.id, function() {
        getEnv(function(env) {
          var xdm = this;
          var proxyToServer = w.fastXDM.getProxyToServer(this.key, method);

          if (proxyToServer) {
            xdm = proxyToServer;
            method = args.shift();
          }

          env.send(xdm, env.json.stringify([method, args]));
        }, this);
      }, this);
    }, this);
  }

  w.fastXDM.Client.prototype.isUnsafeMethod = w.fastXDM.Server.prototype.isUnsafeMethod = function(method) {
    return ~['%proxy%', '%init%'].indexOf(method) || this.options.unsafeMethods && ~this.options.unsafeMethods.indexOf(method);
  }

  if (w.JSON && typeof(w.JSON) === 'object' && w.JSON.parse && w.JSON.stringify && w.JSON.stringify({a:[1,2,3]}).replace(/ /g, '') === '{"a":[1,2,3]}') {
    env.json = {parse: w.JSON.parse, stringify: w.JSON.stringify};
  } else {
    w.fastXDM._needJSON = true;
  }

  // PostMessage cover
  if (w.postMessage) {
    env.protocol = 'p';
    env.send = function(xdm, strData) {
      var win = (xdm.frame ? xdm.frame.contentWindow : xdm.caller);
      if (win) {
        try {
          win.postMessage(xdm.key + ':' + strData, "*");
        } catch(e) {
          window.postMessage.call(win, xdm.key + ':' + strData, "*");
        }
      }
    }

    if (w.addEventListener) {
      w.addEventListener("message", w.fastXDM.onMessage, false);
    } else {
      w.attachEvent("onmessage", w.fastXDM.onMessage);
    }

    if (w.fastXDM._needJSON) {
      w.fastXDM._onlyJSON = true;
      attachScript();
    } else {
      envLoaded();
    }
  } else {
    attachScript();
  }
})(window);

try{stManager.done('api/xdm.js');}catch(e){}
