(**************************************************************************)
(*                                                                        *)
(*  Copyright (C) 2012 Johannes 'josch' Schauer <j.schauer@email.de>      *)
(*  Copyright (C) 2012 Pietro Abate <pietro.abate@pps.jussieu.fr>         *)
(*                                                                        *)
(*  This library is free software: you can redistribute it and/or modify  *)
(*  it under the terms of the GNU Lesser General Public License as        *)
(*  published by the Free Software Foundation, either version 3 of the    *)
(*  License, or (at your option) any later version.  A special linking    *)
(*  exception to the GNU Lesser General Public License applies to this    *)
(*  library, see the COPYING file for more information.                   *)
(**************************************************************************)

open! ExtLib
open Dose_common
open Dose_debian
open Dose_algo
open Dose_doseparse

#define __label __FILE__
let label =  __label ;;
include Util.Logging(struct let label = label end) ;;

let binwithoutsrc ?(reqsrcarchall=false) ?(allowmismatch=false) universe bl =
  CudfAdd.Cudf_set.fold (fun binpkg acc ->
    try
      ignore (BootstrapCommon.get_src_package ~allowmismatch universe binpkg);
      acc
    with Sources.NotfoundSrc ->
      (* Source for this package was not found. But if this package is not
       * arch:all or reqsrcarchall is true, then we don't care. *)
      if (BootstrapCommon.pkg_is_not_arch_all binpkg) || reqsrcarchall then
        CudfAdd.Cudf_set.add binpkg acc
      else
        acc
  ) bl CudfAdd.Cudf_set.empty
;;

let srcwithoutbin ?(addsrcarchall=false) ?(allowmismatch=false) universe bl sl =
  let h = CudfAdd.Cudf_hashtbl.create (Cudf.universe_size universe) in
  CudfAdd.Cudf_set.iter (fun binpkg ->
    (* if the binary package is not arch:all or if addsrcarchall is true, then
     * try to retrieve the source package for this binary package *)
    if (BootstrapCommon.pkg_is_not_arch_all binpkg) || addsrcarchall then begin
      try
        let srcpkg = BootstrapCommon.get_src_package ~allowmismatch universe binpkg in
        CudfAdd.Cudf_hashtbl.replace h srcpkg ()
      with Sources.NotfoundSrc -> ()
    end
  ) bl;
  CudfAdd.Cudf_set.fold
    (fun srcpkg acc -> if CudfAdd.Cudf_hashtbl.mem h srcpkg then acc else CudfAdd.Cudf_set.add srcpkg acc)
    sl CudfAdd.Cudf_set.empty
;;

module PackageGraphCondensed = struct
  module IntSet = BootstrapCommon.IntSet
  module PkgV = struct
    type t =
      | Pkg of int
      | SCC of IntSet.t
    let compare x y = match (x,y) with
      | Pkg p1, Pkg p2 -> Pervasives.compare p1 p2
      | SCC s1, SCC s2 -> IntSet.compare s1 s2
      | Pkg _, SCC _ -> -1
      | SCC _, Pkg _ -> 1
    let hash = function
      (* since package ids are unique in the graph, the minimum set element unique
       * identifies sets *)
      | Pkg p -> p
      | SCC s -> IntSet.min_elt s
    let equal x y = (compare x y) = 0
  end

  module G = Graph.Imperative.Digraph.ConcreteBidirectional(PkgV)

  let dependency_graph ?(reverse=false) ?(global_constraints=[]) universe =
    let gr = G.create () in
    let add_edge v1 v2 = if reverse then
        G.add_edge gr v2 v1
      else
        G.add_edge gr v1 v2
    in
    let essential = if List.length global_constraints > 0 then begin
      let essential = Cudf.get_packages
          ~filter:(fun pkg ->
              BootstrapCommon.debtype_of_cudfpkg pkg = `BinPkg
              && BootstrapCommon.debessential_of_cudfpkg pkg)
          universe
      in
      List.map (fun pkg -> PkgV.Pkg (CudfAdd.pkgtoint universe pkg)) essential
    end else [] in
    Cudf.iter_packages (fun pkg ->
        let pkgv = PkgV.Pkg (CudfAdd.pkgtoint universe pkg) in
        G.add_vertex gr pkgv;
        List.iter (fun vpkgs ->
            (* this is a reverse dependency graph so if A depends on B then an
             * edge is added from B to A *)
            List.iter
              (fun p -> add_edge pkgv (PkgV.Pkg p))
              (List.flatten (List.map (CudfAdd.resolve_vpkg_int universe) vpkgs))
          ) pkg.Cudf.depends;
        (* add edges from all essential packages (if any) to all vertices *)
        List.iter (add_edge pkgv) essential
      ) universe;
    gr

  let condense g =
    let module C = Graph.Components.Make(G) in
    (* calculate SCCs and map each found SCC to either its original vertex in
     * case of a degenerate component of size one or to a vertex containing the
     * set of packages in the SCC *)
    let sccs = Array.map (function
        | [] -> fatal "scc cannot be empty"
        | [v] -> v
        | scc ->
          let pset = List.fold_right (function
              | PkgV.SCC _ -> fatal "cannot condense graph with SSC vertices"
              | PkgV.Pkg p -> IntSet.add p) scc IntSet.empty
          in PkgV.SCC pset)
        (C.scc_array g)
    in
    (* associate with each SCC a unique ID and create a mapping from all
     * vertices in the original graph to its SCC ID *)
    let mapping = Hashtbl.create (G.nb_vertex g) in
    Array.iteri (fun i v ->
        match v with
        | PkgV.Pkg _ -> Hashtbl.add mapping v i
        | PkgV.SCC s -> IntSet.iter (fun p -> Hashtbl.add mapping (PkgV.Pkg p) i) s
      ) sccs;
    (* go through the edges in the original graph, find out the SCC IDs that
     * their vertices belong to and if they are different, add that edge to the
     * new graph connecting the SCCs *)
    let cg = G.create () in
    G.iter_edges (fun v1 v2 ->
      let scc1 = Hashtbl.find mapping v1 in
      let scc2 = Hashtbl.find mapping v2 in
      if scc1 <> scc2 then
        G.add_edge cg sccs.(scc1) sccs.(scc2)
    ) g;
    cg

  let get_dependency_closures universe g =
    let module Dfs = Graph.Traverse.Dfs(G) in
    if Dfs.has_cycle g then fatal "cannot get dependency closures of a cyclic graph";
    (* traverse the vertices in the graph in post-order depth first search and
     * for each visited vertex V add to V the union of:
     *       1. all packages in V (one package in Pkg vertices and multiple in
     *          SCC vertices)
     *       2. all packages associated with the successors of V
     *)
    let intclosureht = Hashtbl.create (G.nb_vertex g) in
    (* a package graph of Debian Sid 2014 has 2500 SCCs *)
    let sccht = Hashtbl.create 2500 in
    let intsettovarset s = IntSet.fold
        (fun p -> CudfAdd.Cudf_set.add (CudfAdd.inttopkg universe p))
        s CudfAdd.Cudf_set.empty
    in
    Dfs.postfix (fun v ->
        let succ_set = G.fold_succ (function
            | PkgV.Pkg pp -> CudfAdd.Cudf_set.union (Hashtbl.find intclosureht pp)
            | PkgV.SCC ps -> CudfAdd.Cudf_set.union (Hashtbl.find sccht ps)
          ) g v CudfAdd.Cudf_set.empty
        in
        match v with
        | PkgV.Pkg p ->
          if Hashtbl.mem intclosureht p then fatal "package already in hashtbl";
          let closure = CudfAdd.Cudf_set.add (CudfAdd.inttopkg universe p) succ_set in
          Hashtbl.add intclosureht p closure
        | PkgV.SCC s ->
          if Hashtbl.mem sccht s then fatal "package already in hashtbl";
          let closure = CudfAdd.Cudf_set.union (intsettovarset s) succ_set in
          Hashtbl.add sccht s closure
      ) g;
    let closureht = Hashtbl.create (G.nb_vertex g) in
    (* expand SCCs into individual packages *)
    Hashtbl.iter
      (fun scc s -> IntSet.iter
          (fun p -> Hashtbl.add closureht (CudfAdd.inttopkg universe p) s) scc)
      sccht;
    (* convert integer based hashtable into var based one *)
    Hashtbl.iter
      (fun k v -> Hashtbl.add closureht (CudfAdd.inttopkg universe k) v)
      intclosureht;
    closureht
end

let maxclosure ?(global_constraints=[]) ?(reqsrcarchall=false) ?(addsrcarchall=false) ?(allowmismatch=false) bl sl =
  let module Set = CudfAdd.Cudf_set in
  let univ = Cudf.load_universe (bl@sl) in
  (* create a reverse dependency graph *)
  info "creating dependency graph";
  let g = PackageGraphCondensed.dependency_graph ~reverse:true ~global_constraints univ in
  let module Dfs = Graph.Traverse.Dfs(PackageGraphCondensed.G) in
  info "condensing";
  (* if graph has cycle, condense strongly connected components into single vertices *)
  let g = if Dfs.has_cycle g then PackageGraphCondensed.condense g else g in
  info "retrieving reverse closures";
  let affectedht_2 = PackageGraphCondensed.get_dependency_closures univ g in
  let affected removed =
    let a = Set.fold (fun pkg acc ->
        Set.union (Hashtbl.find affectedht_2 pkg) acc
      ) removed Set.empty in
    Set.partition (fun p -> BootstrapCommon.debtype_of_cudfpkg p = `BinPkg) a
  in
  let rec aux (notinstacc, notcompacc, nobinacc, nosrcacc, bintocheck, srctocheck) bl sl univ =
    let notinst, notcomp = match bintocheck, srctocheck with
    | [],[] -> Set.empty, Set.empty
    | _ -> begin
        info "checking %d binary packages" (List.length bintocheck);
        (* find binary packages that can't be installed *)
        let notinst = CudfAdd.to_set (Depsolver.find_listbroken ~global_constraints univ bintocheck) in
        info "checking %d source packages" (List.length srctocheck);
        (* find source packages that can't be compiled *)
        let notcomp = CudfAdd.to_set (Depsolver.find_listbroken ~global_constraints univ srctocheck) in
        info "removing %d not installable binary and %d not compilable source packages" (Set.cardinal notinst) (Set.cardinal notcomp);
        notinst, notcomp
      end
    in
    info "checking for source without binary";
    (* find source packages without binary packages *)
    let nobin = srcwithoutbin ~addsrcarchall ~allowmismatch univ bl sl in
    info "checking for binary without source";
    (* find binary packages without source packages *)
    let nosrc = binwithoutsrc ~reqsrcarchall ~allowmismatch univ bl in
    info "removing %d binary packages without source packages and %d source packages without binary packages" (Set.cardinal nosrc) (Set.cardinal nosrc);

    let removedbin = Set.union notinst nosrc in
    if List.length global_constraints > 0 && Set.exists BootstrapCommon.debessential_of_cudfpkg removedbin then
      fatal "will not remove essential package";

    if Set.is_empty notinst && Set.is_empty notcomp && Set.is_empty nobin && Set.is_empty nosrc then
      notinstacc, notcompacc, nobinacc, nosrcacc, (Set.elements bl)
    else begin
      let bl = Set.diff bl removedbin in
      let sl = Set.diff sl (Set.union notcomp nobin) in
      let affbin, affsrc = affected removedbin in
      let bintocheck = Set.inter affbin bl in
      let srctocheck = Set.inter affsrc sl in
      let notinstacc = Set.union notinstacc notinst in
      let notcompacc = Set.union notcompacc notcomp in
      let nobinacc = Set.union nobinacc nobin in
      let nosrcacc = Set.union nosrcacc nosrc in
      let univ = Cudf.load_universe (Set.elements (Set.union bl sl)) in
      aux (notinstacc, notcompacc, nobinacc, nosrcacc, (Set.elements bintocheck), (Set.elements srctocheck)) bl sl univ
    end
  in
  let e = Set.empty in
  aux (e,e,e,e,bl,sl) (CudfAdd.to_set bl) (CudfAdd.to_set sl) univ
;;

module Options = struct
  open OptParse
  let description = (
    "given a repository of binary packages and source packages, clean up that "^
    "repository by removing packages that can't be compiled or installed and "^
    "packages which don't have an associated binary or source package and "^
    "packages which do not match the specified architecture."
  )
  let usage = "%prog [options] Packages Sources"

  let options = OptParser.make ~description ~usage
  include BootstrapCommon.MakeOptions(struct let options = options end)

  let reqsrcarchall = StdOpt.store_true ()
  let addsrcarchall = StdOpt.store_true ()
  let allowsrcmismatch = StdOpt.store_true ()
  let progress = StdOpt.store_true ()

  open OptParser ;;

  let prog_group = add_group options "Program specific options" in

  add options ~group:prog_group ~long_name:"reqsrcarchall" ~help:"require source packages for arch:all binary packages" reqsrcarchall;
  add options ~group:prog_group ~long_name:"addsrcarchall" ~help:"add source packages which only build arch:all binary packages" addsrcarchall;
  add options ~group:prog_group ~long_name:"allowsrcmismatch" ~help:("If a binary package is "^
    "without a source package but there is a source package of same name but "^ 
    "different version, match this binary package to that source package.") allowsrcmismatch;
  add options ~group:prog_group ~long_name:"progress" ~help:"print progress bars" progress;

  include StdOptions.InputOptions;;
  let default = List.filter (fun e -> not (List.mem e ["compare";"checkonly";"latest";"fg";"bg";"inputtype"])) StdOptions.InputOptions.default_options in
  StdOptions.InputOptions.add_options ~default options;;

  include StdOptions.OutputOptions;;
  let default = List.filter (fun e -> not (List.mem e ["outdir"; "dot"])) StdOptions.OutputOptions.default_options in
  StdOptions.OutputOptions.add_options ~default options;;

  include StdOptions.DistribOptions;;
  let default = List.filter (fun e -> not (List.mem e ["deb-profiles"; "deb-builds-from"])) StdOptions.DistribOptions.default_options in
  StdOptions.DistribOptions.add_debian_options ~default options;;
end

let main () =
  let posargs = OptParse.OptParser.parse_argv Options.options in
  StdDebug.enable_debug (OptParse.Opt.get Options.verbose);
  StdDebug.all_quiet (OptParse.Opt.get Options.quiet);
  StdDebug.enable_bars (OptParse.Opt.get Options.progress) ["Depsolver_int.univcheck"];
  Util.Warning.disable "Sources";

  let options = Options.set_deb_options () in
  let buildarch = Option.get options.Debcudf.native in
  let hostarch = match options.Debcudf.host with None -> "" | Some s -> s in
  let foreignarchs = options.Debcudf.foreign in
  let reqsrcarchall = OptParse.Opt.get Options.reqsrcarchall in
  let addsrcarchall = OptParse.Opt.get Options.addsrcarchall in
  let noindep = options.Debcudf.drop_bd_indep in
  let allowmismatch = OptParse.Opt.get Options.allowsrcmismatch in

  let binlist, (fgsrclist,bgsrclist), _ = BootstrapCommon.parse_packages ~noindep Options.parse_cmdline buildarch hostarch foreignarchs posargs in
  let srclist = fgsrclist @ bgsrclist in
  let tables = Debcudf.init_tables ~options (srclist@binlist) in
  let global_constraints = Debcudf.get_essential ~options tables in
  let sl = List.map (Debcudf.tocudf ?inst:None ~options tables) srclist in
  let bl = List.map (Debcudf.tocudf ?inst:None ~options tables) binlist in

  (* save mappings from cudf packages to binary packages *)
  let cudftobin_table = Hashtbl.create 30000 in
  List.iter2 (fun cudfpkg -> fun binpkg ->
    let id = (cudfpkg.Cudf.package, cudfpkg.Cudf.version) in
    Hashtbl.add cudftobin_table id binpkg
  ) bl binlist;

  let notinst, notcomp, nobin, nosrc, bl = maxclosure ~global_constraints ~reqsrcarchall ~addsrcarchall ~allowmismatch bl sl in

  debug "not installable: %s" (BootstrapCommon.string_of_list (BootstrapCommon.string_of_package ?noversion:None) " " (CudfAdd.Cudf_set.elements notinst));
  debug "not compilable: %s" (BootstrapCommon.string_of_list (BootstrapCommon.string_of_package ?noversion:None) " " (CudfAdd.Cudf_set.elements notcomp));
  debug "source without binary: %s" (BootstrapCommon.string_of_list (BootstrapCommon.string_of_package ?noversion:None) " " (CudfAdd.Cudf_set.elements nobin));
  debug "binary without source: %s" (BootstrapCommon.string_of_list (BootstrapCommon.string_of_package ?noversion:None) " " (CudfAdd.Cudf_set.elements nosrc));
  info "not installable: %d" (CudfAdd.Cudf_set.cardinal notinst);
  info "not compilable: %d" (CudfAdd.Cudf_set.cardinal notcomp);
  info "source without binary: %d" (CudfAdd.Cudf_set.cardinal nobin);
  info "binary without source: %d" (CudfAdd.Cudf_set.cardinal nosrc);

  let oc =
    if OptParse.Opt.is_set Options.outfile then
      open_out (OptParse.Opt.get Options.outfile)
    else
      stdout
  in

  (* for each binary package, get the associated format822 stanza and print it
   * to stdout *)
  match bl with
    | [] -> failwith "no binary package remains compilable"
    | _ -> begin
        List.iter (fun p ->
          let id = (p.Cudf.package, p.Cudf.version) in
          let b = Hashtbl.find cudftobin_table id in
          b#pp oc;
        ) (BootstrapCommon.debcudf_sort bl);
    end;

  close_out oc;
;;

main ();;
