(**************************************************************************)
(*  Copyright © 2009-2013 Stéphane Glondu <steph@glondu.net>              *)
(*            © 2010-2013 Mehdi Dogguy <mehdi@dogguy.org>                 *)
(*                                                                        *)
(*  This program is free software: you can redistribute it and/or modify  *)
(*  it under the terms of the GNU Affero General Public License as        *)
(*  published by the Free Software Foundation, either version 3 of the    *)
(*  License, or (at your option) any later version, with the additional   *)
(*  exemption that compiling, linking, and/or using OpenSSL is allowed.   *)
(*                                                                        *)
(*  This program is distributed in the hope that it will be useful, but   *)
(*  WITHOUT ANY WARRANTY; without even the implied warranty of            *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *)
(*  Affero General Public License for more details.                       *)
(*                                                                        *)
(*  You should have received a copy of the GNU Affero General Public      *)
(*  License along with this program.  If not, see                         *)
(*  <http://www.gnu.org/licenses/>.                                       *)
(**************************************************************************)

open Printf
open Ocamlbuild_plugin

let name = "ben"

exception Subprocess_died_unexpectedly of Unix.process_status

let try_exec cmd =
  Sys.command (sprintf "%s >/dev/null 2>&1" cmd) = 0

let try_run cmd = (* reads one single line *)
  let in_c = Unix.open_process_in cmd in
  let line = input_line in_c in
  let _ = Unix.close_process_in in_c in
  line

let version = try_run "dpkg-parsechangelog -S Version"
let build_date =
  try
    let date = Sys.getenv "SOURCE_DATE_EPOCH" in
    let cmd = Printf.sprintf "date -d '@%s' +'%%F %%T %%Z'" date in
    try_run cmd
  with Not_found ->
    try_run "date +'%F %T %Z'"

let has_ocamlopt = try_exec "command -v ocamlopt"
let best =
  try Sys.getenv "OCAMLBEST"
  with Not_found -> if has_ocamlopt then "native" else "byte"
let main_executable = sprintf "bin/%s.%s" name best

let () =
  dispatch begin function

    | Before_options ->
        Options.use_ocamlfind := true;
        Options.use_menhir := true;
        Options.ocaml_yaccflags := ["--explain"]

    | After_rules ->
        Pathname.define_context "lib/benlib" ["lib"];
        Pathname.define_context "frontends" ["lib"];
        Pathname.define_context "templates" ["lib"];
        Pathname.define_context "bin" ["lib"; "frontends"];

        flag ["ocaml"; "compile"; "native"] (S[A"-inline"; A"1000"]);
        flag ["ocaml"; "link"; "native"] (S[A"-inline"; A"1000"]);

        (* Templates *)
        rule "template: cmx -> cmxs"
          ~prod:"%.cmxs"
          ~dep:"%.cmx"
          ~insert:`top
          ~doc:"This rule allows to build a .cmxs from a .cmx, without including all its dependencies." begin
            fun env _ ->
              let cmx = env "%.cmx" in
              let cmxs = env "%.cmxs" in
              Cmd(S[!Options.ocamlopt; A "-shared"; A "-I"; A "templates"; A cmx; A "-o"; A cmxs])
          end;

        (* C stubs *)
        flag ["link"; "library"; "ocaml"; "byte"; "use_libbenl"]
          (S[A"-dllib"; A"-lbenl"; A"-cclib"; A"-lbenl"]);
        flag ["link"; "library"; "ocaml"; "native"; "use_libbenl"]
          (S[A"-cclib"; A"-lbenl"]);
        flag ["link"; "library"; "ocaml"; "native"; "use_libbenl"]
          (S[A"-cclib"; A"-lbenl"]);
        flag ["link"; "program"; "ocaml"; "byte"; "use_libbenl"]
          (S[A"-I"; A"lib"; A"-dllib"; A"-lbenl"]);
        dep  ["link"; "ocaml"; "use_libbenl"] ["lib/libbenl.a"];

        rule "lib/benl_version.ml" ~deps:["lib/benl_version.mlp"] ~prod:"lib/benl_version.ml"
          begin
            fun _ _ ->
            Cmd
              (S [A"sed";
                  A"-e"; A (sprintf "s/@VERSION@/%s/" version);
                  A"-e"; A (sprintf "s/@BUILD_DATE@/%s/" build_date);
                  P"lib/benl_version.mlp"; Sh">"; P"lib/benl_version.ml"])
          end;

        (* rule for the main executable that will link all frontends  *)
        rule "bin/ben.ml" ~deps:["bin/ben.mlp"] ~prod:"bin/ben.ml" begin
          let prefix = name^"_" in
          let p = String.length prefix in
          fun _ _ ->
            let files = Array.to_list (Sys.readdir "../frontends") in
            let static = List.filter
              (fun x ->
                 let n = String.length x in
                 n > p+5 &&
                   String.sub x 0 p = prefix &&
                     String.sub x (n-3) 3 = ".ml")
              files
            in
            let static = List.map
              (fun x -> (Filename.chop_suffix x ".ml")^".frontend")
              static
            in
            let static =
              String.concat "; " (List.map String.capitalize_ascii static)
            in
            Cmd
              (S [A"sed";
                  A"-e"; A (sprintf "s/@STATIC_FRONTENDS@/%s/" static);
                  P"bin/ben.mlp"; Sh">"; P"bin/ben.ml"])
        end;

        rule "META" ~deps:["META.in"] ~prod:"META" begin
          fun _ _ ->
            Cmd
              (S [A"sed";
                  A"-e"; A (sprintf "s/@VERSION@/%s/" version);
                  P"META.in"; Sh">"; P"META"])

        end;
        (* rule for dependency graph *)
        rule "modules.dot" ~deps:[main_executable] ~prod:"modules.dot" begin
          fun _ _ ->
            let ic = Unix.open_process_in "find -name '*.ml.depends'" in
            let buf = Buffer.create 1024 in
            bprintf buf "digraph build_graph {\n";
            let rec loop accu =
              match (try Some (input_line ic) with End_of_file -> None) with
                | None -> accu
                | Some filename ->
                    let node =
                      let n = String.length filename in
                      let j = n - String.length ".ml.depends" in
                      let i =
                        try String.rindex_from filename j '/'
                        with Not_found -> 0
                      in
                      String.capitalize_ascii (String.sub filename (i+1) (j-i-1))
                    in
                    let deps = List.tl (string_list_of_file filename) in
                    loop ((node, deps)::accu)
            in
            let deps_assoc = loop [] in
            let module S = Set.Make(String) in
            let mynodes = List.fold_left
              (fun accu (x, _) -> S.add x accu) S.empty deps_assoc
            in
            let () = List.iter
              (fun (x, deps) -> List.iter
                 (fun dep ->
                    if S.mem dep mynodes then
                      bprintf buf "  \"%s\" -> \"%s\";\n" x dep)
                 deps)
              deps_assoc
            in
            bprintf buf "}\n";
            begin match Unix.close_process_in ic with
              | Unix.WEXITED 0 -> ()
              | e -> raise (Subprocess_died_unexpectedly e)
            end;
            Echo ([Buffer.contents buf], "modules.dot")
        end

    | _ -> ()
  end
