/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const Bundle = require('../Bundler/Bundle');
const PrepackBundle = require('../Bundler/PrepackBundle');
const Promise = require('promise');
const bser = require('bser');
const debug = require('debug')('ReactNativePackager:SocketClient');
const fs = require('fs');
const net = require('net');
const path  = require('path');
const tmpdir = require('os').tmpdir();

const LOG_PATH = path.join(tmpdir, 'react-packager.log');

class SocketClient {
  static create(sockPath) {
    return new SocketClient(sockPath).onReady();
  }

  constructor(sockPath) {
    debug('connecting to', sockPath);

    this._sock = net.connect(sockPath);
    this._ready = new Promise((resolve, reject) => {
      this._sock.on('connect', () => {
        this._sock.removeAllListeners('error');
        process.on('uncaughtException', (error) => {
          console.error('uncaught error', error.stack);
          setImmediate(() => process.exit(1));
        });
        resolve(this);
      });
      this._sock.on('error', (e) => {
        e.message = `Error connecting to server on ${sockPath} ` +
                    `with error: ${e.message}`;
        e.message += getServerLogs();

        reject(e);
      });
    });

    this._resolvers = Object.create(null);
    const bunser = new bser.BunserBuf();
    this._sock.on('data', (buf) => bunser.append(buf));
    bunser.on('value', (message) => this._handleMessage(message));

    this._sock.on('close', () => {
      if (!this._closing) {
        const terminate = (result) => {
          const sockPathExists = fs.existsSync(sockPath);
          throw new Error(
            'Server closed unexpectedly.\n' +
            'Server ping connection attempt result: ' + result + '\n' +
            'Socket path: `' + sockPath + '` ' +
            (sockPathExists ? ' exists.' : 'doesn\'t exist') + '\n' +
            getServerLogs()
          );
        };

        // before throwing ping the server to see if it's still alive
        const socket = net.connect(sockPath);
        socket.on('connect', () => {
          socket.end();
          terminate('OK');
        });
        socket.on('error', error => terminate(error));
      }
    });
  }

  onReady() {
    return this._ready;
  }

  getDependencies(main) {
    return this._send({
      type: 'getDependencies',
      data: main,
    });
  }

  getOrderedDependencyPaths(main) {
    return this._send({
      type: 'getOrderedDependencyPaths',
      data: main,
    });
  }

  buildBundle(options) {
    return this._send({
      type: 'buildBundle',
      data: options,
    }).then(json => Bundle.fromJSON(json));
  }

  buildPrepackBundle(options) {
    return this._send({
      type: 'buildPrepackBundle',
      data: options,
    }).then(json => PrepackBundle.fromJSON(json));
  }

  _send(message) {
    message.id = uid();
    this._sock.write(bser.dumpToBuffer(message));
    return new Promise((resolve, reject) => {
      this._resolvers[message.id] = {resolve, reject};
    });
  }

  _handleMessage(message) {
    if (!(message && message.id && message.type)) {
      throw new Error(
        'Malformed message from server ' + JSON.stringify(message)
      );
    }

    debug('got message with type', message.type);

    const resolver = this._resolvers[message.id];
    if (!resolver) {
      throw new Error(
        'Unrecognized message id (' + message.id + ') ' +
        'message already resolved or never existed.'
      );
    }

    delete this._resolvers[message.id];

    if (message.type === 'error') {
      const errorLog =
        message.data && message.data.indexOf('TimeoutError') === -1
          ? 'See logs ' + LOG_PATH
          : getServerLogs();

      resolver.reject(new Error(message.data + '\n' + errorLog));
    } else {
      resolver.resolve(message.data);
    }
  }

  close() {
    debug('closing connection');
    this._closing = true;
    this._sock.end();
  }
}

module.exports = SocketClient;

function uid(len) {
  len = len || 7;
  return Math.random().toString(35).substr(2, len);
}

function getServerLogs() {
  if (fs.existsSync(LOG_PATH)) {
    return '\nServer logs:\n' + fs.readFileSync(LOG_PATH, 'utf8');
  }

  return '';
}
