(ns debian.dh-clojure-lein.inject
  "Functions that may be dynamically insinuated into project builds by
  Debian's leiningen.

  See dh-clojure-lein(7) for more information."
  (:require
   [clojure.pprint :refer [pprint]]
   [clojure.string :as str]
   [debian.dh-clojure-lein.util
    :refer [abort
            debug
            dep-map-key
            dep-vecs->map
            info
            map->dep-vecs
            normalize-dep]]
   [leiningen.core.main :as main]
   [leiningen.core.project :as proj])
  (:import
   [java.io FileNotFoundException]))

(defn- set-default-dep-map-versions [dep-map]
  (reduce-kv (fn set-default-version [result dep-k info]
               (assoc result dep-k
                      (assoc info :version
                             (case [(:group-id dep-k) (:artifact-id dep-k)]
                               ["org.clojure" "clojure"] "1.x"
                               "debian"))))
             {} dep-map))

(def ^:private configs (atom {})) ;; path -> {:path path :ns namespace}

(defn- drop-config! [path] (swap! configs dissoc path) nil)

(defn- config-for-path
  "Returns the path's config; creates it if it doesn't exist.

  Returns {:path path :ns namespace} where the namespace will have had
  the config loaded into it via load-config."
  ;; For now, we just assume that throwing an exception halts the
  ;; entire process.
  ([path] (config-for-path path nil))
  ([path load-config]
   (locking configs
     (let [config (get @configs path)]
       (cond
         (= config :none) nil
         config config
         :else
         (let [n (when (or load-config (-> path java.io.File. .exists))
                   (let [cfg-ns (create-ns (gensym "dh-clojure-lein-config-"))]
                     (binding [*ns* cfg-ns]
                       (refer-clojure)
                       (try
                         (if load-config
                           (load-config)
                           (load-file path))
                         cfg-ns
                         (catch FileNotFoundException _)))))]
           (if n
             (let [config {:path path :ns n}]
               (swap! configs assoc path config)
               config)
             (do
               (swap! configs assoc path :none)
               nil))))))))

(defn- config-adjust-project
  "Calls (apply dh-clj-adjust-project get-project args) in the config namespace.

  Currently dh-clj-adjust-project must exist and (get-project) will
  return the project after adjusting it to have leiningen project
  style dependency maps (see dep-vecs->map) and after defaulting all
  the version via set-default-dep-map-versions.  A second argument
  indicates when in the build process the call is being made.  Current
  possibilities are :before-plugins and :after-plugins.  The function
  should ignore unrecognized invocations (e.g. additional arguments or
  unexpected \"when\" values).

  The dh-clj-adjust-project invocation must return a possibly modified
  version of the project which will then be returned from this
  function after being converted back to leiningen's project.clj
  format (e.g. with dependency vectors, not maps)."
  [config project args]
  (let [adjust (some-> config :ns (ns-resolve 'dh-clj-adjust-project))
        path (:path config)]
    (when (and config (not adjust)) ;; for now, required
      (abort "dh-clj-adjust-project was not defined in " (pr-str path) "\n"))
    (let [prep-deps #(-> % dep-vecs->map set-default-dep-map-versions)
          prepared (-> project
                       (update :dependencies prep-deps)
                       (update :managed-dependencies prep-deps)
                       (update :plugins prep-deps))]
      ;; Pass a function instead of the project itself, so we'll have
      ;; room to maneuver if needed, e.g. if we need support for
      ;; something like (get-project :unmodified true)
      (-> (if-not adjust
            prepared
            (binding [*ns* (:ns config)]
              (apply adjust (fn get-project [] prepared) args)))
          (update :dependencies map->dep-vecs)
          (update :managed-dependencies map->dep-vecs)
          (update :plugins map->dep-vecs)))))

(defn adjust-project
  [project & args]
  (let [path (str (:root project) "/debian/dh-clojure-lein.clj")
        config (config-for-path path)
        _ (when-not config
            (debug "no debian/dh-clojure-lein.clj (applying defaults)\n"))
        project (config-adjust-project config project args)]
    (when main/*debug*
      (debug "adjusted project\n")
      (binding [*out* *err*] (pprint project)))
    project))
