/*
 *  This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
 *  License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
 */

/*
X11 server channel.

TODO:
  - [ ] other user activity
  - [ ] when stopping project, kill xpra's
*/

import { spawn, SpawnOptions } from "child_process";
import { callback } from "awaiting";
import abspath from "@cocalc/backend/misc/abspath";
import { path_split } from "@cocalc/util/misc";
import { clone } from "underscore";

const x11_channels = {};

// this is used to map a (not necessarily) running process to a path for the "project info"
const pid2path: { [pid: number]: string } = {};

export function get_path_for_pid(pid: number) {
  return pid2path[pid];
}

class X11Channel {
  logger: any;
  display: number;
  path: string;
  name: string;
  channel: any;
  clip: any;

  constructor({
    primus,
    path,
    name,
    logger,
    display,
  }: {
    primus: any;
    path: string;
    name: string;
    logger: any;
    display: number;
  }) {
    this.logger = logger;
    this.log("creating new x11 channel");
    this.display = display; // needed for copy/paste support
    this.path = path;
    this.name = name;
    this.channel = primus.channel(this.name);
    this.init_handlers();
  }

  log(...args): void {
    this.logger.debug(`x11 channel ${this.path} -- `, ...args);
  }

  new_connection(spark: any): void {
    if (this.channel === undefined) {
      return;
    }
    // Now handle the connection
    this.log(`new connection from ${spark.address.ip} -- ${spark.id}`);
    spark.on("data", async (data) => {
      try {
        await this.handle_data(spark, data);
      } catch (err) {
        spark.write({ error: `error handling command -- ${err}` });
      }
    });
  }

  init_handlers(): void {
    this.channel.on("connection", this.new_connection.bind(this));
  }

  async handle_data(_, data): Promise<void> {
    this.log("handle_data ", data);
    if (typeof data !== "object") {
      return; // nothing defined yet
    }

    switch (data.cmd) {
      case "paste":
        await this.paste(data.value, data.wid ? data.wid : 0);
        break;
      case "launch":
        await this.launch(data.command, data.args);
        break;
      default:
        throw Error("WARNING: unknown command -- " + data.cmd);
    }
  }

  async paste(value: string, wid: number): Promise<void> {
    this.log("paste", value, wid);
    await this.set_clipboard(value);
    await this.cause_paste(wid);
  }

  async set_clipboard(value: string): Promise<void> {
    this.log("set_clipboard to string of length", value.length);
    const p = spawn("xclip", [
      "-selection",
      "clipboard",
      "-d",
      `:${this.display}`,
    ]);
    p.stdin.write(value);
    p.stdin.end();
    // wait for exit event.
    await callback((cb) => p.on("exit", cb));
  }

  cause_paste(wid: number): void {
    this.log("paste to window ", wid);
    const env = { DISPLAY: `:${this.display}` };
    const args: string[] = ["key", "Control_L+v"];
    if (wid) {
      args.push("--window");
      args.push(`${wid}`);
    }
    this.log("xdotool", args);
    spawn("xdotool", args, { env }).on("close", (code) => {
      console.log(`xdotool exited with code ${code}`);
    });
  }

  // launch a command and detach -- used to start x11 applications running.
  launch(command: string, args?: string[]): void {
    const env = clone(process.env);
    env.DISPLAY = `:${this.display}`;
    const cwd = this.get_cwd();
    const options: SpawnOptions = { cwd, env, detached: true, stdio: "ignore" };
    args = args != null ? args : [];
    try {
      const sub = spawn(command, args, options);
      sub.unref();
      pid2path[sub.pid] = this.path;
      sub.on("exit", () => {
        delete pid2path[sub.pid];
      });
    } catch (err) {
      this.channel.write({
        error: `error launching ${command} -- ${err}`,
      });
      return;
    }
  }

  private get_cwd(): string {
    return path_split(abspath(this.path)).head; // containing path
  }
}

export async function x11_channel(
  _: any,
  primus: any,
  logger: any,
  path: string,
  display: number
): Promise<string> {
  const name = `x11:${path}`;
  if (x11_channels[name] === undefined) {
    x11_channels[name] = new X11Channel({
      primus,
      path,
      name,
      logger,
      display,
    });
  }
  return name;
}
