/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { dumpn } = require("resource://devtools/shared/DevToolsUtils.js");
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const {
  getFileForBinary,
} = require("resource://devtools/client/shared/remote-debugging/adb/adb-binary.js");
const { setTimeout } = ChromeUtils.importESModule(
  "resource://gre/modules/Timer.sys.mjs"
);

loader.lazyRequireGetter(
  this,
  "runCommand",
  "resource://devtools/client/shared/remote-debugging/adb/commands/index.js",
  true
);
loader.lazyRequireGetter(
  this,
  "check",
  "resource://devtools/client/shared/remote-debugging/adb/adb-running-checker.js",
  true
);

// Waits until a predicate returns true or re-tries the predicate calls
// |retry| times, we wait for 100ms between each calls.
async function waitUntil(predicate, retry = 20) {
  let count = 0;
  while (count++ < retry) {
    if (await predicate()) {
      return true;
    }
    // Wait for 100 milliseconds.
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  // Timed out after trying too many times.
  return false;
}

// Class representing the ADB process.
class AdbProcess extends EventEmitter {
  constructor() {
    super();

    this._ready = false;
    this._didRunInitially = false;
  }

  get ready() {
    return this._ready;
  }

  _getAdbFile() {
    if (this._adbFilePromise) {
      return this._adbFilePromise;
    }
    this._adbFilePromise = getFileForBinary();
    return this._adbFilePromise;
  }

  async _runProcess(process, params) {
    return new Promise((resolve, reject) => {
      process.runAsync(
        params,
        params.length,
        {
          observe(subject, topic) {
            switch (topic) {
              case "process-finished":
                resolve();
                break;
              case "process-failed":
                reject();
                break;
            }
          },
        },
        false
      );
    });
  }

  // We startup by launching adb in server mode, and setting
  // the tcp socket preference to |true|
  async start() {
    const onSuccessfulStart = () => {
      this._ready = true;
      this.emit("adb-ready");
    };

    const isAdbRunning = await check();
    if (isAdbRunning) {
      dumpn("Found ADB process running, not restarting");
      onSuccessfulStart();
      return;
    }
    dumpn("Didn't find ADB process running, restarting");

    this._didRunInitially = true;
    const process = Cc["@mozilla.org/process/util;1"].createInstance(
      Ci.nsIProcess
    );

    // FIXME: Bug 1481691 - We should avoid extracting files every time.
    const adbFile = await this._getAdbFile();
    process.init(adbFile);
    // Hide command prompt window on Windows
    process.startHidden = true;
    process.noShell = true;
    const params = ["start-server"];
    let isStarted = false;
    try {
      await this._runProcess(process, params);
      isStarted = await waitUntil(check);
    } catch (e) {}

    if (isStarted) {
      onSuccessfulStart();
    } else {
      this._ready = false;
      throw new Error("ADB Process didn't start");
    }
  }

  /**
   * Stop the ADB server, but only if we started it.  If it was started before
   * us, we return immediately.
   */
  async stop() {
    if (!this._didRunInitially) {
      return; // We didn't start the server, nothing to do
    }
    await this.kill();
  }

  /**
   * Kill the ADB server.
   */
  async kill() {
    try {
      await runCommand("host:kill");
    } catch (e) {
      dumpn("Failed to send host:kill command");
    }
    dumpn("adb server was terminated by host:kill");
    this._ready = false;
    this._didRunInitially = false;
  }
}

exports.adbProcess = new AdbProcess();
