;   Copyright (c) Rich Hickey. All rights reserved.
;   The use and distribution terms for this software are covered by the
;   Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;   which can be found in the file epl-v10.html at the root of this distribution.
;   By using this software in any fashion, you are agreeing to be bound by
;   the terms of this license.
;   You must not remove this notice, or any other, from this software.

(ns ^{:skip-wiki true}
  clojure.tools.deps.alpha.extensions
  (:require
    [clojure.java.io :as jio]
    [clojure.set :as set]))

;; Helper for autodetect of manifest type

;; vector to control ordering
(def manifest-types
  ["deps.edn" :deps,
   "pom.xml" :pom
   ;; "project.clj" :lein
   ])

(defn detect-manifest
  "Given a directory, detect the manifest type and return the manifest info."
  [dir]
  (loop [[file-name manifest-type & others] manifest-types]
    (when file-name
      (let [f (jio/file dir file-name)]
        (if (and (.exists f) (.isFile f))
          {:deps/manifest manifest-type, :deps/root dir}
          (recur others))))))

;; Methods switching on coordinate type

(defmulti coord-type-keys
  "Takes a coordinate type and returns valid set of keys indicating that coord type"
  (fn [type] type))

(defmethod coord-type-keys :default [type] #{})

(defn procurer-types
  "Returns set of registerd procurer types (results may change if procurer methods are registered)."
  []
  (disj (-> (.getMethodTable ^clojure.lang.MultiFn coord-type-keys) keys set) :default))

(defn coord-type
  "Determine the coordinate type of the coordinate, based on the self-published procurer
  keys from coord-type-keys."
  [coord]
  (when (map? coord)
    (let [exts (procurer-types)
          coord-keys (-> coord keys set)
          matches (reduce (fn [ms type]
                            (cond-> ms
                              (not (empty? (set/intersection (coord-type-keys type) coord-keys)))
                              (conj type)))
                    [] exts)]
      (case (count matches)
        0 (throw (ex-info (format "Coord of unknown type: %s" (pr-str coord)) {:coord coord}))
        1 (first matches)
        (throw (ex-info (format "Coord type is ambiguous: %s" (pr-str coord)) {:coord coord}))))))

(defmulti lib-location
  "Takes a coordinate and returns the location where the lib would be
  installed locally. Location keys:

  :base     local repo base directory path
  :path     path within base dir
  :type     coordinate type"
  (fn [lib coord config] (coord-type coord)))

(defmulti canonicalize
  "Takes a lib and coordinate and returns a canonical form.
  For example, a Maven coordinate might resolve LATEST to a version or a Git
  coordinate might resolve a partial sha to a full sha. Returns [lib coord]."
  (fn [lib coord config] (coord-type coord)))

(defmethod canonicalize :default [lib coord config]
  [lib coord])

(defmulti dep-id
  "Returns an identifier value that can be used to detect a lib/coord cycle while
   expanding deps. This will only be called after canonicalization so it can rely
   on the canonical form."
  (fn [lib coord config] (coord-type coord)))

(defn- throw-bad-coord
  [lib coord]
  (if (map? coord)
    (let [type (coord-type coord)]
      (if (nil? type)
        (throw (ex-info (str "No coordinate type found for library " lib " in coordinate " (pr-str coord)) {:lib lib :coord coord}))
        (throw (ex-info (str "Coordinate type " type " not loaded for library " lib " in coordinate " (pr-str coord))
                 {:lib lib :coord coord}))))
    (throw (ex-info (str "Bad coordinate for library " lib ", expected map: " (pr-str coord)) {:lib lib :coord coord}))))

(defmethod dep-id :default [lib coord config]
  (throw-bad-coord lib coord))

(defmulti manifest-type
  "Takes a lib, a coord, and the root config. Dispatch based on the
   coordinate type. Detect and return the manifest type and location
   for this coordinate."
  (fn [lib coord config] (coord-type coord)))

(defmethod manifest-type :default [lib coord config]
  (throw-bad-coord lib coord))

(defmulti coord-summary
  "Takes a coord, and returns a concise description, used when printing tree"
  (fn [lib coord] (coord-type coord)))

(defmethod coord-summary :default [lib coord]
  (str lib " " (coord-type coord)))

;; Version comparison, either within or across coordinate types

(defmulti compare-versions
  "Given two coordinates, use this as a comparator returning a negative number, zero,
  or positive number when coord-x is logically 'less than', 'equal to', or 'greater than'
  coord-y. The dispatch occurs on the type of x and y."
  (fn [lib coord-x coord-y config] [(coord-type coord-x) (coord-type coord-y)]))

(defmethod compare-versions :default
  [lib coord-x coord-y config]
  (throw (ex-info (str "Unable to compare versions for " lib ": " (pr-str coord-x) " and " (pr-str coord-y))
           {:lib lib :coord-x coord-x :coord-y coord-y})))

;; Find coords

(defmulti find-versions
  "Return a coll of coords based on a lib and a partial coord"
  (fn [lib coord coord-type config] coord-type))

(defmethod find-versions :default [lib coord coord-type config]
  (throw-bad-coord lib coord))

(defn find-all-versions
  "Find versions across all registered procurer types and return first that finds some.
  Returns coll of coordinates for this lib (based on lib and partial coordinate)."
  [lib coord config]
  (some #(find-versions lib coord % config) (procurer-types)))

;; Methods switching on manifest type

(defn- throw-bad-manifest
  [lib coord manifest-type]
  (if manifest-type
    (throw (ex-info (str "Manifest type " manifest-type " not loaded when finding deps for " lib " in coordinate " (pr-str coord))
             {:lib lib :coord coord}))
    (throw (ex-info (str "Manifest type not detected when finding deps for " lib " in coordinate " (pr-str coord))
             {:lib lib :coord coord}))))

(defmulti coord-deps
  "Return coll of immediate [lib coord] external deps for this library."
  (fn [lib coord manifest-type config] manifest-type))

(defmethod coord-deps :default [lib coord manifest-type config]
  (throw-bad-manifest lib coord manifest-type))

(defmulti coord-paths
  "Return coll of classpath roots for this library on disk."
  (fn [lib coord manifest-type config] manifest-type))

(defmethod coord-paths :default [lib coord manifest-type config]
  (throw-bad-manifest lib coord manifest-type))

(defmulti coord-usage
  "Return usage info map for this library with the following optional keys:
    :ns-default - default namespace symbol
    :ns-aliases - map of alias to namespace symbol"
  (fn [lib coord manifest-type config] manifest-type))

(defmethod coord-usage :default [lib coord manifest-type config]
  (throw-bad-manifest lib coord manifest-type))

(defmulti prep-command
  "Return prep command for this library with the following keys:
    :alias - alias to use when invoking (keyword)
    :fn - function to invoke in alias (symbol)
    :ensure - relative path in repo to ensure exists after prep"
  (fn [lib coord manifest-type config] manifest-type))

(defmethod prep-command :default [lib coord manifest-type config]
  (throw-bad-manifest lib coord manifest-type))

(comment
  (require '[clojure.tools.deps.alpha.util.maven :as maven])

  (binding [*print-namespace-maps* false]
    (run! prn
      (find-all-versions 'io.github.clojure/tools.deps.alpha nil {:mvn/repos maven/standard-repos})))

  (binding [*print-namespace-maps* false]
    (run! prn
      (find-all-versions 'org.clojure/tools.deps.alpha nil {:mvn/repos maven/standard-repos})))

  )