;   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
  ^{:author "Stuart Sierra, Chas Emerick, Stuart Halloway",
     :doc "This file defines polymorphic I/O utility functions for Clojure."}
    clojure.erlang.io
    (:require clojure.string)
    (:import
     (erlang.io IReader IWriter File
                StringReader ICloseable)))

(defprotocol ^{:added "1.2"} Coercions
  "Coerce between various 'resource-namish' things."
  (^{:tag erlang.io.File, :added "1.2"} as-file [x] "Coerce argument to a file.")
  (^{:tag clojerl.String, :added "1.2"} as-url [x] "Coerce argument to a URL."))

(defn- url-encode [uri]
  (http_uri/encode (erlang/binary_to_list uri)))

(defn- url-decode [encoded-uri]
  (http_uri/decode (erlang/binary_to_list encoded-uri)))

(defn- escaped-utf8-urlstring->str [s]
  (-> (clojure.string/replace s "+" (url-encode "+"))
      url-decode))

(extend-protocol Coercions
  nil
  (as-file [_] nil)
  (as-url [_] nil)

  clojerl.String
  (as-file [s] (erlang.io.File/open s))
  (as-url [s] s)

  erlang.io.File
  (as-file [f] f)
  (as-url [^erlang.io.File f] (.path f))

  #_ (URL
       (as-url [u] u)
       (as-file [u]
                (if (re-find #"file://" u)
                  (as-file (escaped-utf8-urlstring->str
                            (clojure.string/replace u \/ (erlang.io.File/separator_char))))
                  (throw (str "Not a file: " u))))))

(defprotocol ^{:added "1.2"} IOFactory
  "Factory functions that create ready-to-use, versions of
   the various I/O types, on top of anything that can
   be unequivocally converted to the requested kind of stream.

   Common options include

     :append    true to open stream in append mode
     :encoding  string name of encoding to use, e.g. \"UTF-8\".

   Callers should generally prefer the higher level API provided by
   reader, writer, input-stream, and output-stream."
  (^{:added "1.2"} make-reader [x opts] "Creates a BufferedReader. See also IOFactory docs.")
  (^{:added "1.2"} make-writer [x opts] "Creates a BufferedWriter. See also IOFactory docs."))

(defn ^erlang.io.IReader reader
  "Attempts to coerce its argument into an open erlang.io.IReader.
   Default implementations always return a erlang.io.BufferedReader.

   Default implementations are provided for Reader, BufferedReader,
   InputStream, File, URI, URL, Socket, byte arrays, character arrays,
   and String.

   If argument is a String, it tries to resolve it first as a URI, then
   as a local file name.  URIs with a 'file' protocol are converted to
   local file names.

   Should be used inside with-open to ensure the Reader is properly
   closed."
  {:added "1.2"}
  [x & opts]
  (make-reader x (when opts (apply hash-map opts))))

(defn ^erlang.io.IWriter writer
  "Attempts to coerce its argument into an open erlang.io.IWriter.
   Default implementations always return a erlang.io.IWriter.

   Default implementations are provided for File, String and nil.

   If the argument is a String, it tries to resolve it first as a URI, then
   as a local file name.  URIs with a 'file' protocol are converted to
   local file names.

   Should be used inside with-open to ensure the IWriter is properly
   closed."
  {:added "1.2"}
  [x & opts]
  (make-writer x (when opts (apply hash-map opts))))

(defn encoding [encoding]
  #erl[:encoding (if (= "latin1" encoding) :latin1 :utf8)])

(defn process-opt [acc k v]
  (case k
    :encoding (conj acc (encoding v))
    (if v (conj acc k) acc)))

(defn file-modes [opts & default]
  (reduce-kv process-opt (set default) opts))

(defn file-open [path modes]
  (erlang.io.File/open path (clj_rt/to_list modes)))

(defn file-path [^erlang.io.File file]
  (.path file))

(extend-type clojerl.String
  IOFactory
  (make-reader [this opts]
    (file-open this (file-modes opts :read)))
  (make-writer [this opts]
    (file-open this (file-modes opts :write))))

(extend-type erlang.io.File
  IOFactory
  (make-reader [this opts]
    (file-open (file-path this) (file-modes opts :read)))
  (make-writer [this opts]
    (file-open (file-path this) (file-modes opts :write))))

(extend-type nil
  IOFactory
  (make-reader [x opts]
    (throw (clojerl.BadArgumentError. (str "Cannot open <" (pr-str x) "> as a Reader."))))
  (make-writer [x opts]
    (throw (clojerl.BadArgumentError. (str "Cannot open <" (pr-str x) "> as a Writer.")))))

(defmulti
  ^{:doc "Internal helper for copy"
     :private true
     :arglists '([input output opts])}
  do-copy
  (fn [input output opts] [(type input) (type output)]))

(defmethod do-copy [erlang.io.File erlang.io.StringWriter]
  [^erlang.io.File input ^erlang.io.StringWriter output opts]
  (loop []
    (let [line (.read_line input)]
      (when (not= line :eof)
        (do (.write output line)
            (recur))))))

(defn copy
  "Copies input to output.  Returns nil or throws IOException.
  Input may be an InputStream, Reader, File, byte[], or String.
  Output may be an OutputStream, Writer, or File.

  Options are key/value pairs and may be one of

    :buffer-size  buffer size to use, default is 1024.
    :encoding     encoding to use if converting between
                  byte and char streams.

  Does not close any streams except those it opens itself
  (on a File)."
  {:added "1.2"}
  [input output & opts]
  (do-copy input output (when opts (apply hash-map opts))))

(defn ^clojerl.String as-relative-path
  "Take an as-file-able thing and return a string if it is
   a relative path, else IllegalArgumentException."
  {:added "1.2"}
  [x]
  (let [^erlang.io.File f (as-file x)
        path (.path f)]
    (if (= :absolute (filename/pathtype path))
      (throw (clojerl.BadArgumentError. (str f " is not a relative path")))
      path)))

(defn ^File file
  "Returns a erlang.io.File, passing each arg to as-file.  Multiple-arg
   versions treat the first argument as parent and subsequent args as
   children relative to the parent."
  {:added "1.2"}
  ([arg]
     (as-file arg))
  ([parent child]
   (let [path (-> [parent (as-relative-path child)]
                  (clj_rt/to_list)
                  (filename/join))]
     (file path)))
  ([parent child & more]
   (let [path (-> (map str (list* parent child more))
                  (clj_rt/to_list)
                  (filename/join))]
     (file path))))

(defn delete-file
  "Delete file f. If silently is nil or false, raise an exception on failure, else return the value of silently."
  {:added "1.2"}
  [f & [silently]]
  (or (.delete ^erlang.io.File (as-file f))
      silently
      (throw (clojerl.IOError. (str "Couldn't delete " f)))))

(defn make-parents
  "Given the same arg(s) as for file, creates all parent directories of
   the file they represent."
  {:added "1.2"}
  [f & more]
  (with-open [f (apply file f more)]
    (filelib/ensure_dir (file-path f))))
