#!/usr/bin/env python
# Copyright 2018 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

### generate Rust FIDL facade crates

import argparse
import datetime
import json
import os
import re
import subprocess
import shutil
import sys

ROOT_PATH = os.path.abspath(__file__ + "/../../..")
TOML_DEFAULT_PATH = os.path.join(ROOT_PATH, "garnet", "rust_fidl_crates.json")
WORKSPACE_TOML_DEFAULT_PATH = os.path.join(ROOT_PATH, "garnet", "Cargo.toml")
OUT_PUB_DEFAULT_PATH = os.path.join(ROOT_PATH, "garnet", "public", "rust", "fidl_crates")
OUT_PRIV_DEFAULT_PATH = os.path.join(ROOT_PATH, "garnet", "lib", "rust", "fidl_crates")

BUILD_GN_CONTENTS = '''\
# Copyright %(year)s The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import("//build/rust/rust_library.gni")
import("//build/rust/toolchain.gni")

rust_library("%(crate_name)s") {
  deps = [
    "//%(fidl_target)s:%(fidl_label)s($rust_toolchain)",
  ]
}
'''

CARGO_TOML_CONTENTS = '''\
# Copyright %(year)s The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
[package]
name = "%(crate_name)s"
version = "0.1.0"
license = "BSD-3-Clause"
authors = ["rust-fuchsia@fuchsia.com"]
description = "Generated FIDL interface"
repository = "https://fuchsia.googlesource.com/garnet/"

[dependencies]
fidl = "0.1"
fuchsia-zircon = "0.3"
futures = "=0.2.0-alpha"
fuchsia-async = "0.1"
%(deps)s
'''

LIB_RS_CONTENTS = '''\
// Copyright %(year)s The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
include!(concat!(env!("FIDL_GEN_ROOT"), "/%(path)s"));
'''

def cur_year():
    return datetime.datetime.now().year

def rm_left_slashes(path):
    if path.startswith("//"):
        return path[len("//"):]
    else:
        return path

def ensure_dir(path):
    if not os.path.exists(path):
        os.makedirs(path)

def write_lib_rs(crate_path, lib_name, fidl_target_fs_path):
    ensure_dir(crate_path)
    src_path = os.path.join(crate_path, "src")
    ensure_dir(src_path)
    lib_rs_path = os.path.join(src_path, "lib.rs")
    path_to_generated_code = "%s/%s.fidl.rs" % (fidl_target_fs_path, lib_name)
    with open(lib_rs_path, "w") as f:
        f.write(LIB_RS_CONTENTS % { "year": cur_year(), "path": path_to_generated_code })

def write_cargo_toml_file(crate_path, crate_name, deps):
    cargo_toml_path = os.path.join(crate_path, "Cargo.toml")
    cargo_deps = "\n".join(map(lambda x: 'fidl_%s = "0.1"' % x, deps))
    with open(cargo_toml_path, "w") as f:
        f.write(CARGO_TOML_CONTENTS % {
            "crate_name": crate_name,
            "deps": cargo_deps,
            "year": cur_year()
        })

def write_build_gn_file(crate_path, crate_name, fidl_target_fs_path, fidl_target_label):
    build_gn_path = os.path.join(crate_path, "BUILD.gn")
    with open(build_gn_path, "w") as f:
        f.write(BUILD_GN_CONTENTS % {
            "crate_name": crate_name,
            "fidl_target": fidl_target_fs_path,
            "fidl_label": fidl_target_label,
            "year": cur_year(),
        })
    # Format the file
    command_args = [ os.path.join(ROOT_PATH, "scripts", "fx"), "gn", "format", build_gn_path]
    subprocess.check_call(command_args)

def write_facade(folder_path, lib_name, fidl_target, deps):
    ensure_dir(folder_path)
    fidl_target_sections = fidl_target.split(":")
    assert len(fidl_target_sections) == 2
    fidl_target_gn_path = fidl_target_sections[0]
    fidl_target_label = fidl_target_sections[1]
    fidl_target_fs_path = rm_left_slashes(fidl_target_gn_path)
    crate_name = "fidl_" + lib_name
    crate_path = os.path.join(folder_path, crate_name)
    write_lib_rs(crate_path, lib_name, fidl_target_fs_path)
    write_cargo_toml_file(crate_path, crate_name, deps)
    write_build_gn_file(crate_path, crate_name, fidl_target_fs_path, fidl_target_label)
    return (crate_name, crate_path)

def insert_alphabetically_between(lines, line_to_add, first_line_stripped, last_line_stripped):
    # Find the start of the area to insert
    i = 0
    while True:
        if len(lines) == i:
            assert False, "Start of alphabetical list not found"
        if "".join(lines[i].split()) == first_line_stripped:
            first_line = i + 1
            break
        i += 1

    line_to_add_stripped = "".join(line_to_add.split())

    i = first_line
    while i < len(lines):
        stripped_line = "".join(lines[i].split())
        if stripped_line == line_to_add_stripped:
            # If our line has already been added, we can return
            return lines

        if stripped_line == last_line_stripped or stripped_line > line_to_add_stripped:
            # We've found the first line that should be after our line alphabetically
            # Insert the line at the position just before the last line read
            lines.insert(i, line_to_add)

            # Remove any non-alphabetical copies of the entry we just added
            i += 1
            while i < len(lines):
                stripped_line = "".join(lines[i].split())
                if stripped_line == line_to_add_stripped:
                    lines.pop(i)
                else:
                    i += 1
            return lines

        i += 1

    if last_line_stripped != "":
        assert False, "Reached end of file before end of alphabetical list found"
    else:
        lines.append(line_to_add)
        return lines

def update_workspace_cargo_toml(workspace_toml_path, crate_names_and_paths):
    with open(workspace_toml_path, "r+") as f:
        content = f.readlines()
        for (crate_name, crate_path) in crate_names_and_paths:
            crate_rel_path = os.path.relpath(crate_path, os.path.join(workspace_toml_path, os.pardir))
            content = insert_alphabetically_between(
                    content, '  "%s",\n' % crate_rel_path, "members=[", "]")
            content = insert_alphabetically_between(
                    content, '%s = { path = "%s" }\n' % (crate_name, crate_rel_path), "[patch.crates-io]", "")
        f.seek(0)
        f.write("".join(content))
        f.truncate()

def main():
    parser = argparse.ArgumentParser("Generate Rust FIDL facade crates")
    parser.add_argument("--target-json",
                        help="Path to the JSON file listing the crates to generate",
                        default=TOML_DEFAULT_PATH)
    parser.add_argument("--out-pub",
                        help="Path to the output directory for public crates",
                        default=OUT_PUB_DEFAULT_PATH)
    parser.add_argument("--out-priv",
                        help="Path to the output directory for private crates",
                        default=OUT_PRIV_DEFAULT_PATH)
    parser.add_argument("--workspace-toml",
                        help="Path to the workspace Cargo.toml file",
                        default=WORKSPACE_TOML_DEFAULT_PATH)
    args = parser.parse_args()

    with open(args.target_json, "r") as file:
        target_json = json.load(file)

    shutil.rmtree(args.out_pub, ignore_errors=True, onerror=None)
    shutil.rmtree(args.out_priv, ignore_errors=True, onerror=None)

    crate_names_and_paths = []

    crate_name_to_direct_deps = {}
    for crate in target_json:
        crate_name_to_direct_deps[crate["library_name"]] = set(crate["library_deps"])

    crate_name_to_transitive_deps = {}
    for crate in target_json:
        deps = set()
        to_insert_deps_of = set()
        for dep in crate_name_to_direct_deps[crate["library_name"]]:
            deps.add(dep)
            to_insert_deps_of.update(crate_name_to_direct_deps[dep])
        while len(to_insert_deps_of) != 0:
            dep = to_insert_deps_of.pop()
            if not dep in deps:
                deps.add(dep)
                to_insert_deps_of.update(crate_name_to_direct_deps[dep])
        crate_name_to_transitive_deps[crate["library_name"]] = deps

    for crate in target_json:
        out_dir = args.out_pub if crate["public"] else args.out_priv
        crate_names_and_paths.append(
            write_facade(
                out_dir,
                crate["library_name"],
                crate["fidl_target"],
                crate_name_to_transitive_deps[crate["library_name"]]))

    update_workspace_cargo_toml(args.workspace_toml, crate_names_and_paths)
    return 0


if __name__ == '__main__':
    sys.exit(main())

