import {isObject} from '../bin_utils';
import {convertToUint8Array, 
  bufferConcat, nextRandomInt, bytesToHex, longToBytes,
  bytesCmp, bigStringInt} from '../bin_utils';
import {TLDeserialization, TLSerialization} from './tl_utils';
import CryptoWorker from '../crypto/cryptoworker';
import AppStorage from '../storage';
import Schema from './schema';

import timeManager from './timeManager';
import NetworkerFactory from './networkerFactory';
import dcConfigurator from './dcConfigurator';
import Socket from './transports/websocket';
import HTTP from './transports/http';
import { logger, LogLevels } from '../logger';
import { Modes, App } from './mtproto_config';
import { InvokeApiOptions } from '../../types';

//console.error('networker included!', new Error().stack);

type Message = {
  msg_id: string,
  seq_no: number,
  body?: Uint8Array | number[],
  isAPI?: boolean,

  acked?: boolean,

  deferred?: {
    resolve: any,
    reject: any
  },

  container?: boolean,
  inner?: string[],

  notContentRelated?: boolean,
  noSchedule?: boolean,

  resultType?: string,

  singleInRequest?: boolean,
  longPoll?: boolean,
  noResponse?: boolean, // only with http (http_wait for longPoll)
};

class MTPNetworker {
  private authKeyUint8: Uint8Array;

  private upload: boolean;

  private lastServerMessages: Array<string> = [];

  private sentMessages: {
    [msgID: string]: Message
  } = {};

  private pendingMessages: any = {};
  private pendingAcks: Array<string> = [];
  private pendingResends: Array<string> = [];
  private connectionInited = false;

  //private longPollInt: number;
  private longPollPending = 0;

  private seqNo: number = 0;
  private prevSessionID: Array<number> = [];
  private sessionID: Array<number> = [];

  private sleepAfter = 0;

  private offline = false;

  private checkConnectionTimeout: number;
  private checkConnectionPeriod = 0;
  
  private nextReqTimeout: number;
  private nextReq: number = 0;

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

  private lastResendReq: {
    req_msg_id: string,
    resend_msg_ids: Array<string>
  } | null = null;

  private transport: Socket | HTTP;

  private log: ReturnType<typeof logger>;

  constructor(private dcID: number, private authKey: number[], private authKeyID: Uint8Array,
    private serverSalt: number[], private options: InvokeApiOptions = {}) {
    this.authKeyUint8 = convertToUint8Array(this.authKey);
    //this.authKeyID = sha1BytesSync(this.authKey).slice(-8);

    this.upload = this.options.fileUpload || this.options.fileDownload || false;
    this.log = logger('NET-' + dcID + (this.upload ? '-U' : ''));
    this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */);

    /* // Test resend after bad_server_salt
    if(this.dcID == 1 && this.upload) {
      this.serverSalt[0] = 0;
    } */

    this.updateSession();

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

    this.transport = dcConfigurator.chooseServer(this.dcID, this.upload);

    if(this.transport instanceof HTTP) {
      /* this.longPollInt =  */setInterval(this.checkLongPoll.bind(this), 10000);
      this.checkLongPoll();
    } else {
      (this.transport as Socket).networker = this;
    }
  }

  public updateSession() {
    this.seqNo = 0;
    this.prevSessionID = this.sessionID;
    this.sessionID = new Array(8);
    this.sessionID = [...new Uint8Array(this.sessionID.length).randomize()];
    //MTProto.secureRandom.nextBytes(this.sessionID);
  }

  public updateSentMessage(sentMessageID: any) {
    var sentMessage = this.sentMessages[sentMessageID];
    if(!sentMessage) {
      return false;
    }
    var self = this;
    if(sentMessage.container) {
      var newInner: any = [];
      sentMessage.inner.forEach((innerSentMessageID: any) => {
        var innerSentMessage = self.updateSentMessage(innerSentMessageID);
        if(innerSentMessage) {
          newInner.push(innerSentMessage.msg_id);
        }
      })
      sentMessage.inner = newInner;
    }
  
    sentMessage.msg_id = timeManager.generateID();
    sentMessage.seq_no = this.generateSeqNo(
      sentMessage.notContentRelated ||
      sentMessage.container
    );
    this.sentMessages[sentMessage.msg_id] = sentMessage;
    delete self.sentMessages[sentMessageID];
  
    return sentMessage;
  }

  public generateSeqNo(notContentRelated?: boolean) {
    var seqNo = this.seqNo * 2;
  
    if(!notContentRelated) {
      seqNo++;
      this.seqNo++;
    }
  
    return seqNo;
  }

  public wrapMtpCall(method: string, params: any = {}, options: any = {}) {
    var serializer = new TLSerialization({mtproto: true});
  
    serializer.storeMethod(method, params);
  
    var messageID = timeManager.generateID();
    var seqNo = this.generateSeqNo();
    var message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes()
    };
  
    if(Modes.debug) {
      this.log('MT call', method, params, messageID, seqNo);
    }
  
    return this.pushMessage(message, options);
  }
  
  public wrapMtpMessage(object: any = {}, options: any = {}) {
    var serializer = new TLSerialization({mtproto: true});
    serializer.storeObject(object, 'Object');
  
    var messageID = timeManager.generateID();
    var seqNo = this.generateSeqNo(options.notContentRelated);
    var message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes()
    };
  
    if(Modes.debug) {
      this.log('MT message', object, messageID, seqNo);
    }
  
    return this.pushMessage(message, options);
  }

  public wrapApiCall(method: string, params: any = {}, options: InvokeApiOptions = {}) {
    let serializer = new TLSerialization(options);
  
    if(!this.connectionInited) { // this will call once for each new session
      ///////this.log('Wrap api call !this.connectionInited');
      
      let invokeWithLayer = Schema.API.methods.find(m => m.method == 'invokeWithLayer');
      if(!invokeWithLayer) throw new Error('no invokeWithLayer!');
      serializer.storeInt(+invokeWithLayer.id >>> 0, 'invokeWithLayer');

      // @ts-ignore
      serializer.storeInt(Schema.layer, 'layer');
  
      let initConnection = Schema.API.methods.find(m => m.method == 'initConnection');
      if(!initConnection) throw new Error('no initConnection!');
  
      serializer.storeInt(+initConnection.id >>> 0, 'initConnection');
      serializer.storeInt(0x0, 'flags');
      serializer.storeInt(App.id, 'api_id');
      serializer.storeString(navigator.userAgent || 'Unknown UserAgent', 'device_model');
      serializer.storeString(navigator.platform || 'Unknown Platform', 'system_version');
      serializer.storeString(App.version, 'app_version');
      serializer.storeString(navigator.language || 'en', 'system_lang_code');
      serializer.storeString('', 'lang_pack');
      serializer.storeString(navigator.language || 'en', 'lang_code');
      //serializer.storeInt(0x0, 'proxy');
      /* serializer.storeMethod('initConnection', {
        'flags': 0,
        'api_id': App.id,
        'device_model': navigator.userAgent || 'Unknown UserAgent',
        'system_version': navigator.platform || 'Unknown Platform',
        'app_version': App.version,
        'system_lang_code': navigator.language || 'en',
        'lang_pack': '',
        'lang_code': navigator.language || 'en'
      }); */
    }
  
    if(options.afterMessageID) {
      let invokeAfterMsg = Schema.API.methods.find(m => m.method == 'invokeAfterMsg');
      if(!invokeAfterMsg) throw new Error('no invokeAfterMsg!');

      this.log('Api call options.afterMessageID!');
      serializer.storeInt(+invokeAfterMsg.id >>> 0, 'invokeAfterMsg');
      serializer.storeLong(options.afterMessageID, 'msg_id');
    }
  
    options.resultType = serializer.storeMethod(method, params);

    /* if(method == 'account.updateNotifySettings') {
      this.log('api call body:', serializer.getBytes(true));
    } */
  
    var messageID = timeManager.generateID();
    var seqNo = this.generateSeqNo();
    var message = {
      msg_id: messageID,
      seq_no: seqNo,
      body: serializer.getBytes(true),
      isAPI: true
    };
  
    if(Modes.debug/*  || true */) {
      this.log('Api call', method, message, params, options);
    } else {
      this.log('Api call', method, params, options);
    }
  
    return this.pushMessage(message, options);
  }

  public checkLongPoll() {
    const isClean = this.cleanupSent();
    //this.log('Check lp', this.longPollPending, tsNow(), this.dcID, isClean, this);
    if((this.longPollPending && Date.now() < this.longPollPending) ||
      this.offline) {
      //this.log('No lp this time');
      return false;
    }

    AppStorage.get<number>('dc').then((baseDcID: number) => {
      if(isClean && (
          baseDcID != this.dcID ||
          this.upload ||
          (this.sleepAfter && Date.now() > this.sleepAfter)
        )) {
        //console.warn(dT(), 'Send long-poll for DC is delayed', this.dcID, this.sleepAfter);
        return;
      }

      this.sendLongPoll();
    });
  }

  public sendLongPoll() {
    let maxWait = 25000;

    this.longPollPending = Date.now() + maxWait;
    //this.log('Set lp', this.longPollPending, tsNow())
  
    this.wrapMtpCall('http_wait', {
      max_delay: 500,
      wait_after: 150,
      max_wait: maxWait
    }, {
      noResponse: true,
      longPoll: true
    }).then(() => {
      this.longPollPending = 0;
      setTimeout(this.checkLongPoll.bind(this), 0);
    }, (error: ErrorEvent) => {
      this.log('Long-poll failed', error);
    });
  }

  // тут можно сделать таймаут и выводить дисконнект
  public pushMessage(message: {
    msg_id: string,
    seq_no: number,
    body: Uint8Array | number[],
    isAPI?: boolean
  }, options: any = {}) {
    return new Promise((resolve, reject) => {
      this.sentMessages[message.msg_id] = Object.assign(message, options, {
        deferred: {resolve, reject}
      });

      // this.log('Networker pushMessage:', this.sentMessages[message.msg_id]);

      this.pendingMessages[message.msg_id] = 0;
    
      if(!options || !options.noSchedule) {
        this.scheduleRequest();
      }

      if(isObject(options)) {
        options.messageID = message.msg_id;
      }
    });
  }

  public pushResend(messageID: string, delay = 0) {
    const value = delay ? Date.now() + delay : 0;
    const sentMessage = this.sentMessages[messageID];
    if(sentMessage.container) {
      for(let i = 0, length = sentMessage.inner.length; i < length; i++) {
        this.pendingMessages[sentMessage.inner[i]] = value;
      }
    } else {
      this.pendingMessages[messageID] = value;
    }
  
    this.log('Resend due', messageID, this.pendingMessages);
  
    this.scheduleRequest(delay);
  }

  public async getMsgKey(dataWithPadding: ArrayBuffer, isOut: boolean) {
    const authKey = this.authKeyUint8;
    const x = isOut ? 0 : 8
    const msgKeyLargePlain = bufferConcat(authKey.subarray(88 + x, 88 + x + 32), dataWithPadding);

    const msgKeyLarge = await CryptoWorker.sha256Hash(msgKeyLargePlain);
    const msgKey = new Uint8Array(msgKeyLarge).subarray(8, 24);
    return msgKey;
  };

  public getAesKeyIv(msgKey: Uint8Array | number[], isOut: boolean): Promise<[Uint8Array, Uint8Array]> {
    var authKey = this.authKeyUint8;
    var x = isOut ? 0 : 8;
    var sha2aText = new Uint8Array(52);
    var sha2bText = new Uint8Array(52);
    var promises: Array<Promise<number[]>> = [];
  
    sha2aText.set(msgKey, 0);
    sha2aText.set(authKey.subarray(x, x + 36), 16);
    promises.push(CryptoWorker.sha256Hash(sha2aText));
  
    sha2bText.set(authKey.subarray(40 + x, 40 + x + 36), 0);
    sha2bText.set(msgKey, 36);
    promises.push(CryptoWorker.sha256Hash(sha2bText));

    return Promise.all(promises).then((results) => {
      var aesKey = new Uint8Array(32);
      var aesIv = new Uint8Array(32);
      var sha2a = new Uint8Array(results[0]);
      var sha2b = new Uint8Array(results[1]);
  
      aesKey.set(sha2a.subarray(0, 8));
      aesKey.set(sha2b.subarray(8, 24), 8);
      aesKey.set(sha2a.subarray(24, 32), 24);
  
      aesIv.set(sha2b.subarray(0, 8));
      aesIv.set(sha2a.subarray(8, 24), 8);
      aesIv.set(sha2b.subarray(24, 32), 24);
  
      return [aesKey, aesIv];
    });
  }

  public checkConnection(event: Event | string) {
    /* $rootScope.offlineConnecting = true */
  
    this.log('Check connection', event);
    clearTimeout(this.checkConnectionTimeout);
    this.checkConnectionTimeout = 0;
  
    var serializer = new TLSerialization({mtproto: true});
    var pingID = [nextRandomInt(0xFFFFFFFF), nextRandomInt(0xFFFFFFFF)];
  
    serializer.storeMethod('ping', {
      ping_id: pingID
    });
  
    var pingMessage = {
      msg_id: timeManager.generateID(),
      seq_no: this.generateSeqNo(true),
      body: serializer.getBytes()
    };
  
    var self = this;
    this.sendEncryptedRequest(pingMessage, {
      timeout: 15000
    }).then((result) => {
      /* delete $rootScope.offlineConnecting */
      self.toggleOffline(false);
    }, () => {
      this.log('Delay ', self.checkConnectionPeriod * 1000);
      self.checkConnectionTimeout = setTimeout(self.checkConnection.bind(self), self.checkConnectionPeriod * 1000 | 0);
      self.checkConnectionPeriod = Math.min(60, self.checkConnectionPeriod * 1.5);
      /* setTimeout(function() {
        delete $rootScope.offlineConnecting
      }, 1000); */
    });
  }

  public toggleOffline(enabled: boolean) {
    // this.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.transport instanceof HTTP)) {
      this.log('toggle ', enabled, this.dcID);
      return;
    }
  
    if(this.offline) {
      clearTimeout(this.nextReqTimeout);
      this.nextReqTimeout = 0;
      this.nextReq = 0;
  
      if(this.checkConnectionPeriod < 1.5) {
        this.checkConnectionPeriod = 0;
      }
  
      this.checkConnectionTimeout = setTimeout(this.checkConnection.bind(this), this.checkConnectionPeriod * 1000 | 0);
      this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5);
  
      document.body.addEventListener('online', this.onOnlineCb, false);
      document.body.addEventListener('focus', this.onOnlineCb, false);
    } else {
      this.checkLongPoll();

      this.scheduleRequest();
  
      document.body.removeEventListener('online', this.onOnlineCb);
      document.body.removeEventListener('focus', this.onOnlineCb);

      clearTimeout(this.checkConnectionTimeout);
      this.checkConnectionTimeout = 0;
    }
  }

  public performScheduledRequest() {
    // this.log('scheduled', this.dcID, this.iii)
    if(this.offline) {
      this.log('Cancel scheduled');
      return false;
    }

    this.nextReq = 0;
    if(this.pendingAcks.length) {
      var ackMsgIDs: Array<string> = this.pendingAcks.slice();
      /* for(var i = 0; i < this.pendingAcks.length; i++) {
        ackMsgIDs.push(this.pendingAcks[i]);
      } */
      // this.log('acking messages', ackMsgIDs)
      this.wrapMtpMessage({
        _: 'msgs_ack',
        msg_ids: ackMsgIDs
      }, {
        notContentRelated: true,
        noSchedule: true
      });
    }
  
    if(this.pendingResends.length) {
      var resendMsgIDs: Array<string> = this.pendingResends.slice();
      var resendOpts = {
        noSchedule: true,
        notContentRelated: true,
        messageID: '' // will set in wrapMtpMessage->pushMessage
      };
      /* for(var i = 0; i < this.pendingResends.length; i++) {
        resendMsgIDs.push(this.pendingResends[i]);
      } */
      this.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[] = [],
      message: Message;
    var messagesByteLen = 0;
    var currentTime = Date.now();
    var hasApiCall = false;
    var hasHttpWait = false;
    var lengthOverflow = false;
    var singlesCount = 0;
    var self = this;
  
    for(let messageID in this.pendingMessages) {
      let value = this.pendingMessages[messageID];

      if(!value || value >= currentTime) {
        if(message = this.sentMessages[messageID]) {
          //this.log('performScheduledRequest message:', message);
          var messageByteLength = (/* message.body.byteLength ||  */message.body.length) + 32;
          if(!message.notContentRelated &&
            lengthOverflow) {
            continue; // maybe break here
          }

          if(!message.notContentRelated &&
            messagesByteLen &&
            messagesByteLen + messageByteLength > 655360) { // 640 Kb
            this.log.warn('lengthOverflow', message);
            lengthOverflow = true;
            continue; // maybe break here
          }

          if(message.singleInRequest) {
            singlesCount++;
            if(singlesCount > 1) {
              continue; // maybe break here
            }
          }

          messages.push(message);
          messagesByteLen += messageByteLength;
          if(message.isAPI) {
            hasApiCall = true;
          } else if(message.longPoll) {
            hasHttpWait = true;
          }
        } else {
          // this.log(message, messageID)
        }

        delete self.pendingMessages[messageID];
      }
    }
  
    if(hasApiCall && !hasHttpWait && this.transport instanceof HTTP) {
      var serializer = new TLSerialization({mtproto: true});
      serializer.storeMethod('http_wait', {
        max_delay: 500,
        wait_after: 150,
        max_wait: 3000
      });

      messages.push({
        msg_id: timeManager.generateID(),
        seq_no: this.generateSeqNo(),
        body: serializer.getBytes()
      });
    }
  
    if(!messages.length) {
      // this.log('no scheduled messages')
      return;
    }
  
    var noResponseMsgs: Array<string> = [];
  
    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 innerMessages: string[] = [];
      messages.forEach((message, i) => {
        container.storeLong(message.msg_id, 'CONTAINER[' + i + '][msg_id]');
        innerMessages.push(message.msg_id);
        container.storeInt(message.seq_no, 'CONTAINER[' + i + '][seq_no]');
        container.storeInt(message.body.length, 'CONTAINER[' + i + '][bytes]');
        container.storeRawBytes(message.body, 'CONTAINER[' + i + '][body]');
        if(message.noResponse) {
          noResponseMsgs.push(message.msg_id);
        }
      });
  
      var containerSentMessage: Message = {
        msg_id: timeManager.generateID(),
        seq_no: this.generateSeqNo(true),
        container: true,
        inner: innerMessages
      };
  
      message = Object.assign({
        body: container.getBytes(true)
      }, containerSentMessage);
  
      this.sentMessages[message.msg_id] = containerSentMessage;
  
      if(Modes.debug || true) {
        this.log('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 = [];

    let promise = this.sendEncryptedRequest(message);
    
    if(!(this.transport instanceof HTTP)) {
      if(noResponseMsgs.length) this.log.error('noResponseMsgs length!', noResponseMsgs);
    } else promise.then((result) => {
      self.toggleOffline(false);
      // this.log('parse for', message)
      self.parseResponse(result).then((response) => {
        if(Modes.debug) {
          this.log('Server response', self.dcID, response);
        }
  
        self.processMessage(response.response, response.messageID, response.sessionID);
  
        noResponseMsgs.forEach((msgID) => {
          if(self.sentMessages[msgID]) {
            var deferred = self.sentMessages[msgID].deferred;
            delete self.sentMessages[msgID];
            deferred.resolve();
          }
        });
  
        if(self.transport instanceof HTTP) {
          self.checkLongPoll();
  
          this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod));
        }
      });
    }, (error) => {
      this.log.error('Encrypted request failed', error, message);
  
      if(message.container) {
        message.inner.forEach((msgID: string) => {
          self.pendingMessages[msgID] = 0;
        });

        delete self.sentMessages[message.msg_id];
      } else {
        self.pendingMessages[message.msg_id] = 0;
      }
  
      noResponseMsgs.forEach((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.scheduleRequest();
    }
  }

  public async getEncryptedMessage(dataWithPadding: ArrayBuffer) {
    let msgKey = await this.getMsgKey(dataWithPadding, true);
    let keyIv = await this.getAesKeyIv(msgKey, true);
    // this.log('after msg key iv')

    let encryptedBytes = await CryptoWorker.aesEncrypt(dataWithPadding, keyIv[0], keyIv[1]);
    // this.log('Finish encrypt')

    return {
      bytes: encryptedBytes,
      msgKey: msgKey
    };
  }

  public getDecryptedMessage(msgKey: Uint8Array | number[], encryptedData: Uint8Array | number[]): Promise<ArrayBuffer> {
    // this.log('get decrypted start')
    return this.getAesKeyIv(msgKey, false).then((keyIv) => {
      // this.log('after msg key iv')
      return CryptoWorker.aesDecrypt(encryptedData, keyIv[0], keyIv[1]);
    });
  }

  public sendEncryptedRequest(message: any, options: any = {}) {
    var self = this;

    this.log.debug('Send encrypted', message, options, this.authKeyID);
    // console.trace()
    var data = new TLSerialization({
      startMaxLength: message.body.length + 2048
    });
  
    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');
  
    var dataBuffer = data.getBuffer();
  
    var paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5));
    var padding = new Array(paddingLength);
    padding = [...new Uint8Array(padding.length).randomize()];
    //MTProto.secureRandom.nextBytes(padding);
  
    var dataWithPadding = bufferConcat(dataBuffer, padding);
    // this.log('Adding padding', dataBuffer, padding, dataWithPadding)
    // this.log('auth_key_id', bytesToHex(self.authKeyID))
  
    return this.getEncryptedMessage(dataWithPadding).then((encryptedResult) => {
      this.log.debug('Got encrypted out message', encryptedResult);

      let 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.getBytes(true) as Uint8Array;
      let requestData = request.getBytes(true);

      let baseError = {
        code: 406,
        type: 'NETWORK_BAD_RESPONSE',
        transport: this.transport
      };

      let promise = this.transport.send(requestData);
      if(!(this.transport instanceof HTTP)) return promise;

      return promise.then((result) => {
        if(!result || !result.byteLength) {
          return Promise.reject(baseError);
        }

        return result;
      }, (error) => {
        if(!error.message && !error.type) {
          error = Object.assign(baseError, {
          type: 'NETWORK_BAD_REQUEST',
          originalError: error
          });
        }
        return Promise.reject(error);
      });
    });
  }

  public parseResponse(responseBuffer: Uint8Array) {
    this.log.debug('Start parsing response'/* , responseBuffer */);

    let self = this;
    let deserializer = new TLDeserialization(responseBuffer);
  
    let authKeyID = deserializer.fetchIntBytes(64, true, 'auth_key_id');
    if(!bytesCmp(authKeyID, this.authKeyID)) {
      throw new Error('[MT] Invalid server auth_key_id: ' + authKeyID.hex);
    }
    let msgKey = deserializer.fetchIntBytes(128, true, 'msg_key');
    let encryptedData = deserializer.fetchRawBytes(responseBuffer.byteLength - deserializer.getOffset(), true, 'encrypted_data');
  
    return self.getDecryptedMessage(msgKey, encryptedData).then((dataWithPadding) => {
      // this.log('after decrypt')
      return self.getMsgKey(dataWithPadding, false).then((calcMsgKey) => {
        if(!bytesCmp(msgKey, calcMsgKey)) {
          this.log.warn('[MT] msg_keys', msgKey, calcMsgKey);
          this.updateSession(); // fix 28.01.2020
          throw new Error('[MT] server msgKey mismatch, updating session');
        }
        // this.log('after msgKey check')
  
        let deserializer = new TLDeserialization(dataWithPadding, {mtproto: true});
  
        /* let salt =  */deserializer.fetchIntBytes(64, false, 'salt'); // need
        let sessionID = deserializer.fetchIntBytes(64, false, 'session_id');
        let messageID = deserializer.fetchLong('message_id');
  
        if(!bytesCmp(sessionID, self.sessionID) &&
          (!self.prevSessionID || !bytesCmp(sessionID, self.prevSessionID))) {
          this.log.warn('Sessions', sessionID, self.sessionID, self.prevSessionID, dataWithPadding);
          //this.updateSession();
          //this.sessionID = sessionID;
          throw new Error('[MT] Invalid server session_id: ' + bytesToHex(sessionID));
        }
  
        let seqNo = deserializer.fetchInt('seq_no');
  
        let totalLength = dataWithPadding.byteLength;
  
        let messageBodyLength = deserializer.fetchInt('message_data[length]');
        let offset = deserializer.getOffset();
  
        if((messageBodyLength % 4) ||
          messageBodyLength > totalLength - offset) {
          throw new Error('[MT] Invalid body length: ' + messageBodyLength);
        }
        let messageBody = deserializer.fetchRawBytes(messageBodyLength, true, 'message_data');
  
        offset = deserializer.getOffset();
        let paddingLength = totalLength - offset;
        if(paddingLength < 12 || paddingLength > 1024) {
          throw new Error('[MT] Invalid padding length: ' + paddingLength);
        }
  
        //let buffer = bytesToArrayBuffer(messageBody);
        deserializer = new TLDeserialization(/* buffer */messageBody, {mtproto: true});
        // костыль
        deserializer.override = {
          mt_message: (function(this: TLDeserialization, result: any, field: string) {
            result.msg_id = this.fetchLong(field + '[msg_id]');
            result.seqno = this.fetchInt(field + '[seqno]');
            result.bytes = this.fetchInt(field + '[bytes]');

            var offset = this.getOffset();

            //self.log('mt_message!!!!!', result, field);

            try {
              result.body = this.fetchObject('Object', field + '[body]');
            } catch(e) {
              self.log.error('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)
              // this.log(result)
              this.offset = offset + result.bytes;
            }
            // this.log('override message', result)
          }).bind(deserializer),
          mt_rpc_result: (function(this: TLDeserialization, result: any, field: any) {
            result.req_msg_id = this.fetchLong(field + '[req_msg_id]');

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

            if(result.req_msg_id && !sentMessage) {
              // console.warn(dT(), 'Result for unknown message', result);
              return;
            }

            result.result = this.fetchObject(type, field + '[result]');
            // self.log(dT(), 'override rpc_result', sentMessage, type, result);
          }).bind(deserializer)
        };

        var response = deserializer.fetchObject('', 'INPUT');
  
        return {
          response: response,
          messageID: messageID,
          sessionID: sessionID,
          seqNo: seqNo
        };
      });
    });
  }

  public applyServerSalt(newServerSalt: string) {
    var serverSalt = longToBytes(newServerSalt);
  
    AppStorage.set({
      ['dc' + this.dcID + '_server_salt']: bytesToHex(serverSalt)
    });
  
    this.serverSalt = serverSalt;
    return true;
  }

  public scheduleRequest(delay = 0) {
    if(!(this.transport instanceof HTTP)) { // if socket
      return this.performScheduledRequest();
    }

    if(this.offline/*  && this.transport instanceof HTTP */) {
      this.checkConnection('forced schedule');
    }

    /* if(delay && !(this.transport instanceof HTTP)) {
      delay = 0;
    } */

    var nextReq = Date.now() + delay;
  
    if(delay && this.nextReq && this.nextReq <= nextReq) {
      return false;
    }
  
    // this.log('schedule req', delay)
    // console.trace()
  
    clearTimeout(this.nextReqTimeout);
    this.nextReqTimeout = 0;
    if(delay > 0) {
      this.nextReqTimeout = setTimeout(this.performScheduledRequest.bind(this), delay || 0);
    } else {
      setTimeout(this.performScheduledRequest.bind(this), 0);
    }
  
    this.nextReq = nextReq;
  }

  public ackMessage(msgID: string) {
    // this.log('ack message', msgID)
    this.pendingAcks.push(msgID);
    this.scheduleRequest(30000);
  }
  
  public reqResendMessage(msgID: string) {
    this.log('Req resend', msgID);
    this.pendingResends.push(msgID);
    this.scheduleRequest(100);
  }

  public cleanupSent() {
    let notEmpty = false;
    // this.log('clean start', this.dcID/*, this.sentMessages*/)
    Object.keys(this.sentMessages).forEach((msgID) => {
      const message = this.sentMessages[msgID];
    
      // this.log('clean iter', msgID, message)
      if(message.notContentRelated && this.pendingMessages[msgID] === undefined) {
        // this.log('clean notContentRelated', msgID)
        delete this.sentMessages[msgID];
      } else if(message.container) {
        for(let i = 0; i < message.inner.length; i++) {
          if(this.sentMessages[message.inner[i]] !== undefined) {
            // this.log('clean failed, found', msgID, message.inner[i], this.sentMessages[message.inner[i]].seq_no)
            notEmpty = true;
            return;
          }
        }
        // this.log('clean container', msgID)
        delete this.sentMessages[msgID];
      } else {
        notEmpty = true;
      }
    });
  
    return !notEmpty;
  }

  public processMessageAck(messageID: string) {
    var sentMessage = this.sentMessages[messageID];
    if(sentMessage && !sentMessage.acked) {
      delete sentMessage.body;
      sentMessage.acked = true;
  
      return true;
    }
  
    return false;
  }

  public processError(rawError: {error_message: string, error_code: number}) {
    var matches = (rawError.error_message || '').match(/^([A-Z_0-9]+\b)(: (.+))?/) || [];
    rawError.error_code = 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
    };
  }

  /**
   * только для сокета, возможно это будет неправильно работать, но в тесте сработало правильно
   */
  public resend() {
    for(let id in this.sentMessages) {
      const msg = this.sentMessages[id];
      if(msg.body) {
        this.pushResend(id);
      }
    }
  }

  public processMessage(message: any, messageID: string, sessionID: Uint8Array | number[]) {
    var msgidInt = parseInt(messageID/* .toString(10) */.substr(0, -10), 10);
    if(msgidInt % 2) {
      this.log.warn('[MT] Server even message id: ', messageID, message);
      return;
    }

    this.log.debug('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], message.messages[i].msg_id, sessionID);
        }
        break;
  
      case 'bad_server_salt':
        this.log('Bad server salt', message);
        var sentMessage = this.sentMessages[message.bad_msg_id];
        if(!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
          this.log(message.bad_msg_id, message.bad_msg_seqno);
          throw new Error('[MT] Bad server salt for invalid message');
        }

        this.applyServerSalt(message.new_server_salt);
        this.pushResend(message.bad_msg_id);
        this.ackMessage(messageID);
        
        /* // simulate disconnect
        try {
          this.log('networker state:', this);
          // @ts-ignore
          this.transport.ws.close(1000);
        } catch(err) {
          this.log.error('transport', this.transport, err);
        } */

        break;
  
      case 'bad_msg_notification':
        this.log('Bad msg notification', message);
        var sentMessage = this.sentMessages[message.bad_msg_id];
        if(!sentMessage || sentMessage.seq_no != message.bad_msg_seqno) {
          this.log(message.bad_msg_id, message.bad_msg_seqno);
          throw new Error('[MT] Bad msg notification for invalid message');
        }
  
        if(message.error_code == 16 || message.error_code == 17) {
          if(timeManager.applyServerTime(
              bigStringInt(messageID).shiftRight(32).toString(10)
            )) {
            this.log('Update session');
            this.updateSession();
          }

          var badMessage = this.updateSentMessage(message.bad_msg_id);
          if(badMessage) this.pushResend(badMessage.msg_id); // fix 23.01.2020
          this.ackMessage(messageID);
        }
        break;
  
      case 'message':
        if(this.lastServerMessages.indexOf(messageID) != -1) {
          // console.warn('[MT] Server same messageID: ', messageID)
          this.ackMessage(messageID);
          return;
        }
        this.lastServerMessages.push(messageID);
        if(this.lastServerMessages.length > 100) {
          this.lastServerMessages.shift();
        }
        this.processMessage(message.body, message.msg_id, sessionID);
        break;
  
      case 'new_session_created':
        this.ackMessage(messageID);

        this.log.debug('new_session_created', message);
        //this.updateSession();
  
        this.processMessageAck(message.first_msg_id);
        this.applyServerSalt(message.server_salt);
  
        AppStorage.get<number>('dc').then((baseDcID: number) => {
          if(baseDcID == this.dcID && !this.upload && NetworkerFactory.updatesProcessor) {
            NetworkerFactory.updatesProcessor(message, true);
          }
        });
        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':
        if(this.pendingAcks.indexOf(message.answer_msg_id)) {
          break;
        }
        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 badMsgID, pos;
          for(let 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;
        var sentMessage = this.sentMessages[sentMessageID];

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

              sentMessage.deferred.resolve(message.result);
            }

            if(sentMessage.isAPI && !this.connectionInited) {
              this.connectionInited = true;
              ////this.log('Rpc set connectionInited to:', this.connectionInited);
            }
          }
  
          delete this.sentMessages[sentMessageID];
        }
        break;
  
      default:
        this.ackMessage(messageID);

        this.log.debug('Update', message);
        
        if(NetworkerFactory.updatesProcessor !== null) {
          NetworkerFactory.updatesProcessor(message, true);
        }
        break;
    }
  }
}

export {MTPNetworker};
