const puppeteer = require('puppeteer');
const path = require('path');
const EventEmitter = require('events');

const { RoomController } = require('./room');
const logger = require('./logger');
const versionConfig = require('../version-config.json');
const { mkdirSync } = require('fs');

/**
 * Emitted when new RoomController is added.
 * @event Haxroomie#room-added
 * @param {RoomController} room - The added RoomController.
 */

/**
 * Emitted when RoomController is removed.
 * @event Haxroomie#room-removed
 * @param {RoomController} room - The removed RoomController.
 */

// allow only launching one browser
let browserLock = undefined;

/**
 * Class for spawning the headless chrome browser and managing
 * [RoomControllers]{@link RoomController}.
 *
 * Each [RoomController]{@link RoomController} controls one room
 * running in a browsers tab.
 *
 * After creating the Haxroomie instance it is required to launch the browser
 * with the [launchBrowser method]{@link Haxroomie#launchBrowser} before
 * anything else.
 */
class Haxroomie extends EventEmitter {
  /**
   * Constructor for Haxroomie.
   *
   * @param {object} [options] - options
   * @param {object} [options.viewport={ width: 400, height: 500 }] - Viewport
   *    size settings for the browser.
   * @param {number} [options.port=3066] - Port that the headless browser will use
   *    as the remote-debugging-port to communicate with Haxroomie. Use a
   *    port that is not open outside your LAN!
   * @param {boolean} [options.noSandbox=false] - Makes the browser run without
   *    sandbox. Useful only if it gives you error in sandboxed mode. It is
   *    not recommended to set this true for security reasons.
   * @param {boolean} [options.headless=true] - Setting this to false will make
   *    puppeteer try to spawn a browser window. Useful for debugging.
   * @param {boolean} [options.userDataDir] - Path to where
   *    browser should store data like localStorage. Defaults to [project
   *    root directory]/user-data-dir.
   * @param {boolean} [options.timeout=30] - How long to wait for a room to open
   *    before failing.
   * @param {string} [options.executablePath] - Path to chrome launcher.
   * @param {string} [options.downloadDirectory] - Directory to where the files
   *    downloaded from the browser are saved.
   * @param {array} [options.chromiumArgs] - Additional arguments for the
   *    chromium browser.
   */
  constructor({
    viewport = { width: 400, height: 500 },
    port = 3066,
    noSandbox = false,
    headless = true,
    userDataDir = path.join(__dirname, '..', 'user-data-dir'),
    timeout = 30,
    executablePath,
    downloadDirectory,
    chromiumArgs,
  } = {}) {
    super();
    if (!downloadDirectory) {
      throw new Error('Missing argument: downloadDirectory');
    }
    this.browser = null;
    this.rooms = new Map();

    this.viewport = viewport;
    this.port = port;
    if (this.port === 0) {
      throw new Error('INVALID_PORT: 0');
    }
    this.downloadDirectory = downloadDirectory;
    this.noSandbox = noSandbox;
    this.headless = headless;
    this.userDataDir = userDataDir;
    this.userDataDir = path.resolve(process.cwd(), this.userDataDir);
    try {
      mkdirSync(this.userDataDir, { recursive: true });
    } catch (err) {
      // Don't do anything. Directory probably exists already.
    }
    this.timeout = timeout;
    this.executablePath = executablePath
      ? path.resolve(process.cwd(), executablePath)
      : undefined;
    this.chromiumArgs = chromiumArgs;
  }

  /**
   * Launches the puppeteer controlled browser using the remote-debugging-port
   * given in Haxroomie classes constructor. It is only possible to launch one
   * browser.
   */
  async launchBrowser() {
    // make sure there isnt a browser running already
    let browser = await this.getRunningBrowser();
    // if there is a browser running throw an error
    if (browser || browserLock)
      throw new Error('You can launch only 1 browser!');

    browserLock = true;

    let browserArgs = [
      `--remote-debugging-port=${this.port}`,
      `--disable-features=WebRtcHideLocalIpsWithMdns`,
    ];

    if (this.noSandbox) {
      browserArgs.push('--no-sandbox');
      browserArgs.push('--disable-setuid-sandbox');
    }

    if (this.chromiumArgs) {
      browserArgs.push(...this.chromiumArgs);
    }

    let launchOptions = {
      headless: this.headless,
      devtools: !this.headless,
      userDataDir: this.userDataDir,
      args: browserArgs,
    };
    if (this.executablePath) launchOptions.executablePath = this.executablePath;

    this.browser = await puppeteer.launch(launchOptions);
    return this.browser;
  }

  /**
   * @private
   */
  async getRunningBrowser() {
    try {
      this.browser = await puppeteer.connect({
        browserURL: `http://localhost:${this.port}`,
      });
    } catch (err) {
      return null;
    }
    return this.browser;
  }

  /**
   * Closes the puppeteer controlled browser.
   */
  async closeBrowser() {
    if (this.browser) await this.browser.close();
    this.rooms = new Map();
    browserLock = false;
    this.browser = null;
  }

  /**
   * Checks that the instance has a connection to the browser.
   * @private
   */
  ensureInstanceIsUsable() {
    if (!this.browser) {
      throw new Error(`Browser is not running!`);
    }
  }

  /**
   * Validates the given id.
   * @param {string|number} id - ID to validate.
   * @private
   */
  validateRoomID(id) {
    if (
      (!id && id !== 0) ||
      (typeof id !== 'number' && typeof id !== 'string')
    ) {
      throw new Error('invalid id');
    }
  }

  /**
   * Checks if there is a room running with the given id.
   *
   * @param {string|number} id - An id of the room.
   * @returns {boolean} - Is there a room with given id?
   */
  hasRoom(id) {
    this.validateRoomID(id);
    this.ensureInstanceIsUsable();
    return this.rooms.has(id);
  }
  /**
   * Returns a RoomController with the given id.
   *
   * @param {string|number} id - An id of the room.
   * @returns {RoomController} - RoomController with the given id or
   *    undefined if there is no such room.
   */
  getRoom(id) {
    this.validateRoomID(id);
    this.ensureInstanceIsUsable();
    return this.rooms.get(id);
  }

  /**
   * Returns an array of available RoomControllers.
   * @returns {Array.<RoomController>} - Available RoomControllers.
   */
  getRooms() {
    let rooms = [];
    for (let r of this.rooms.values()) {
      rooms.push(r);
    }
    return rooms;
  }

  /**
   * Returns the RoomController that was first added.
   * @returns {RoomController} - First RoomController or
   *    undefined if there is no such room.
   */
  getFirstRoom() {
    for (let r of this.rooms.values()) {
      return r;
    }
  }

  /**
   * Removes a RoomController with the given id.
   *
   * Removing deletes the RoomController and closes the browser tab
   * it is controlling.
   *
   * @param {string|number} id
   */
  async removeRoom(id) {
    this.validateRoomID(id);
    this.ensureInstanceIsUsable();
    let roomController = this.rooms.get(id);
    if (roomController) {
      try {
        await roomController.page.close();
      } catch (err) {
        logger.debug(err);
      }
      this.rooms.delete(id);
      this.emit('room-removed', roomController);
    }
  }

  /**
   * Tests if the parameter is an instance of RoomController.
   * @param {*} roomController
   * @private
   */
  isRoomController(roomController) {
    return (
      typeof roomController === 'object' &&
      roomController instanceof RoomController
    );
  }
  /**
   * Adds a new RoomController.
   *
   * If `roomController` is a string or number, then it will be used as
   * an id for the new RoomController.
   *
   * @param {RoomController|string|number} roomController - Instance of
   *    RoomController or id for the RoomController.
   * @param {object} [roomControllerOptions] - Additional options for the
   *    [RoomController constructor]{@link RoomController#constructor} if
   *    `roomController` is an id.
   * @return {RoomController} - The created RoomController.
   */
  async addRoom(roomController, roomControllerOptions) {
    this.ensureInstanceIsUsable();

    if (this.isRoomController(roomController)) {
      if (this.rooms.has(roomController.id)) {
        throw new Error('id must be unique');
      }
      // Set the download path.
      await roomController.page._client.send('Page.setDownloadBehavior', {
        behavior: 'allow',
        downloadPath: this.downloadDirectory,
      });
      this.rooms.set(roomController.id, roomController);
      this.emit('room-added', roomController);
      return roomController;
    }

    const id = roomController;

    this.validateRoomID(id);
    if (this.rooms.has(id)) throw new Error('id must be unique');

    const rcOptions = { id: id, ...roomControllerOptions };
    rcOptions.hhmVersion = rcOptions.hhmVersion || versionConfig.hhmVersion;
    rcOptions.defaultRepoVersion =
      rcOptions.defaultRepoVersion || versionConfig.defaultRepoVersion;

    const room = await this.createRoomController(rcOptions);
    // Set the download path.
    await room.page._client.send('Page.setDownloadBehavior', {
      behavior: 'allow',
      downloadPath: this.downloadDirectory,
    });
    this.rooms.set(id, room);
    this.emit('room-added', room);
    return room;
  }

  /**
   * Returns a new Puppeteer.Page object.
   * @private
   */
  async getNewPage() {
    return this.browser.newPage();
  }

  /**
   * Factory method for creating RoomController instances.
   * @private
   */
  async createRoomController(rcOptions) {
    const page = await this.getNewPage();
    const device = {
      name: 'Galaxy S5',
      userAgent:
        'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3452.0 Mobile Safari/537.36',
      viewport: {
        width: this.viewport.width,
        height: this.viewport.height,
        deviceScaleFactor: 1,
        isMobile: false,
        hasTouch: false,
        isLandscape: false,
      },
    };

    await page.emulate(device);

    let room = new RoomController({
      timeout: this.timeout,
      page,
      ...rcOptions,
    });

    return room;
  }
}

module.exports = Haxroomie;