'use strict';

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var Promise = require('bluebird');
var EventEmitter = require('events').EventEmitter;

var RETRIES = 7;
var DELAY = 500;
var LANG_EXTS = {
  python: 'py',
  ruby: 'rb',
  php: 'php',
  python3: 'py',
  nodejs: 'js',
  go: 'go',
  java: 'java',
  cpp: 'cpp',
  cpp11: 'cpp',
  c: 'c',
  csharp: 'cs',
  fsharp: 'fs',
  rust: 'rs',
  swift: 'swift',
  react_native: 'js',
  jest: 'js'
};

function mainFile(lang) {
  var file = void 0;
  switch (lang) {
    case 'java':
      file = 'Main';
      break;
    case 'react_native':
    case 'nodejs':
      file = 'index';
      break;
    default:
      file = 'main';
  }

  return file + '.' + LANG_EXTS[lang];
}

var ReplitClient = function (_EventEmitter) {
  _inherits(ReplitClient, _EventEmitter);

  function ReplitClient(host, port, language, authToken, socketCreator, options) {
    _classCallCheck(this, ReplitClient);

    var _this = _possibleConstructorReturn(this, (ReplitClient.__proto__ || Object.getPrototypeOf(ReplitClient)).call(this));

    _this._host = host;
    _this._port = port;
    _this._language = language;
    if (typeof authToken === 'string') {
      _this._authToken = authToken;
    } else {
      _this._authToken = authToken.time_created + ':' + authToken.msg_mac;
    }
    _this._init();
    _this._retries = RETRIES;
    _this._handshakeRetries = RETRIES;
    _this._socketCreator = socketCreator;
    if (options != null) {
      _this._timeout = options.timeout;
    }
    return _this;
  }

  _createClass(ReplitClient, [{
    key: 'connect',
    value: function connect() {
      if (this._connecting) {
        return this._connecting;
      }

      this._connecting = this._connect();
      return this._connecting;
    }
  }, {
    key: 'disconnect',
    value: function disconnect() {
      var _this2 = this;

      if (!(this._ws && this._ws.readyState === this._ws.OPEN)) {
        return Promise.resolve(true);
      }

      clearTimeout(this._pingTimer);
      return new Promise(function (resolve) {
        _this2._ws.onclose = function () {
          return resolve(true);
        };
        _this2._ws.close();
      });
    }
  }, {
    key: 'write',
    value: function write(str) {
      var _this3 = this;

      return this._connectOrTry().then(function () {
        _this3._sendMessage('input', str);
      });
    }
  }, {
    key: 'evaluate',
    value: function evaluate(code, _ref) {
      var _this4 = this;

      var stdout = _ref.stdout;

      return this._connectOrTry().then(function () {
        _this4._stdout = stdout;
        _this4._sendMessage('eval', code);

        return _this4._waitForResult();
      });
    }
  }, {
    key: 'lint',
    value: function lint(code) {
      var _this5 = this;

      return this._connectOrTry().then(function () {
        _this5._sendMessage('lint', code);
        return _this5._waitForLintResult().then(function (result) {
          result.data = parseJSON(result.data);

          return result;
        });
      });
    }
  }, {
    key: 'evaluateOnce',
    value: function evaluateOnce(code, options) {
      var _this6 = this;

      return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();

        var t = null;
        if (options != null && options.timeout) {
          var _timeout = options.timeout;
          var _callback = function _callback() {
            // this is just to make flow happy
            var stop = _timeout.callback();
            if (stop) {
              req.abort();
              reject(new Error('Aborted'));
            } else {
              t = setTimeout(_callback, _timeout.time);
            }
          };
          t = setTimeout(_callback, _timeout.time);
        }

        req.onload = function () {
          if (req.status !== 200) {
            if (t) {
              clearTimeout(t);
            }

            reject(new Error(req.responseText));
          } else {
            var _data = [];
            try {
              _data = parseJSON(req.responseText);
            } catch (e) {
              if (t) {
                clearTimeout(t);
              }

              reject(e);
              return;
            }
            _data.forEach(function (msg) {
              if (msg.command === 'output') {
                if (options && options.stdout != null) {
                  options.stdout(msg.data);
                }
              } else if (msg.command === 'result') {
                if (t) {
                  clearTimeout(t);
                }

                resolve(msg);
              }
            });
          }
        };

        var protocol = 'http';
        var port = _this6._port;
        if ((typeof location === 'undefined' ? 'undefined' : _typeof(location)) === 'object' && location.protocol === 'https:') {
          protocol = 'https';
          if (port === 80 || port === '80') {
            port = 443;
          }
        }

        req.open('POST', protocol + '://' + _this6._host + ':' + port + '/eval', true);

        req.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; param=value');

        var data = '';
        data += 'auth=' + encodeURIComponent(_this6._authToken);
        data += '&language=' + encodeURIComponent(_this6._language);
        data += '&code=' + encodeURIComponent(code);

        req.send(data);
      });
    }
  }, {
    key: 'stop',
    value: function stop() {
      return this._reset('stop');
    }
  }, {
    key: 'reset',
    value: function reset() {
      return this._reset('reset');
    }
  }, {
    key: '_emptyResultQueue',
    value: function _emptyResultQueue() {
      // Empty all the resolvers for run because reset/stop we'll most likely
      // never get the result back.
      var newResolvers = [];

      var _iteratorNormalCompletion = true;
      var _didIteratorError = false;
      var _iteratorError = undefined;

      try {
        for (var _iterator = this._msgResolvers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
          var _step$value = _step.value,
              _commandRe = _step$value.commandRe,
              _resolve = _step$value.resolve;

          if (_commandRe.test('result')) {
            try {
              _resolve({ command: 'skip' });
            } catch (e) {
              // ignore.
            }
          } else {
            newResolvers.push({ commandRe: _commandRe, resolve: _resolve });
          }
        }
      } catch (err) {
        _didIteratorError = true;
        _iteratorError = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion && _iterator.return) {
            _iterator.return();
          }
        } finally {
          if (_didIteratorError) {
            throw _iteratorError;
          }
        }
      }

      this._msgResolvers = newResolvers;

      // We should not have to do that but right now there is a bug
      // in pid1 where reset/stop sends back a result.
      this._msgQueue = this._msgQueue.filter(function (_ref2) {
        var command = _ref2.command;
        return command !== 'result';
      });
    }
  }, {
    key: '_reset',
    value: function _reset(message) {
      var _this7 = this;

      return this._connectOrTry().then(function () {
        _this7._emptyResultQueue();
        _this7._sendMessage(message);
        return _this7._onMessage(/ready/).then(function (msg) {
          _this7._emptyResultQueue();
          return msg;
        });
      });
    }
  }, {
    key: 'fileChanges',
    value: function fileChanges() {
      var _this8 = this;

      return this._connectOrTry().then(function () {
        _this8._sendMessage('changedFiles', '');
      });
    }
  }, {
    key: 'fileContents',
    value: function fileContents(path) {
      var _this9 = this;

      return this._connectOrTry().then(function () {
        _this9._sendMessage('fileContents', path);

        return _this9._waitForFileResult();
      });
    }
  }, {
    key: 'removeFile',
    value: function removeFile(path) {
      var _this10 = this;

      return this._connectOrTry().then(function () {
        _this10._sendMessage('removeFile', path);
        return _this10._waitForRemoveFileResult();
      });
    }
  }, {
    key: 'runProject',
    value: function runProject(files, _ref3) {
      var _this11 = this;

      var stdout = _ref3.stdout;

      return this._connectOrTry().then(function () {
        _this11._stdout = stdout;

        // Need to stringify files because 'data' field is expected
        // to be string and not array.
        _this11._sendMessage('runProject', JSON.stringify(files));

        return _this11._waitForResult();
      });
    }
  }, {
    key: 'format',
    value: function format(code) {
      var _this12 = this;

      return this._connectOrTry().then(function () {
        _this12._sendMessage('format', code);
        return _this12._waitForFormatResult().then(function (result) {
          if (result.error) {
            return { error: result.error };
          }

          return { code: result.data };
        });
      });
    }
  }, {
    key: 'runUnitTests',
    value: function runUnitTests(_ref4) {
      var _this13 = this;

      var files = _ref4.files,
          suiteCode = _ref4.suiteCode;

      return this.reset().then(function () {
        // Need to stringify files because 'data' field is expected
        // to be string and not array.
        _this13._sendMessage('runUnitTests', JSON.stringify({
          files: files,
          suiteCode: suiteCode
        }));

        return _this13._waitForResult().then(function (result) {
          var data = void 0;
          if (result.data) {
            data = parseJSON(result.data);
          } else {
            data = { passed: false, failures: [] };
          }
          return {
            passed: data.passed,
            failures: data.failures,
            error: result.error
          };
        });
      });
    }
  }, {
    key: 'runSingleUnitTests',
    value: function runSingleUnitTests(_ref5) {
      var code = _ref5.code,
          suiteCode = _ref5.suiteCode;

      var files = [{
        name: mainFile(this._language),
        content: code
      }];

      return this.runUnitTests({
        files: files,
        suiteCode: suiteCode
      });
    }
  }, {
    key: 'debug',
    value: function debug(code, _ref6) {
      var stdout = _ref6.stdout;

      var loading = this.reset();
      this._stdout = stdout;
      var debug = new Debugger(this, loading, code);
      this._ops = loading.then(function () {
        return new Promise(function (resolve) {
          debug.on('done', function () {
            return resolve();
          });
        });
      });

      return debug;
    }
  }, {
    key: '_init',
    value: function _init() {
      this._ws = null;
      this._stdout = null;
      this._evaluating = null;

      this._msgQueue = [];
      this._msgResolvers = [];
      this._ops = Promise.resolve();
    }
  }, {
    key: '_onMessage',
    value: function _onMessage(commandRe) {
      var _this14 = this;

      return new Promise(function (resolve) {
        if (_this14._msgQueue.length && _this14._msgQueue[_this14._msgQueue.length - 1].command.match(commandRe)) {
          resolve(_this14._msgQueue.shift());
        } else {
          _this14._msgResolvers.push({ commandRe: commandRe, resolve: resolve });
        }
      });
    }
  }, {
    key: '_nextMessage',
    value: function _nextMessage() {
      var _this15 = this;

      return new Promise(function (resolve) {
        if (_this15._msgQueue.length) {
          resolve(_this15._msgQueue.shift());
        } else {
          _this15._msgResolvers.unshift({ commandRe: /.*/, resolve: resolve });
        }
      });
    }

    // This will make sure we at least try to connect
    // and not get stuck in some past error.
    // use case is what if we were disconnected from the internet
    // and now we are connected.

  }, {
    key: '_connectOrTry',
    value: function _connectOrTry() {
      var _this16 = this;

      return this.connect().catch(function () {
        _this16._connecting = null;
        return _this16.connect();
      });
    }
  }, {
    key: '_handshakeRetry',
    value: function _handshakeRetry(resolve, reject) {
      this._init();
      this._handshakeRetries -= 1;

      if (this._handshakeRetries < 1) {
        return reject(new HandshakeError('Handshake error'));
      }

      this._connect().then(resolve, reject);
    }
  }, {
    key: '_connect',
    value: function _connect() {
      var _this17 = this;

      return new Promise(function (resolve, reject) {
        _this17.emit('connecting');

        var protocol = 'ws';
        var port = _this17._port;
        if ((typeof location === 'undefined' ? 'undefined' : _typeof(location)) === 'object' && location.protocol === 'https:') {
          protocol = 'wss';
          if (port === 80 || port === '80') {
            port = 443;
          }
        }

        var url = protocol + '://' + _this17._host + ':' + port + '/ws';
        _this17._ws = _this17._socketCreator ? _this17._socketCreator(url) : new WebSocket(url);

        _this17._ws.onopen = function () {
          _this17._ws.onclose = _this17._handshakeRetry.bind(_this17, resolve, reject);

          var handshakePromise = _this17._handshake();

          if (_this17._timeout) {
            handshakePromise = handshakePromise.timeout(_this17._timeout);
          }

          handshakePromise.then(function () {
            _this17._ws.onclose = _this17._closeHandler.bind(_this17);
            // Reset retries.
            _this17._retries = RETRIES;
            resolve(_this17);
            _this17.emit('connected');
          }).catch(function (err) {
            _this17.disconnect();

            if (err instanceof Promise.TimeoutError) {
              _this17._handshakeRetry(resolve, reject);
              return;
            }

            reject(err);
          });
        };

        _this17._ws.onclose = function (e) {
          reject(new Error(errMsgFromCloseEvent(e)));
        };
        _this17._ws.onmessage = _this17._messageHandler.bind(_this17);
      });
    }
  }, {
    key: '_handshake',
    value: function _handshake() {
      var _this18 = this;

      this._sendMessage('auth', this._authToken);
      return this._nextMessage().then(function (msg) {
        if (msg.command !== 'auth_result') {
          throw new Error('Expected "auth_result" command: ' + msg.command);
        }
        if (msg.data !== 'true') {
          _this18._authFailed = true;
          throw new Error('Failed to auth');
        }
        _this18._sendMessage('select_language', _this18._language);
        return _this18._nextMessage();
      }).then(function (msg) {
        if (msg.command !== 'ready') {
          _this18._authFailed = true;
          if (msg.command === 'error') {
            throw new Error(msg.data);
          }
          throw new Error('Expected "ready" command but got: ' + msg.command);
        }

        _this18._ping();
      });
    }
  }, {
    key: '_sendMessage',
    value: function _sendMessage(command) {
      var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';

      var msg = { command: command, data: data };
      this._ws.send(JSON.stringify(msg));
    }
  }, {
    key: '_messageHandler',
    value: function _messageHandler(e) {
      var msg = parseJSON(e.data);

      if (msg.command === 'pong') {
        this._ping();
        return;
      } else if (msg.command === 'ping') {
        this._pong();
        return;
      } else if (msg.command === 'output') {
        if (this._stdout != null) {
          this._stdout(msg.data);
        }
        return;
      } else if (msg.command === 'files') {
        this.emit('files', msg);
        return;
      } else if (msg.command === 'clearConsole') {
        this.emit('clearConsole');
        return;
      } else if (msg.command.indexOf('event:') === 0) {
        this.emit(msg.command, msg);
        return;
      }

      var found = null;
      var _iteratorNormalCompletion2 = true;
      var _didIteratorError2 = false;
      var _iteratorError2 = undefined;

      try {
        for (var _iterator2 = this._msgResolvers[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
          var resolver = _step2.value;
          var _commandRe2 = resolver.commandRe,
              _resolve2 = resolver.resolve;


          if (msg.command.match(_commandRe2)) {
            found = resolver;
            _resolve2(msg);
            break;
          }
        }
      } catch (err) {
        _didIteratorError2 = true;
        _iteratorError2 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion2 && _iterator2.return) {
            _iterator2.return();
          }
        } finally {
          if (_didIteratorError2) {
            throw _iteratorError2;
          }
        }
      }

      if (found) {
        this._msgResolvers.splice(this._msgResolvers.indexOf(found), 1);
      } else {
        this._msgQueue.push(msg);
      }
    }
  }, {
    key: '_waitForLintResult',
    value: function _waitForLintResult() {
      return this._onMessage(/^lintResult$/);
    }
  }, {
    key: '_waitForFormatResult',
    value: function _waitForFormatResult() {
      return this._onMessage(/^formatResult$/).then(function (msg) {
        if (msg.command !== 'formatResult') {
          throw new Error('Expected formatResult but got: ' + msg.command);
        }

        return msg;
      });
    }
  }, {
    key: '_waitForResult',
    value: function _waitForResult() {
      return this._onMessage(/^result$/).then(function (msg) {
        if (msg.command === 'skip') {
          return { command: 'result', data: '' };
        }

        return msg;
      });
    }
  }, {
    key: '_waitForFileResult',
    value: function _waitForFileResult() {
      return this._onMessage(/^file:.+/);
    }
  }, {
    key: '_waitForRemoveFileResult',
    value: function _waitForRemoveFileResult() {
      return this._onMessage(/^removeFile$/);
    }
  }, {
    key: '_closeHandler',
    value: function _closeHandler(e) {
      var _this19 = this;

      this._init();
      this._retries -= 1;
      clearTimeout(this._pingTimer);

      if (this._authFailed) {
        this._retries = 0;
      }

      if (this._retries < 1) {
        this.emit('disconnected', { retry: false });
        this._connecting = null;
        this._retries = 0;
        return Promise.reject(e);
      } else {
        var delay = (RETRIES - this._retries) * DELAY;
        this.emit('disconnected', { retry: true, delay: delay });
        this._connecting = new Promise(function (resolve, reject) {
          setTimeout(function () {
            return _this19._connect().then(resolve, reject);
          }, delay);
        });
        return this._connecting.catch(function (e) {
          return _this19._closeHandler(e);
        });
      }
    }
  }, {
    key: '_ping',
    value: function _ping() {
      var _this20 = this;

      this._pingTimer = setTimeout(function () {
        _this20._sendMessage('ping');
      }, 10 * 1000);
    }
  }, {
    key: '_pong',
    value: function _pong() {
      this._sendMessage('pong');
    }
  }]);

  return ReplitClient;
}(EventEmitter);

/**
 * debug = Debugger(client);
 * debug.step().then(..) // returns {stack, done}
 * debug.result().tnen(..) // returns result
 */
var Debugger = function (_EventEmitter2) {
  _inherits(Debugger, _EventEmitter2);

  function Debugger(client, loading, code) {
    _classCallCheck(this, Debugger);

    var _this21 = _possibleConstructorReturn(this, (Debugger.__proto__ || Object.getPrototypeOf(Debugger)).call(this));

    _this21._client = client;
    _this21._stack = [];

    _this21._ready = loading.then(function () {
      _this21._client._sendMessage('debuggerStart', code);
      return _this21._client._onMessage(/^debuggerReady/);
    });
    return _this21;
  }

  _createClass(Debugger, [{
    key: 'step',
    value: function step() {
      var _this22 = this;

      return this._ready.then(function () {
        _this22._client._sendMessage('debuggerStep');
        return _this22._client._onMessage(/^debuggerBreak/);
      }).then(function (msg) {
        var info = parseJSON(msg.data);
        _this22._stack = info.stack;
        return info;
      });
    }
  }, {
    key: 'stepOver',
    value: function stepOver() {
      var _this23 = this;

      if (this._stack.length === 0) {
        // First call nothing to step over.
        return this.step();
      }

      var currentStack = this._stack;
      var stepUntil = function stepUntil() {
        return _this23.step().then(function (info) {
          if (info.done) {
            return info;
          }

          if (info.stack.length === currentStack.length || info.stack.length < currentStack.length) {
            return info;
          }

          return stepUntil();
        });
      };

      return stepUntil();
    }
  }, {
    key: 'stepOut',
    value: function stepOut() {
      var _this24 = this;

      // Note: if this is the first call (i.e. stack.length is 0) it will step
      // through the entire program which is probably expected?
      var currentStack = this._stack;
      var stepUntil = function stepUntil() {
        return _this24.step().then(function (info) {
          if (info.done) {
            return info;
          }

          if (info.stack.length < currentStack.length) {
            return info;
          }

          return stepUntil();
        });
      };

      return stepUntil();
    }
  }, {
    key: 'resume',
    value: function resume() {
      var _this25 = this;

      var stepUntil = function stepUntil() {
        return _this25.step().then(function (info) {
          if (info.done) {
            return _this25.result();
          }
          return stepUntil();
        });
      };

      return stepUntil();
    }
  }, {
    key: 'result',
    value: function result() {
      var _this26 = this;

      return this._ready.then(function () {
        return _this26._client._onMessage(/^result$/);
      }).finally(function () {
        _this26.emit('done');
      });
    }
  }]);

  return Debugger;
}(EventEmitter);

function errMsgFromCloseEvent(e) {
  var msg = e.reason || e.type || '';
  if (e.code) {
    msg += ': ' + e.code;
  }
  return msg;
}

function parseJSON(json) {
  try {
    return JSON.parse(json);
  } catch (e) {
    throw new JSONParseError('Error parsing: ' + json + '\n' + e.message);
  }
}

var HandshakeError = function (_Error) {
  _inherits(HandshakeError, _Error);

  function HandshakeError(message) {
    _classCallCheck(this, HandshakeError);

    var _this27 = _possibleConstructorReturn(this, (HandshakeError.__proto__ || Object.getPrototypeOf(HandshakeError)).call(this, message));

    _this27.name = 'HandshakeError';
    return _this27;
  }

  return HandshakeError;
}(Error);

var JSONParseError = function (_Error2) {
  _inherits(JSONParseError, _Error2);

  function JSONParseError(message) {
    _classCallCheck(this, JSONParseError);

    var _this28 = _possibleConstructorReturn(this, (JSONParseError.__proto__ || Object.getPrototypeOf(JSONParseError)).call(this, message));

    _this28.name = 'JSONParseError';
    return _this28;
  }

  return JSONParseError;
}(Error);

module.exports = ReplitClient;

