/*!
 * Webogram v0.3.9 - messaging web application for MTProto
 * https://github.com/zhukov/webogram
 * Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
 * https://github.com/zhukov/webogram/blob/master/LICENSE
 */

angular.module('izhukov.mtproto', ['izhukov.utils'])

.factory('MtpDcConfigurator', function () {
  var sslSubdomains = ['pluto', 'venus', 'aurora', 'vesta', 'flora'];

  var dcOptions = Config.Modes.test
    ? [
      {id: 1, host: '149.154.175.10', port: 80},
      {id: 2, host: '149.154.167.40', port: 80},
      {id: 3, host: '174.140.142.5', port: 80}
    ]
    : [
      {id: 1, host: '149.154.175.50', port: 80},
      {id: 2, host: '149.154.167.51', port: 80},
      {id: 3, host: '174.140.142.6', port: 80},
      {id: 4, host: '149.154.167.91', port: 80},
      {id: 5, host: '149.154.171.5',   port: 80}
    ];

  var chosenServers = {};

  function chooseServer(dcID, upload) {
    if (chosenServers[dcID] === undefined) {
      var chosenServer = false,
          i, dcOption;

      if (Config.Modes.ssl) {
        var subdomain = sslSubdomains[dcID - 1] + (upload ? '-1' : '');
        var path = Config.Modes.test ? 'apiw_test1' : 'apiw1';
        chosenServer = 'https://' + subdomain + '.web.telegram.org/' + path;
        return chosenServer;
      }

      for (i = 0; i < dcOptions.length; i++) {
        dcOption = dcOptions[i];
        if (dcOption.id == dcID) {
          chosenServer = 'http://' + dcOption.host + (dcOption.port != 80 ? ':' + dcOption.port : '') + '/apiw1';
          break;
        }
      }
      chosenServers[dcID] = chosenServer;
    }

    return chosenServers[dcID];
  }

  return {
    chooseServer: chooseServer
  };
})

.factory('MtpRsaKeysManager', function () {

/**
*  Server public key, obtained from here: https://core.telegram.org/api/obtaining_api_id
*
* -----BEGIN RSA PUBLIC KEY-----
* MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
* lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
* an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
* Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
* 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
* Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
* -----END RSA PUBLIC KEY-----
*/

  var publisKeysHex = [{
    modulus: 'c150023e2f70db7985ded064759cfecf0af328e69a41daf4d6f01b538135a6f91f8f8b2a0ec9ba9720ce352efcf6c5680ffc424bd634864902de0b4bd6d49f4e580230e3ae97d95c8b19442b3c0a10d8f5633fecedd6926a7f6dab0ddb7d457f9ea81b8465fcd6fffeed114011df91c059caedaf97625f6c96ecc74725556934ef781d866b34f011fce4d835a090196e9a5f0e4449af7eb697ddb9076494ca5f81104a305b6dd27665722c46b60e5df680fb16b210607ef217652e60236c255f6a28315f4083a96791d7214bf64c1df4fd0db1944fb26a2a57031b32eee64ad15a8ba68885cde74a5bfc920f6abf59ba5c75506373e7130f9042da922179251f',
    exponent: '010001'
  }];

  var publicKeysParsed = {};
  var prepared = false;

  function prepareRsaKeys () {
    if (prepared) {
      return;
    }

    for (var i = 0; i < publisKeysHex.length; i++) {
      var keyParsed = publisKeysHex[i];

      var RSAPublicKey = new TLSerialization();
      RSAPublicKey.storeBytes(bytesFromHex(keyParsed.modulus), 'n');
      RSAPublicKey.storeBytes(bytesFromHex(keyParsed.exponent), 'e');

      var buffer = RSAPublicKey.getBuffer();

      var fingerprintBytes = sha1BytesSync(buffer).slice(-8);
      fingerprintBytes.reverse();

      publicKeysParsed[bytesToHex(fingerprintBytes)] = {
        modulus: keyParsed.modulus,
        exponent: keyParsed.exponent
      };
    }

    prepared = true;
  };

  function selectRsaKeyByFingerPrint (fingerprints) {
    prepareRsaKeys();

    var fingerprintHex, foundKey, i;
    for (i = 0; i < fingerprints.length; i++) {
      fingerprintHex = bigStringInt(fingerprints[i]).toString(16);
      if (foundKey = publicKeysParsed[fingerprintHex]) {
        return angular.extend({fingerprint: fingerprints[i]}, foundKey);
      }
    }

    return false;
  };

  return {
    prepare: prepareRsaKeys,
    select: selectRsaKeyByFingerPrint
  };
})

.service('MtpSecureRandom', function ($window) {
  $($window).on('click keydown', rng_seed_time);
  return new SecureRandom();
})

.factory('MtpTimeManager', function (Storage) {
  var lastMessageID = [0, 0],
      timeOffset = 0;

  Storage.get('server_time_offset').then(function (to) {
    if (to) {
      timeOffset = to;
    }
  });

  function generateMessageID () {
    var timeTicks = tsNow(),
        timeSec   = Math.floor(timeTicks / 1000) + timeOffset,
        timeMSec  = timeTicks % 1000,
        random    = nextRandomInt(0xFFFF);

    var messageID = [timeSec, (timeMSec << 21) | (random << 3) | 4];
    if (lastMessageID[0] > messageID[0] ||
        lastMessageID[0] == messageID[0] && lastMessageID[1] >= messageID[1]) {

      messageID = [lastMessageID[0], lastMessageID[1] + 4];
    }

    lastMessageID = messageID;

    // console.log('generated msg id', messageID, timeOffset);

    return longFromInts(messageID[0], messageID[1]);
  };

  function applyServerTime (serverTime, localTime) {
    var newTimeOffset = serverTime - Math.floor((localTime || tsNow()) / 1000),
        changed = Math.abs(timeOffset - newTimeOffset) > 10;
    Storage.set({server_time_offset: newTimeOffset});

    lastMessageID = [0, 0];
    timeOffset = newTimeOffset;
    console.log(dT(), 'Apply server time', serverTime, localTime, newTimeOffset, changed);

    return changed;
  };

  return {
    generateID: generateMessageID,
    applyServerTime: applyServerTime,
  };
})

.factory('MtpAuthorizer', function (MtpDcConfigurator, MtpRsaKeysManager, MtpSecureRandom, MtpTimeManager, CryptoWorker, $http, $q, $timeout) {

  var chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/),
      chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false,
      xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30);

  delete $http.defaults.headers.post['Content-Type'];
  delete $http.defaults.headers.common['Accept'];

  function mtpSendPlainRequest (dcID, requestBuffer) {
    var requestLength = requestBuffer.byteLength,
        requestArray  = new Int32Array(requestBuffer);

    var header = new TLSerialization();
    header.storeLongP(0, 0, 'auth_key_id'); // Auth key
    header.storeLong(MtpTimeManager.generateID(), 'msg_id'); // Msg_id
    header.storeInt(requestLength, 'request_length');

    var headerBuffer = header.getBuffer(),
        headerArray  = new Int32Array(headerBuffer),
        headerLength = headerBuffer.byteLength;

    var resultBuffer = new ArrayBuffer(headerLength + requestLength),
        resultArray  = new Int32Array(resultBuffer);

    resultArray.set(headerArray);
    resultArray.set(requestArray, headerArray.length);

    var requestData = xhrSendBuffer ? resultBuffer : resultArray,
        requestPromise;
    try {
      requestPromise =  $http.post(MtpDcConfigurator.chooseServer(dcID), requestData, {
        responseType: 'arraybuffer',
        transformRequest: null
      });
    } catch (e) {
      requestPromise = $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE', originalError: e});
    }
    return requestPromise.then(
      function (result) {
        if (!result.data || !result.data.byteLength) {
          return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE'});
        }

        try {

          var deserializer = new TLDeserialization(result.data, {mtproto: true});
          var auth_key_id = deserializer.fetchLong('auth_key_id');
          var msg_id      = deserializer.fetchLong('msg_id');
          var msg_len     = deserializer.fetchInt('msg_len');

        } catch (e) {
          return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE', originalError: e});
        }

        return deserializer;
      },
      function (error) {
        if (!error.message && !error.type) {
          error = {code: 406, type: 'NETWORK_BAD_REQUEST', originalError: error};
        }
        return $q.reject(error);
      }
    );
  };

  function mtpSendReqPQ (auth) {
    var deferred = auth.deferred;

    var request = new TLSerialization({mtproto: true});

    request.storeMethod('req_pq', {nonce: auth.nonce});

    console.log(dT(), 'Send req_pq', bytesToHex(auth.nonce));
    mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
      var response = deserializer.fetchObject('ResPQ');

      if (response._ != 'resPQ') {
        throw new Error('resPQ response invalid: ' + response._);
      }

      if (!bytesCmp (auth.nonce, response.nonce)) {
        throw new Error('resPQ nonce mismatch');
      }

      auth.serverNonce = response.server_nonce;
      auth.pq = response.pq;
      auth.fingerprints = response.server_public_key_fingerprints;

      console.log(dT(), 'Got ResPQ', bytesToHex(auth.serverNonce), bytesToHex(auth.pq), auth.fingerprints);

      auth.publicKey = MtpRsaKeysManager.select(auth.fingerprints);

      if (!auth.publicKey) {
        throw new Error('No public key found');
      }

      console.log(dT(), 'PQ factorization start', auth.pq);
      CryptoWorker.factorize(auth.pq).then(function (pAndQ) {
        auth.p = pAndQ[0];
        auth.q = pAndQ[1];
        console.log(dT(), 'PQ factorization done', pAndQ[2]);
        mtpSendReqDhParams(auth);
      }, function (error) {
        console.log('Worker error', error, error.stack);
        deferred.reject(error);
      });
    }, function (error) {
      console.log(dT(), 'req_pq error', error.message);
      deferred.reject(error);
    });

    $timeout(function () {
      MtpRsaKeysManager.prepare();
    });
  };

  function mtpSendReqDhParams (auth) {

    var deferred = auth.deferred;

    auth.newNonce = new Array(32);
    MtpSecureRandom.nextBytes(auth.newNonce);

    var data = new TLSerialization({mtproto: true});
    data.storeObject({
      _: 'p_q_inner_data',
      pq: auth.pq,
      p: auth.p,
      q: auth.q,
      nonce: auth.nonce,
      server_nonce: auth.serverNonce,
      new_nonce: auth.newNonce
    }, 'P_Q_inner_data', 'DECRYPTED_DATA');

    var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());

    var request = new TLSerialization({mtproto: true});
    request.storeMethod('req_DH_params', {
      nonce: auth.nonce,
      server_nonce: auth.serverNonce,
      p: auth.p,
      q: auth.q,
      public_key_fingerprint: auth.publicKey.fingerprint,
      encrypted_data: rsaEncrypt(auth.publicKey, dataWithHash)
    });

    console.log(dT(), 'Send req_DH_params');
    mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
      var response = deserializer.fetchObject('Server_DH_Params', 'RESPONSE');

      if (response._ != 'server_DH_params_fail' && response._ != 'server_DH_params_ok') {
        deferred.reject(new Error('Server_DH_Params response invalid: ' + response._));
        return false;
      }

      if (!bytesCmp (auth.nonce, response.nonce)) {
        deferred.reject(new Error('Server_DH_Params nonce mismatch'));
        return false;
      }

      if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
        deferred.reject(new Error('Server_DH_Params server_nonce mismatch'));
        return false;
      }

      if (response._ == 'server_DH_params_fail') {
        var newNonceHash = sha1BytesSync(auth.newNonce).slice(-16);
        if (!bytesCmp (newNonceHash, response.new_nonce_hash)) {
          deferred.reject(new Error('server_DH_params_fail new_nonce_hash mismatch'));
          return false;
        }
        deferred.reject(new Error('server_DH_params_fail'));
        return false;
      }

      try {
        mtpDecryptServerDhDataAnswer(auth, response.encrypted_answer);
      } catch (e) {
        deferred.reject(e);
        return false;
      }

      mtpSendSetClientDhParams(auth);
    }, function (error) {
      deferred.reject(error);
    });
  };

  function mtpDecryptServerDhDataAnswer (auth, encryptedAnswer) {
    auth.localTime = tsNow();

    auth.tmpAesKey = sha1BytesSync(auth.newNonce.concat(auth.serverNonce)).concat(sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(0, 12));
    auth.tmpAesIv = sha1BytesSync(auth.serverNonce.concat(auth.newNonce)).slice(12).concat(sha1BytesSync([].concat(auth.newNonce, auth.newNonce)), auth.newNonce.slice(0, 4));

    var answerWithHash = aesDecryptSync(encryptedAnswer, auth.tmpAesKey, auth.tmpAesIv);

    var hash = answerWithHash.slice(0, 20);
    var answerWithPadding = answerWithHash.slice(20);
    var buffer = bytesToArrayBuffer(answerWithPadding);

    var deserializer = new TLDeserialization(buffer, {mtproto: true});
    var response = deserializer.fetchObject('Server_DH_inner_data');

    if (response._ != 'server_DH_inner_data') {
      throw new Error('server_DH_inner_data response invalid: ' + constructor);
    }

    if (!bytesCmp (auth.nonce, response.nonce)) {
      throw new Error('server_DH_inner_data nonce mismatch');
    }

    if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
      throw new Error('server_DH_inner_data serverNonce mismatch');
    }

    console.log(dT(), 'Done decrypting answer');
    auth.g          = response.g;
    auth.dhPrime    = response.dh_prime;
    auth.gA         = response.g_a;
    auth.serverTime = response.server_time;
    auth.retry      = 0;

    var offset = deserializer.getOffset();

    if (!bytesCmp(hash, sha1BytesSync(answerWithPadding.slice(0, offset)))) {
      throw new Error('server_DH_inner_data SHA1-hash mismatch');
    }

    MtpTimeManager.applyServerTime(auth.serverTime, auth.localTime);
  };

  function mtpSendSetClientDhParams(auth) {
    var deferred = auth.deferred,
        gBytes = bytesFromHex(auth.g.toString(16));

    auth.b = new Array(256);
    MtpSecureRandom.nextBytes(auth.b);

    CryptoWorker.modPow(gBytes, auth.b, auth.dhPrime).then(function (gB) {

      var data = new TLSerialization({mtproto: true});
      data.storeObject({
        _: 'client_DH_inner_data',
        nonce: auth.nonce,
        server_nonce: auth.serverNonce,
        retry_id: [0, auth.retry++],
        g_b: gB,
      }, 'Client_DH_Inner_Data');

      var dataWithHash = sha1BytesSync(data.getBuffer()).concat(data.getBytes());

      var encryptedData = aesEncryptSync(dataWithHash, auth.tmpAesKey, auth.tmpAesIv);

      var request = new TLSerialization({mtproto: true});
      request.storeMethod('set_client_DH_params', {
        nonce: auth.nonce,
        server_nonce: auth.serverNonce,
        encrypted_data: encryptedData
      });

      console.log(dT(), 'Send set_client_DH_params');
      mtpSendPlainRequest(auth.dcID, request.getBuffer()).then(function (deserializer) {
        var response = deserializer.fetchObject('Set_client_DH_params_answer');

        if (response._ != 'dh_gen_ok' && response._ != 'dh_gen_retry' && response._ != 'dh_gen_fail') {
          deferred.reject(new Error('Set_client_DH_params_answer response invalid: ' + response._));
          return false;
        }

        if (!bytesCmp (auth.nonce, response.nonce)) {
          deferred.reject(new Error('Set_client_DH_params_answer nonce mismatch'));
          return false
        }

        if (!bytesCmp (auth.serverNonce, response.server_nonce)) {
          deferred.reject(new Error('Set_client_DH_params_answer server_nonce mismatch'));
          return false;
        }

        CryptoWorker.modPow(auth.gA, auth.b, auth.dhPrime).then(function (authKey) {
          var authKeyHash = sha1BytesSync(authKey),
              authKeyAux  = authKeyHash.slice(0, 8),
              authKeyID   = authKeyHash.slice(-8);

          console.log(dT(), 'Got Set_client_DH_params_answer', response._);
          switch (response._) {
            case 'dh_gen_ok':
              var newNonceHash1 = sha1BytesSync(auth.newNonce.concat([1], authKeyAux)).slice(-16);

              if (!bytesCmp(newNonceHash1, response.new_nonce_hash1)) {
                deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash1 mismatch'));
                return false;
              }

              var serverSalt = bytesXor(auth.newNonce.slice(0, 8), auth.serverNonce.slice(0, 8));
              // console.log('Auth successfull!', authKeyID, authKey, serverSalt);

              auth.authKeyID = authKeyID;
              auth.authKey = authKey;
              auth.serverSalt = serverSalt;

              deferred.resolve(auth);
              break;

            case 'dh_gen_retry':
              var newNonceHash2 = sha1BytesSync(auth.newNonce.concat([2], authKeyAux)).slice(-16);
              if (!bytesCmp(newNonceHash2, response.new_nonce_hash2)) {
                deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash2 mismatch'));
                return false;
              }

              return mtpSendSetClientDhParams(auth);

            case 'dh_gen_fail':
              var newNonceHash3 = sha1BytesSync(auth.newNonce.concat([3], authKeyAux)).slice(-16);
              if (!bytesCmp(newNonceHash3, response.new_nonce_hash3)) {
                deferred.reject(new Error('Set_client_DH_params_answer new_nonce_hash3 mismatch'));
                return false;
              }

              deferred.reject(new Error('Set_client_DH_params_answer fail'));
              return false;
          }
        }, function (error) {
          deferred.reject(error);
        })
      }, function (error) {
        deferred.reject(error);
      });
    }, function (error) {
      deferred.reject(error);
    })
  };

  var cached = {};

  function mtpAuth (dcID) {
    if (cached[dcID] !== undefined) {
      return cached[dcID];
    }

    var nonce = [];
    for (var i = 0; i < 16; i++) {
      nonce.push(nextRandomInt(0xFF));
    }

    if (!MtpDcConfigurator.chooseServer(dcID)) {
      return $q.reject(new Error('No server found for dc ' + dcID));
    }

    var auth = {
      dcID: dcID,
      nonce: nonce,
      deferred: $q.defer()
    };

    $timeout(function () {
      mtpSendReqPQ(auth);
    });

    cached[dcID] = auth.deferred.promise;

    cached[dcID]['catch'](function () {
      delete cached[dcID];
    })

    return cached[dcID];
  };

  return {
    auth: mtpAuth
  };

})

.factory('MtpNetworkerFactory', function (MtpDcConfigurator, MtpTimeManager, MtpSecureRandom, Storage, CryptoWorker, AppRuntimeManager, $http, $q, $timeout, $interval, $rootScope) {

  var updatesProcessor,
      iii = 0,
      offline,
      offlineInited = false,
      akStopped = false,
      chromeMatches = navigator.userAgent.match(/Chrome\/(\d+(\.\d+)?)/),
      chromeVersion = chromeMatches && parseFloat(chromeMatches[1]) || false,
      xhrSendBuffer = !('ArrayBufferView' in window) && (!chromeVersion || chromeVersion < 30);

  delete $http.defaults.headers.post['Content-Type'];
  delete $http.defaults.headers.common['Accept'];

  $rootScope.retryOnline = function () {
    $(document.body).trigger('online');
  }

  function MtpNetworker(dcID, authKey, serverSalt, options) {
    options = options || {};

    this.dcID = dcID;
    this.iii = iii++;

    this.authKey = authKey;
    this.authKeyUint8 = convertToUint8Array(authKey);
    this.authKeyBuffer = convertToArrayBuffer(authKey);
    this.authKeyID = sha1BytesSync(authKey).slice(-8);

    this.serverSalt = serverSalt;

    this.upload = options.fileUpload || options.fileDownload || false;

    this.updateSession();

    this.currentRequests = 0;
    this.checkConnectionPeriod = 0;

    this.sentMessages = {};
    this.serverMessages = [];
    this.clientMessages = [];

    this.pendingMessages = {};
    this.pendingAcks = [];
    this.pendingResends = [];
    this.connectionInited = false;

    this.pendingTimeouts = [];

    this.longPollInt = $interval(this.checkLongPoll.bind(this), 10000);
    this.checkLongPoll();

    if (!offlineInited) {
      offlineInited = true;
      $rootScope.offline = true;
      $rootScope.offlineConnecting = true;
    }

    if (Config.Navigator.mobile) {
      this.setupMobileSleep();
    }
  };

  MtpNetworker.prototype.updateSession = function () {
    this.seqNo = 0;
    this.sessionID = new Array(8);
    MtpSecureRandom.nextBytes(this.sessionID);

    if (false) {
      this.sessionID[0] = 0xAB;
      this.sessionID[1] = 0xCD;
    }
  };

  MtpNetworker.prototype.setupMobileSleep = function () {
    var self = this;
    $rootScope.$watch('idle.isIDLE', function (isIDLE) {
      if (isIDLE) {
        self.sleepAfter = tsNow() + 30000;
      } else {
        delete self.sleepAfter;
        self.checkLongPoll();
      }
    });

    $rootScope.$on('push_received', function () {
      // console.log(dT(), 'push recieved', self.sleepAfter);
      if (self.sleepAfter) {
        self.sleepAfter = tsNow() + 30000;
        self.checkLongPoll();
      }
    });
  };

  MtpNetworker.prototype.updateSentMessage = function (sentMessageID) {
    var sentMessage = this.sentMessages[sentMessageID];
    if (!sentMessage) {
      return false;
    }
    var self = this;
    if (sentMessage.container) {
      var newInner = [];
      angular.forEach(sentMessage.inner, function(innerSentMessageID){
        var innerSentMessage = self.updateSentMessage(innerSentMessageID);
        if (innerSentMessage) {
          newInner.push(innerSentMessage.msg_id);
        }
      });
      sentMessage.inner = newInner;
    }

    sentMessage.msg_id = MtpTimeManager.generateID();
    sentMessage.seq_no = this.generateSeqNo(
      sentMessage.notContentRelated ||
      sentMessage.container
    );
    this.sentMessages[sentMessage.msg_id] = sentMessage;
    delete self.sentMessages[sentMessageID];

    return sentMessage;
  };

  MtpNetworker.prototype.generateSeqNo = function (notContentRelated) {
    var seqNo = this.seqNo * 2;

    if (!notContentRelated) {
      seqNo++;
      this.seqNo++;
    }

    return seqNo;
  }

  MtpNetworker.prototype.wrapMtpCall = function (method, params, options) {
    var serializer = new TLSerialization({mtproto: true});

    serializer.storeMethod(method, params);

    var messageID = MtpTimeManager.generateID(),
        seqNo = this.generateSeqNo(),
        message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes()
    };

    if (Config.Modes.debug) {
      console.log(dT(), 'MT call', method, params, messageID, seqNo);
    }

    return this.pushMessage(message, options);
  };

  MtpNetworker.prototype.wrapMtpMessage = function (object, options) {
    options = options || {};

    var serializer = new TLSerialization({mtproto: true});
    serializer.storeObject(object, 'Object');

    var messageID = MtpTimeManager.generateID(),
        seqNo = this.generateSeqNo(options.notContentRelated),
        message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes()
    };

    if (Config.Modes.debug) {
      console.log(dT(), 'MT message', object, messageID, seqNo);
    }

    return this.pushMessage(message, options);
  };

  MtpNetworker.prototype.wrapApiCall = function (method, params, options) {
    var serializer = new TLSerialization(options);

    if (!this.connectionInited) {
      serializer.storeInt(0xda9b0d0d, 'invokeWithLayer');
      serializer.storeInt(Config.Schema.API.layer, 'layer');
      serializer.storeInt(0x69796de9, 'initConnection');
      serializer.storeInt(Config.App.id, 'api_id');
      serializer.storeString(navigator.userAgent || 'Unknown UserAgent', 'device_model');
      serializer.storeString(navigator.platform  || 'Unknown Platform', 'system_version');
      serializer.storeString(Config.App.version, 'app_version');
      serializer.storeString(navigator.language || 'en', 'lang_code');
    }

    if (options.afterMessageID) {
      serializer.storeInt(0xcb9f372d, 'invokeAfterMsg');
      serializer.storeLong(options.afterMessageID, 'msg_id');
    }

    options.resultType = serializer.storeMethod(method, params);

    var messageID = MtpTimeManager.generateID(),
        seqNo = this.generateSeqNo(),
        message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes(true),
      isAPI: true
    };

    if (Config.Modes.debug) {
      console.log(dT(), 'Api call', method, params, messageID, seqNo, options);
    } else {
      console.log(dT(), 'Api call', method);
    }

    return this.pushMessage(message, options);
  };

  MtpNetworker.prototype.checkLongPoll = function(force) {
    var isClean = this.cleanupSent();
    // console.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean);
    if (this.longPollPending && tsNow() < this.longPollPending ||
        this.offline ||
        akStopped) {
      return false;
    }
    var self = this;
    Storage.get('dc').then(function (baseDcID) {
      if (isClean && (
                      baseDcID != self.dcID ||
                      self.upload ||
                      self.sleepAfter && tsNow() > self.sleepAfter
      )) {
        // console.warn(dT(), 'Send long-poll for DC is delayed', self.dcID, self.sleepAfter);
        return;
      }
      self.sendLongPoll();
    });
  };

  MtpNetworker.prototype.sendLongPoll = function() {
    var maxWait = 25000,
        self = this;

    this.longPollPending = tsNow() + maxWait;
    // console.log('Set lp', this.longPollPending, tsNow());

    this.wrapMtpCall('http_wait', {
      max_delay: 0,
      wait_after: 0,
      max_wait: maxWait
    }, {
      noResponse: true,
      longPoll: true
    }).then(function () {
      delete self.longPollPending;
      setZeroTimeout(self.checkLongPoll.bind(self));
    }, function () {
      console.log('Long-poll failed');
    });

  };

  MtpNetworker.prototype.pushMessage = function(message, options) {
    var deferred = $q.defer();

    this.sentMessages[message.msg_id] = angular.extend(message, options || {}, {deferred: deferred});
    this.pendingMessages[message.msg_id] = 0;

    if (!options || !options.noShedule) {
      this.sheduleRequest();
    }
    if (angular.isObject(options)) {
      options.messageID = message.msg_id;
    }

    return deferred.promise;
  };

  MtpNetworker.prototype.pushResend = function(messageID, delay) {
    var value = delay ? tsNow() + delay : 0;
    var sentMessage = this.sentMessages[messageID];
    if (sentMessage.container) {
      for (var i = 0; i < sentMessage.inner.length; i++) {
        this.pendingMessages[sentMessage.inner[i]] = value;
      }
    } else {
      this.pendingMessages[messageID] = value;
    }

    // console.log('Resend due', messageID, this.pendingMessages);

    this.sheduleRequest(delay);
  };

  MtpNetworker.prototype.getMsgKeyIv = function (msgKey, isOut) {
    var authKey = this.authKeyUint8,
        x = isOut ? 0 : 8,
        sha1aText = new Uint8Array(48),
        sha1bText = new Uint8Array(48),
        sha1cText = new Uint8Array(48),
        sha1dText = new Uint8Array(48),
        promises = {};

    sha1aText.set(msgKey, 0);
    sha1aText.set(authKey.subarray(x, x + 32), 16);
    promises.sha1a = CryptoWorker.sha1Hash(sha1aText);

    sha1bText.set(authKey.subarray(x + 32, x + 48), 0);
    sha1bText.set(msgKey, 16);
    sha1bText.set(authKey.subarray(x + 48, x + 64), 32);
    promises.sha1b = CryptoWorker.sha1Hash(sha1bText);

    sha1cText.set(authKey.subarray(x + 64, x + 96), 0);
    sha1cText.set(msgKey, 32);
    promises.sha1c = CryptoWorker.sha1Hash(sha1cText);

    sha1dText.set(msgKey, 0);
    sha1dText.set(authKey.subarray(x + 96, x + 128), 16);
    promises.sha1d = CryptoWorker.sha1Hash(sha1dText);

    return $q.all(promises).then(function (result) {
      var aesKey = new Uint8Array(32),
          aesIv = new Uint8Array(32);
          sha1a = new Uint8Array(result.sha1a),
          sha1b = new Uint8Array(result.sha1b),
          sha1c = new Uint8Array(result.sha1c),
          sha1d = new Uint8Array(result.sha1d);

      aesKey.set(sha1a.subarray(0, 8));
      aesKey.set(sha1b.subarray(8, 20), 8);
      aesKey.set(sha1c.subarray(4, 16), 20);

      aesIv.set(sha1a.subarray(8, 20));
      aesIv.set(sha1b.subarray(0, 8), 12);
      aesIv.set(sha1c.subarray(16, 20), 20);
      aesIv.set(sha1d.subarray(0, 8), 24);

      return [aesKey, aesIv];
    });
  };

  MtpNetworker.prototype.checkConnection = function(event) {
    $rootScope.offlineConnecting = true;

    console.log(dT(), 'Check connection', event);
    $timeout.cancel(this.checkConnectionPromise);

    var serializer = new TLSerialization({mtproto: true}),
        pingID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];

    serializer.storeMethod('ping', {ping_id: pingID});

    var pingMessage = {
      msg_id: MtpTimeManager.generateID(),
      seq_no: this.generateSeqNo(true),
      body: serializer.getBytes()
    };

    var self = this;
    this.sendEncryptedRequest(pingMessage, {timeout: 15000}).then(function (result) {
      delete $rootScope.offlineConnecting;
      self.toggleOffline(false);
    }, function () {
      console.log(dT(), 'Delay ', self.checkConnectionPeriod * 1000);
      self.checkConnectionPromise = $timeout(self.checkConnection.bind(self), parseInt(self.checkConnectionPeriod * 1000));
      self.checkConnectionPeriod = Math.min(60, self.checkConnectionPeriod * 1.5);
      $timeout(function () {
        delete $rootScope.offlineConnecting;
      }, 1000);
    })
  };

  MtpNetworker.prototype.toggleOffline = function(enabled) {
    // console.log('toggle ', enabled, this.dcID, this.iii);
    if (this.offline !== undefined && this.offline == enabled) {
      return false;
    }

    this.offline = enabled;
    $rootScope.offline = enabled;
    $rootScope.offlineConnecting = false;

    if (this.offline) {
      $timeout.cancel(this.nextReqPromise);
      delete this.nextReq;

      if (this.checkConnectionPeriod < 1.5) {
        this.checkConnectionPeriod = 0;
      }

      this.checkConnectionPromise = $timeout(this.checkConnection.bind(this), parseInt(this.checkConnectionPeriod * 1000));
      this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5);

      this.onOnlineCb = this.checkConnection.bind(this);

      $(document.body).on('online focus', this.onOnlineCb);
    } else {
      delete this.longPollPending;
      this.checkLongPoll();
      this.sheduleRequest();

      if (this.onOnlineCb) {
        $(document.body).off('online focus', this.onOnlineCb);
      }
      $timeout.cancel(this.checkConnectionPromise);
    }

  };



  MtpNetworker.prototype.performSheduledRequest = function() {
    // console.log(dT(), 'sheduled', this.dcID, this.iii);
    if (this.offline || akStopped) {
      console.log(dT(), 'Cancel sheduled');
      return false;
    }
    delete this.nextReq;
    if (this.pendingAcks.length) {
      var ackMsgIDs = [];
      for (var i = 0; i < this.pendingAcks.length; i++) {
        ackMsgIDs.push(this.pendingAcks[i]);
      }
      // console.log('acking messages', ackMsgIDs);
      this.wrapMtpMessage({_: 'msgs_ack', msg_ids: ackMsgIDs}, {notContentRelated: true, noShedule: true});
    }

    if (this.pendingResends.length) {
      var resendMsgIDs = [],
          resendOpts = {noShedule: true, notContentRelated: true};
      for (var i = 0; i < this.pendingResends.length; i++) {
        resendMsgIDs.push(this.pendingResends[i]);
      }
      // console.log('resendReq messages', resendMsgIDs);
      this.wrapMtpMessage({_: 'msg_resend_req', msg_ids: resendMsgIDs}, resendOpts);
      this.lastResendReq = {req_msg_id: resendOpts.messageID, resend_msg_ids: resendMsgIDs};
    }

    var messages = [],
        message,
        messagesByteLen = 0,
        currentTime = tsNow(),
        hasApiCall = false,
        hasHttpWait = false,
        lengthOverflow = false,
        singlesCount = 0,
        self = this;

    angular.forEach(this.pendingMessages, function (value, messageID) {
      if (!value || value >= currentTime) {
        if (message = self.sentMessages[messageID]) {
          var messageByteLength = (message.body.byteLength || message.body.length) + 32;
          if (!message.notContentRelated &&
              lengthOverflow) {
            return;
          }
          if (!message.notContentRelated &&
              messagesByteLen &&
              messagesByteLen + messageByteLength > 655360) { // 640 Kb
            lengthOverflow = true;
            return;
          }
          if (message.singleInRequest) {
            singlesCount++;
            if (singlesCount > 1) {
              return;
            }
          }
          messages.push(message);
          messagesByteLen += messageByteLength;
          if (message.isAPI) {
            hasApiCall = true;
          }
          else if (message.longPoll) {
            hasHttpWait = true;
          }
        } else {
          // console.log(message, messageID);
        }
        delete self.pendingMessages[messageID];
      }
    });

    if (hasApiCall && !hasHttpWait) {
      var serializer = new TLSerialization({mtproto: true});
      serializer.storeMethod('http_wait', {max_delay: 0, wait_after: 0, max_wait: 1000});
      messages.push({
        msg_id: MtpTimeManager.generateID(),
        seq_no: this.generateSeqNo(),
        body: serializer.getBytes()
      });
    }

    if (!messages.length) {
      // console.log('no sheduled messages');
      return;
    }

    var noResponseMsgs = [];

    if (messages.length > 1) {
      var container = new TLSerialization({mtproto: true, startMaxLength: messagesByteLen + 64});
      container.storeInt(0x73f1f8dc, 'CONTAINER[id]');
      container.storeInt(messages.length, 'CONTAINER[count]');
      var onloads = [];
      var innerMessages = [];
      for (var i = 0; i < messages.length; i++) {
        container.storeLong(messages[i].msg_id, 'CONTAINER[' + i + '][msg_id]');
        innerMessages.push(messages[i].msg_id);
        container.storeInt(messages[i].seq_no, 'CONTAINER[' + i + '][seq_no]');
        container.storeInt(messages[i].body.length, 'CONTAINER[' + i + '][bytes]');
        container.storeRawBytes(messages[i].body, 'CONTAINER[' + i + '][body]');
        if (messages[i].noResponse) {
          noResponseMsgs.push(messages[i].msg_id);
        }
      }

      var containerSentMessage = {
        msg_id: MtpTimeManager.generateID(),
        seq_no: this.generateSeqNo(true),
        container: true,
        inner: innerMessages
      }

      message = angular.extend({body: container.getBytes(true)}, containerSentMessage);

      this.sentMessages[message.msg_id] = containerSentMessage;

      if (Config.Modes.debug) {
        console.log(dT(), 'Container', innerMessages, message.msg_id, message.seq_no);
      }
    } else {
      if (message.noResponse) {
        noResponseMsgs.push(message.msg_id);
      }
      this.sentMessages[message.msg_id] = message;
    }

    this.pendingAcks = [];

    var self = this;
    this.sendEncryptedRequest(message).then(function (result) {
      self.toggleOffline(false);
      self.parseResponse(result.data).then(function (response) {
        if (Config.Modes.debug) {
          console.log(dT(), 'Server response', self.dcID, response);
        }

        self.processMessage(response.response, response.messageID, response.sessionID);

        angular.forEach(noResponseMsgs, function (msgID) {
          if (self.sentMessages[msgID]) {
            var deferred = self.sentMessages[msgID].deferred;
            delete self.sentMessages[msgID];
            deferred.resolve();
          }
        });

        self.checkLongPoll();

        this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));

      });
    }, function (error) {
      console.log('Encrypted request failed', error);

      if (message.container) {
        angular.forEach(message.inner, function (msgID) {
          self.pendingMessages[msgID] = 0;
        });
        delete self.sentMessages[message.msg_id];
      } else {
        self.pendingMessages[message.msg_id] = 0;
      }

      angular.forEach(noResponseMsgs, function (msgID) {
        if (self.sentMessages[msgID]) {
          var deferred = self.sentMessages[msgID].deferred;
          delete self.sentMessages[msgID];
          delete self.pendingMessages[msgID];
          deferred.reject();
        }
      });

      self.toggleOffline(true);
    });

    if (lengthOverflow || singlesCount > 1) {
      this.sheduleRequest()
    }
  };

  MtpNetworker.prototype.getEncryptedMessage = function (bytes) {
    var self = this;

    // console.log(dT(), 'Start encrypt', bytes.byteLength);
    return CryptoWorker.sha1Hash(bytes).then(function (bytesHash) {
      // console.log(dT(), 'after hash');
      var msgKey = new Uint8Array(bytesHash).subarray(4, 20);
      return self.getMsgKeyIv(msgKey, true).then(function (keyIv) {
        // console.log(dT(), 'after msg key iv');
        return CryptoWorker.aesEncrypt(bytes, keyIv[0], keyIv[1]).then(function (encryptedBytes) {
          // console.log(dT(), 'Finish encrypt');
          return {
            bytes: encryptedBytes,
            msgKey: msgKey
          };
        })
      })
    })
  };

  MtpNetworker.prototype.getDecryptedMessage = function (msgKey, encryptedData) {
    // console.log(dT(), 'get decrypted start');
    return this.getMsgKeyIv(msgKey, false).then(function (keyIv) {
      // console.log(dT(), 'after msg key iv');
      return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
    });
  };

  MtpNetworker.prototype.sendEncryptedRequest = function (message, options) {
    var self = this;
    options = options || {};
    // console.log(dT(), 'Send encrypted'/*, message*/);
    // console.trace();
    var data = new TLSerialization({startMaxLength: message.body.length + 64});

    data.storeIntBytes(this.serverSalt, 64, 'salt');
    data.storeIntBytes(this.sessionID, 64, 'session_id');

    data.storeLong(message.msg_id, 'message_id');
    data.storeInt(message.seq_no, 'seq_no');

    data.storeInt(message.body.length, 'message_data_length');
    data.storeRawBytes(message.body, 'message_data');

    return this.getEncryptedMessage(data.getBuffer()).then(function (encryptedResult) {
      // console.log(dT(), 'Got encrypted out message'/*, encryptedResult*/);
      var request = new TLSerialization({startMaxLength: encryptedResult.bytes.byteLength + 256});
      request.storeIntBytes(self.authKeyID, 64, 'auth_key_id');
      request.storeIntBytes(encryptedResult.msgKey, 128, 'msg_key');
      request.storeRawBytes(encryptedResult.bytes, 'encrypted_data');

      var requestData = xhrSendBuffer ? request.getBuffer() : request.getArray();

      var requestPromise;
      try {
        options = angular.extend(options || {}, {
          responseType: 'arraybuffer',
          transformRequest: null
        });
        requestPromise =  $http.post(MtpDcConfigurator.chooseServer(self.dcID, self.upload), requestData, options);
      } catch (e) {
        requestPromise = $q.reject(e);
      }
      return requestPromise.then(
        function (result) {
          if (!result.data || !result.data.byteLength) {
            return $q.reject({code: 406, type: 'NETWORK_BAD_RESPONSE'});
          }
          return result;
        },
        function (error) {
          if (error.status == 404 &&
              (error.data || '').indexOf('nginx/0.3.33') != -1) {
            Storage.remove(
              'dc' + self.dcID + '_server_salt',
              'dc' + self.dcID + '_auth_key'
            ).then(function () {
              AppRuntimeManager.reload();
            });
          }
          if (!error.message && !error.type) {
            error = {code: 406, type: 'NETWORK_BAD_REQUEST'};
          }
          return $q.reject(error);
        }
      );
    });
  };

  MtpNetworker.prototype.parseResponse = function (responseBuffer) {
    // console.log(dT(), 'Start parsing response');
    var self = this;

    var deserializer = new TLDeserialization(responseBuffer);

    var authKeyID = deserializer.fetchIntBytes(64, false, 'auth_key_id');
    if (!bytesCmp(authKeyID, this.authKeyID)) {
      throw new Error('Invalid server auth_key_id: ' + bytesToHex(authKeyID));
    }
    var msgKey = deserializer.fetchIntBytes(128, true, 'msg_key'),
        encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data');

    return this.getDecryptedMessage(msgKey, encryptedData).then(function (dataWithPadding) {
      // console.log(dT(), 'after decrypt');
      var deserializer = new TLDeserialization(dataWithPadding, {mtproto: true});

      var salt = deserializer.fetchIntBytes(64, false, 'salt');
      var sessionID = deserializer.fetchIntBytes(64, false, 'session_id');
      var messageID = deserializer.fetchLong('message_id');

      var seqNo = deserializer.fetchInt('seq_no');

      var messageBody = deserializer.fetchRawBytes(false, true, 'message_data');

      // console.log(dT(), 'before hash');
      var hashData = convertToUint8Array(dataWithPadding).subarray(0, deserializer.getOffset());

      return CryptoWorker.sha1Hash(hashData).then(function (dataHash) {
        if (!bytesCmp(msgKey, bytesFromArrayBuffer(dataHash).slice(-16))) {
          console.warn(msgKey, bytesFromArrayBuffer(dataHash));
          throw new Error('server msgKey mismatch');
        }

        var buffer = bytesToArrayBuffer(messageBody);
        var deserializerOptions = {
          mtproto: true,
          override: {
            mt_message: function (result, field) {
              result.msg_id = this.fetchLong(field + '[msg_id]');
              result.seqno = this.fetchInt(field + '[seqno]');
              result.bytes = this.fetchInt(field + '[bytes]');

              var offset = this.getOffset();

              try {
                result.body = this.fetchObject('Object', field + '[body]');
              } catch (e) {
                console.error(dT(), 'parse error', e.message, e.stack);
                result.body = {_: 'parse_error', error: e};
              }
              if (this.offset != offset + result.bytes) {
                console.warn(dT(), 'set offset', this.offset, offset, result.bytes);
                // console.log(dT(), result);
                this.offset = offset + result.bytes;
              }
              // console.log(dT(), 'override message', result);
            },
            mt_rpc_result: function (result, field) {
              result.req_msg_id = this.fetchLong(field + '[req_msg_id]');

              var sentMessage = self.sentMessages[result.req_msg_id],
                  type = sentMessage && sentMessage.resultType || 'Object';

              result.result = this.fetchObject(type, field + '[result]');
              // console.log(dT(), 'override rpc_result', type, result);
            }
          }
        };
        var deserializer = new TLDeserialization(buffer, deserializerOptions);
        var response = deserializer.fetchObject('', 'INPUT');

        return {
          response: response,
          messageID: messageID,
          sessionID: sessionID,
          seqNo: seqNo
        };
      });
    });
  };

  MtpNetworker.prototype.applyServerSalt = function (newServerSalt) {
    var serverSalt = longToBytes(newServerSalt);

    var storeObj  = {};
    storeObj['dc' + this.dcID + '_server_salt'] = bytesToHex(serverSalt);
    Storage.set(storeObj);

    this.serverSalt = serverSalt;
    return true;
  };

  MtpNetworker.prototype.sheduleRequest = function (delay) {
    if (this.offline) {
      this.checkConnection('forced shedule');
    }
    var nextReq = tsNow() + delay;

    if (delay && this.nextReq && this.nextReq <= nextReq) {
      return false;
    }

    // console.log(dT(), 'shedule req', delay);
    // console.trace();

    $timeout.cancel(this.nextReqPromise);
    if (delay > 0) {
      this.nextReqPromise = $timeout(this.performSheduledRequest.bind(this), delay || 0);
    } else {
      setZeroTimeout(this.performSheduledRequest.bind(this))
    }

    this.nextReq = nextReq;
  };

  MtpNetworker.prototype.ackMessage = function (msgID) {
    // console.log('ack message', msgID);
    this.pendingAcks.push(msgID);
    this.sheduleRequest(30000);
  };

  MtpNetworker.prototype.reqResendMessage = function (msgID) {
    console.log(dT(), 'Req resend', msgID);
    this.pendingResends.push(msgID);
    this.sheduleRequest(100);
  };

  MtpNetworker.prototype.cleanupSent = function () {
    var self = this;
    var notEmpty = false;
    // console.log('clean start', this.dcID/*, this.sentMessages*/);
    angular.forEach(this.sentMessages, function(message, msgID) {
      // console.log('clean iter', msgID, message);
      if (message.notContentRelated && self.pendingMessages[msgID] === undefined) {
        // console.log('clean notContentRelated', msgID);
        delete self.sentMessages[msgID];
      }
      else if (message.container) {
        for (var i = 0; i < message.inner.length; i++) {
          if (self.sentMessages[message.inner[i]] !== undefined) {
            // console.log('clean failed, found', msgID, message.inner[i], self.sentMessages[message.inner[i]].seq_no);
            notEmpty = true;
            return;
          }
        }
        // console.log('clean container', msgID);
        delete self.sentMessages[msgID];
      } else {
        notEmpty = true;
      }
    });

    return !notEmpty;
  };


  MtpNetworker.prototype.processMessageAck = function (messageID) {
    var sentMessage = this.sentMessages[messageID];
    if (sentMessage && !sentMessage.acked) {
      delete sentMessage.body;
      sentMessage.acked = true;

      return true;
    }

    return false;
  };

  MtpNetworker.prototype.processError = function (rawError) {
    var matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
    rawError.error_code = uintToInt(rawError.error_code);

    return  {
      code: !rawError.error_code || rawError.error_code <= 0 ? 500 : rawError.error_code,
      type: matches[1] || 'UNKNOWN',
      description: matches[3] || ('CODE#' + rawError.error_code + ' ' + rawError.error_message),
      originalError: rawError
    };
  };


  MtpNetworker.prototype.processMessage = function (message, messageID, sessionID) {
    // console.log('process message', message, messageID, sessionID);
    switch (message._) {
      case 'msg_container':
        var len = message.messages.length;
        for (var i = 0; i < len; i++) {
          this.processMessage(message.messages[i], messageID, sessionID);
        }
        break;

      case 'bad_server_salt':
        console.log(dT(), 'Bad server salt', message);
        var sentMessage = this.sentMessages[message.bad_msg_id];
        if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
          console.log(message.bad_msg_id, message.bad_msg_seqno);
          throw new Error('Bad server salt for invalid message');
        }

        this.applyServerSalt(message.new_server_salt);
        this.pushResend(message.bad_msg_id);
        this.ackMessage(messageID);
        break;

      case 'bad_msg_notification':
        console.log(dT(), 'Bad msg notification', message);
        var sentMessage = this.sentMessages[message.bad_msg_id];
        if (!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
          console.log(message.bad_msg_id, message.bad_msg_seqno);
          throw new Error('Bad msg notification for invalid message');
        }

        if (message.error_code == 16 || message.error_code == 17) {
          if (MtpTimeManager.applyServerTime(
            bigStringInt(messageID).shiftRight(32).toString(10)
          )) {
            console.log(dT(), 'Update session');
            this.updateSession();
          }
          var badMessage = this.updateSentMessage(message.bad_msg_id);
          this.pushResend(badMessage.msg_id);
          this.ackMessage(messageID);
        }
        break;

      case 'message':
        this.serverMessages.push(message.msg_id);
        this.processMessage(message.body, message.msg_id, sessionID);
        break;

      case 'new_session_created':
        this.ackMessage(messageID);

        this.processMessageAck(message.first_msg_id);
        this.applyServerSalt(message.server_salt);

        var self = this;
        Storage.get('dc').then(function (baseDcID) {
          if (baseDcID == self.dcID && !self.upload && updatesProcessor) {
            updatesProcessor(message);
          }
        });
        break;

      case 'msgs_ack':
        for (var i = 0; i < message.msg_ids.length; i++) {
          this.processMessageAck(message.msg_ids[i]);
        }
        break;

      case 'msg_detailed_info':
        if (!this.sentMessages[message.msg_id]) {
          this.ackMessage(message.answer_msg_id);
          break;
        }
      case 'msg_new_detailed_info':
        // this.ackMessage(message.answer_msg_id);
        this.reqResendMessage(message.answer_msg_id);
        break;

      case 'msgs_state_info':
        this.ackMessage(message.answer_msg_id);
        if (this.lastResendReq && this.lastResendReq.req_msg_id == message.req_msg_id && this.pendingResends.length) {
          var i, badMsgID, pos;
          for (i = 0; i < this.lastResendReq.resend_msg_ids.length; i++) {
            badMsgID = this.lastResendReq.resend_msg_ids[i];
            pos = this.pendingResends.indexOf(badMsgID);
            if (pos != -1) {
              this.pendingResends.splice(pos, 1);
            }
          }
        }
        break;

      case 'rpc_result':
        this.ackMessage(messageID);

        var sentMessageID = message.req_msg_id,
            sentMessage = this.sentMessages[sentMessageID];

        this.processMessageAck(sentMessageID);
        if (sentMessage) {
          var deferred = sentMessage.deferred;
          if (message.result._ == 'rpc_error') {
            var error = this.processError(message.result);
            console.log(dT(), 'Rpc error', error)
            if (deferred) {
              deferred.reject(error)
            }
          } else {
            if (deferred) {
              if (Config.Modes.debug) {
                console.log(dT(), 'Rpc response', message.result);
              } else {
                var dRes = message.result._;
                if (!dRes) {
                  if (message.result.length > 5) {
                    dRes = '[..' + message.result.length + '..]';
                  } else {
                    dRes = message.result;
                  }
                }
                console.log(dT(), 'Rpc response', dRes);
              }
              sentMessage.deferred.resolve(message.result);
            }
            if (sentMessage.isAPI) {
              this.connectionInited = true;
            }
          }

          delete this.sentMessages[sentMessageID];
        }
        break;

      default:
        this.ackMessage(messageID);

        // console.log('Update', message);
        if (updatesProcessor) {
          updatesProcessor(message);
        }
        break;

    }
  };

  function startAll() {
    if (akStopped) {
      akStopped = false;
      updatesProcessor({_: 'new_session_created'});
    }
  }

  function stopAll() {
    akStopped = true;
  }

  return {
    getNetworker: function (dcID, authKey, serverSalt, options) {
      return new MtpNetworker(dcID, authKey, serverSalt, options);
    },
    setUpdatesProcessor: function (callback) {
      updatesProcessor = callback;
    },
    stopAll: stopAll,
    startAll: startAll
  };

})
