/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

import fs from 'fs';

import yargs from 'yargs';

import {Command} from './Command';
import {Event} from './Event';
import {Graph} from './Graph';
import {Property} from './Property';
import {PropsType, Type} from './Type';

import {HeaderWriter} from './HeaderWriter';
import {ImplementationWriter} from './ImplementationWriter';

// $FlowFixMe[cannot-resolve-module] : this isn't a module, just a JSON file.
const standard = require('devtools-protocol/json/js_protocol.json');

const custom = require('../src/custom.json');

type Descriptor = {|
  types: Array<Type>,
  commands: Array<Command>,
  events: Array<Event>,
|};

function mergeDomains(original: any, extra: any) {
  return {...original, domains: original.domains.concat(extra.domains)};
}

const proto = mergeDomains(standard, custom);

function parseDomains(
  domainObjs: Array<any>,
  ignoreExperimental: boolean,
  includeExperimental: Set<string>,
): Descriptor {
  const desc = {
    types: [],
    commands: [],
    events: [],
  };

  for (const obj of domainObjs) {
    const domain = obj.domain;

    for (const typeObj of obj.types || []) {
      const type = Type.create(domain, typeObj, ignoreExperimental);
      if (type) {
        desc.types.push(type);
      }
    }

    for (const commandObj of obj.commands || []) {
      const command = Command.create(
        domain,
        commandObj,
        !includeExperimental.has(`${domain}.${commandObj.name}`) &&
          ignoreExperimental,
      );
      if (command) {
        desc.commands.push(command);
      }
    }

    for (const eventObj of obj.events || []) {
      const event = Event.create(domain, eventObj, ignoreExperimental);
      if (event) {
        desc.events.push(event);
      }
    }
  }

  return desc;
}

function buildGraph(desc: Descriptor): Graph {
  const graph = new Graph();

  const types = desc.types;
  const commands = desc.commands;
  const events = desc.events;

  const maybeAddPropEdges = function (nodeId: string, props: ?Array<Property>) {
    if (props) {
      for (const prop of props) {
        const refId = prop.getRefDebuggerName();
        (prop: Object).recursive = refId && refId === nodeId;
        if (refId && refId !== nodeId) {
          // Don't add edges for recursive properties.
          graph.addEdge(nodeId, refId);
        }
      }
    }
  };

  for (const type of types) {
    graph.addNode(type.getDebuggerName());

    if (type instanceof PropsType) {
      maybeAddPropEdges(type.getDebuggerName(), type.properties);
    }
  }

  for (const command of commands) {
    graph.addNode(command.getDebuggerName());

    maybeAddPropEdges(command.getDebuggerName(), command.parameters);
    maybeAddPropEdges(command.getDebuggerName(), command.returns);
  }

  for (const event of events) {
    graph.addNode(event.getDebuggerName());

    maybeAddPropEdges(event.getDebuggerName(), event.parameters);
  }

  return graph;
}

function parseRoots(desc: Descriptor, rootsPath: ?string): Array<string> {
  const roots = [];

  if (rootsPath) {
    const buf = fs.readFileSync(rootsPath);
    for (let line of buf.toString().split('\n')) {
      line = line.trim();

      // ignore comments and blank lines
      if (!line.match(/\s*#/) && line.length > 0) {
        roots.push(line);
      }
    }
  } else {
    for (const type of desc.types) {
      roots.push(type.getDebuggerName());
    }
    for (const command of desc.commands) {
      roots.push(command.getDebuggerName());
    }
    for (const event of desc.events) {
      roots.push(event.getDebuggerName());
    }
  }

  return roots;
}

// only include types, commands, events that can be reached from the given
// root messages
function filterReachableFromRoots(
  desc: Descriptor,
  graph: Graph,
  roots: Array<string>,
): Descriptor {
  const topoSortedIds = graph.traverse(roots);

  // Types can include other types by value, so they need to be topologically
  // sorted in the header.
  const typeMap: Map<string, Type> = new Map();
  for (const type of desc.types) {
    typeMap.set(type.getDebuggerName(), type);
  }

  const types = [];
  for (const id of topoSortedIds) {
    const type = typeMap.get(id);
    if (type) {
      types.push(type);
    }
  }

  // Commands and events don't depend on each other, so just emit them in the
  // order we got them from the JSON file.
  const ids = new Set(topoSortedIds);
  const commands = desc.commands.filter(cmd => ids.has(cmd.getDebuggerName()));
  const events = desc.events.filter(event => ids.has(event.getDebuggerName()));

  // Sort commands and events so the code is easier to read. Types have to be
  // topologically sorted as explained above.
  const comparator = (a: Command | Event, b: Command | Event) => {
    const id1 = a.getDebuggerName();
    const id2 = b.getDebuggerName();
    return id1 < id2 ? -1 : id1 > id2 ? 1 : 0;
  };
  commands.sort(comparator);
  events.sort(comparator);

  return {types, commands, events};
}

function main() {
  const args = yargs
    .usage('Usage: msggen <header_path> <cpp_path>')
    .alias('h', 'help')
    .help('h')
    .boolean('e')
    .alias('e', 'ignore-experimental')
    .describe('e', 'ignore experimental commands, props, and types')
    .alias('i', 'include-experimental')
    .describe('i', 'experimental commands to include')
    .alias('r', 'roots')
    .describe('r', 'path to a file listing root types, events, and commands')
    .nargs('r', 1)
    .demandCommand(2, 2).argv;

  const ignoreExperimental = !!args.e;
  const includeExperimental = new Set(
    typeof args.i === 'string' ? args.i.split(',') : [],
  );
  const [headerPath, implPath] = args._;

  const headerStream = fs.createWriteStream(headerPath);
  const implStream = fs.createWriteStream(implPath);

  const desc = parseDomains(
    proto.domains,
    ignoreExperimental,
    includeExperimental,
  );
  const graph = buildGraph(desc);
  const roots = parseRoots(desc, String(args.roots));

  const reachable = filterReachableFromRoots(desc, graph, roots);

  const hw = new HeaderWriter(
    headerStream,
    reachable.types,
    reachable.commands,
    reachable.events,
  );
  hw.write();

  const iw = new ImplementationWriter(
    implStream,
    reachable.types,
    reachable.commands,
    reachable.events,
  );
  iw.write();
}

main();
