;;; Gash --- Guile As SHell
;;; Copyright © 2016,2017,2018 R.E.W. van Beusekom <rutger.van.beusekom@gmail.com>
;;; Copyright © 2018 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
;;;
;;; This file is part of Gash.
;;;
;;; Gash is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; Gash 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 General Public License for more
;;; details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with Gash.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gash script)
  #:use-module (ice-9 ftw)
  #:use-module (ice-9 getopt-long)
  #:use-module (ice-9 local-eval)
  #:use-module (ice-9 match)
  #:use-module (ice-9 pretty-print)
  #:use-module (ice-9 rdelim)
  #:use-module (ice-9 receive)
  #:use-module (ice-9 regex)

  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)

  #:use-module (gash bournish-commands)
  #:use-module (gash builtins)
  #:use-module (gash config)
  ;;#:use-module (gash environment)
  #:use-module (gash environment)
  #:use-module (gash word)
  #:use-module (gash gash)
  #:use-module (gash io)
  #:use-module (gash job)
  #:use-module (gash pipe)
  #:use-module (gash util)

  #:export (
            and-terms
            background
            brace-group
            builtin
            command
            delim
            doublequotes
            file-name
            for-clause
            do-group
            expression
            glob
            ignore-error
            literal
            or-terms
            pipeline
            run
            script-status
            sequence
            singlequotes
            source
            splice
            split
            substitution
            word
            xtrace
            variable-hash
            variable-hash-hash
            variable-percent
            variable-percent-percent
            variable-slash
            ))

(define* (variable name #:optional (default ""))
  (parameter-ref name default))

(define (background term)
  (format (current-error-port) "background: ~s\n" term)
  (match (pke 'background-term term)
    (('pipeline command) (pke 'background: `(pipeline+ #f ,command)))
    (_ term)))

(define (source file-name)
  (let* ((string (with-input-from-file file-name read-string))
         (ast (parse-string string)))
    (run ast)))

(define (command . args)
  (define (flatten o)
    (match o
      ((h t ...) (append (flatten h) (append-map flatten t)))
      (_ (list o))))
  (define (exec command)
    (cond ((procedure? command) command)
          ((assoc-ref %functions (car command))
           =>
           (lambda (function)
             (parameterize ((%command-line args))
               (last (apply function args)))))
          ((every string? command)
           (let* ((program (car command))
                  (escape-builtin? (and (string? program) (string-prefix? "\\" program)))
                  (program (if escape-builtin? (string-drop program 1) program))
                  (command (cons program (cdr command))))
             (or (builtin command #:prefer-builtin? (or %prefer-builtins?
                                                        escape-builtin?))
                 (lambda _ (status:exit-val (apply system* command))))))
          (else (lambda () #t))))
  (when (> %debug-level 1)
    (format (current-error-port) "command: ~s\n" args))
  (let ((args (flatten args)))
    (match args
      (((or "." "source") file-name)
       (let* ((string (with-input-from-file file-name read-string))
              (ast (parse-string string)))
         (run ast)
         0))
      (((? string?) ..1) (exec (append-map glob args)))
      (_  (exec (append-map glob args))))))

(define (glob? pattern)
  (and (string? pattern) (string-match "\\?|\\*" pattern)))

(define* (glob->regex pattern #:key (begin "^") (end "$"))
    (let* ((pattern (regexp-substitute/global #f "\\." pattern 'pre "\\." 'post))
           (pattern (regexp-substitute/global #f "\\?" pattern 'pre "." 'post))
           (pattern (regexp-substitute/global #f "\\*" pattern 'pre ".*" 'post)))
      (make-regexp (string-append begin pattern end))))

(define (glob pattern)
  (define (glob-match regex path) ;; pattern path -> bool
    (regexp-match? (regexp-exec regex path)))
  (define (glob- pattern file-names)
    (map (lambda (file-name)
           (if (string-prefix? "./" file-name) (string-drop file-name 2) file-name))
         (append-map (lambda (file-name)
                       (map (cut string-append (if (string=? "/" file-name) "" file-name) "/" <>)
                            (filter (conjoin (negate (cut string-prefix? "." <>))
                                             (cute glob-match (glob->regex pattern) <>))
                                    (or (scandir file-name) '()))))
                     file-names)))
  (cond
   ((not pattern) '(""))
   ((glob? pattern) (let ((absolute? (string-prefix? "/" pattern)))
                      (let loop ((patterns (filter (negate string-null?) (string-split pattern #\/)))
                                 (file-names (if absolute? '("/") '("."))))
                        (if (null? patterns)
                            file-names
                            (begin
                              (loop (cdr patterns) (glob- (car patterns) file-names)))))))
   (#t (list pattern))))

(define (singlequotes . o)
  (string-join o ""))

(define (doublequotes . o)
  (string-join (append-map glob  o) ""))

(define (sequence . args)
  (let ((glob (append-map glob (apply append args))))
    glob))

(define (run script)
  ;; fixme: work towards simple eval -- must remove begin for now
  (match script
    (('begin script ...)
     (last (map (cut local-eval <> (the-environment)) script)))
    (_ (local-eval script (the-environment)))))

(define (script-status)
  ((compose string->number variable) "?"))

(define (for-clause name sequence body)
  (for-each (lambda (value)
              (assignment name value)
              (body))
            sequence))

(define (split o)
  ((compose string-tokenize string-trim-right) o))

(define (xtrace o)
  (o))

(define (literal o)
  o)

(define (word . o)
  (define (flatten o)
    (match o
      ((h t ...) (append (flatten h) (append-map flatten t)))
      (_ (list o))))
  (match o
    (((? string?) ...) (string-join (flatten o) ""))
    ((((? string?) ...)) (flatten (car o)))
    (_ o)))

(define-syntax-rule (substitution commands)
  (string-trim-right (with-output-to-string (lambda _ commands))))

(define-syntax-rule (ignore-error o)
  (let ((errexit (shell-opt? "errexit")))
    (when errexit
      (set-shell-opt! "errexit" #f))
    (let ((r o))
      (assignment "?" "0")
      (when errexit
        (set-shell-opt! " errexit" #t))
      r)))

(define-syntax true?
  (lambda (x)
    (syntax-case x ()
      ((_ pipeline)
       (with-syntax ((it (datum->syntax x 'it)))
         #'(let ((it (ignore-error pipeline)))
             (status->bool it)))))))

(define (status->bool o)
  (match o
    (#t #t)
    ((? number?) (zero? o))
    (_ #f)))

(define-syntax expression
  (lambda (x)
    (syntax-case x ()
      ((_ (command word ...))
       #'(list word ...)))))

(define-syntax do-group
  (lambda (x)
    (syntax-case x ()
      ((_ term ...)
       #'(lambda _ term ...)))))

(define-syntax and-terms
  (lambda (x)
    (syntax-case x ()
      ((_ left right)
       (with-syntax ((it (datum->syntax x 'it)))
         #'(let ((it left))
             (if (zero? it) right it)))))))

(define-syntax or-terms
  (lambda (x)
    (syntax-case x ()
      ((_ left right)
       (with-syntax ((it (datum->syntax x 'it)))
         #'(let ((it (ignore-error left)))
             (if (zero? it) it right)))))))

(define (pipeline . commands)
  (define (handle job)
    (when (> %debug-level 1)
      (format (current-error-port) "job=~s\n" job))
    (let* ((stati (cond ((job? job) (map status:exit-val (job-status job)))
                        ((boolean? job) (list (if job 0 1)))
                        ((number? job) (list job))
                        (else (list 0))))
           (foo (when (> %debug-level 1)
                  (format (current-error-port) "stati=~s\n" stati)))
           (status (if (shell-opt? "pipefail") (or (find (negate zero?) stati) 0)
                       (car stati)))
           (pipestatus (string-append
                        "("
                        (string-join
                         (map (lambda (s i)
                                (format #f "[~a]=\"~a\"" s i))
                              stati
                              (iota (length stati))))
                        ")")))
      (assignment "PIPESTATUS" pipestatus)
      (assignment "?" (number->string status))
      (when (and (not (zero? status))
                 (shell-opt? "errexit"))
        (when (> %debug-level 0)
          (format (current-error-port) "set -e: exiting\n"))
        (exit status))
      (status->bool status)))
  (let ((commands (filter (lambda (x) (not (eq? x *unspecified*))) commands)))
    (when (> %debug-level 1)
      (format (current-error-port) "pijp: commands=~s\n" commands))
    ;; FIXME: after running a builtin, we still end up here with the builtin's result
    ;; that should probably not happen, however, cater for it here for now
    (match commands
      (((and (? boolean?) boolean))
       (handle boolean))
      (((and (? number?) number))
       (handle number))
      (((? unspecified?))
       (handle #t))
      (((? unspecified?) t ... #t)
       #t)
      (_ (handle (apply pipeline+ #t commands))))))

(define* (builtin ast #:key prefer-builtin?)
  ;; FIXME: distinguish between POSIX compliant builtins and
  ;; `best-effort'/`fallback'?
  "Possibly modify command to use a builtin."
  (when (> %debug-level 0)
    (format (current-error-port) "builtin ast=~s\n" ast))
  (receive (command args)
      (match ast
        (((and (? string?) command) args ...) (values command args))
        (_ (values #f #f)))
    (let ((program (and command
                        (cond ((string-prefix? "/" command)
                               (when (not (file-exists? command))
                                 (format (current-error-port) "gash: ~a: no such file or directory\n" command))
                               command)
                              (else (PATH-search-path command))))))
      ;; FIXME: find some generic strerror/errno way: what about permissions and stuff?
      ;; after calling  system* we're too late for that?
      (when (> %debug-level 0)
        (format (current-error-port) "command ~a => ~s ~s\n" (or program 'builtin) command args))
      (cond ((and program (not prefer-builtin?))
             (when (not program)
               (format (current-error-port) "gash: ~a: command not found\n" command))
             (when (not (access? program X_OK))
               (format (current-error-port) "gash: ~a: permission denied\n" command))
             #f)
            ((and command (or (assoc-ref %builtin-commands command)
                              (assoc-ref (%bournish-commands) command)))
             =>
             (lambda (command)
               (if args
                   (apply command (map (cut local-eval <> (the-environment)) args))
                   (command))))
            (else #f)))))

(define (brace-group . o)
  o)

(define (file-name o)
  o)

(define (regexp-exec-non-greedy regexp string)
  (let ((max (string-length string)))
    (let loop ((size 1))
      (and (<= size max)
           (or (regexp-exec regexp (substring string 0 size))
               (loop (1+ size)))))))

(define (regexp-exec-non-greedy-reverse regexp string)
  (let ((max (string-length string)))
    (let loop ((start (1- max)))
      (and (>= start 0)
           (or (regexp-exec regexp (substring string start))
               (loop (1- start)))))))

(define (variable-regex name sep pattern)
  (match sep
    ("##" (variable-hash-hash name pattern))
    ("#" (variable-hash name pattern))
    ("%%" (variable-percent-percent name pattern))
    ("%" (variable-percent name pattern))
    ("/" (variable-replace name pattern))))

(define (variable-replace name pattern)
  (let* ((value (variable name))
         (at (string-index pattern #\/))
         (regex (if at (substring pattern 0 at) pattern))
         (subst (if at (substring pattern (1+ at)) "")))
    (regexp-substitute/global #f regex value 'pre subst 'post)))

(define (variable-hash name pattern)
  (let ((value (variable name))
        (glob? (glob? pattern)))
    (if glob? (let* ((regexp (glob->regex pattern #:end ""))
                     (match (regexp-exec-non-greedy regexp value)))
                (if match (string-drop value (match:end match))
                    value))
        (if (string-prefix? pattern value) (string-drop value (string-length pattern))
            value))))

(define (variable-hash-hash name pattern)
  (let ((value (variable name))
        (glob? (glob? pattern)))
    (if glob? (let* ((regexp (glob->regex pattern #:end ""))
                     (match (regexp-exec regexp value)))
                (if match (string-drop value (match:end match))
                    value))
        (if (string-prefix? pattern value) (string-drop value (string-length pattern))
            value))))

(define (variable-percent name pattern)
  (let ((value (variable name))
        (glob? (glob? pattern)))
    (if glob? (let* ((regexp (glob->regex pattern #:begin ""))
                     (match (regexp-exec-non-greedy-reverse regexp value)))
                (if match (substring value 0 (- (string-length value) (match:end match)))
                    value))
        (if (string-suffix? pattern value) (substring value 0 (string-length pattern))
            value))))

(define (variable-percent-percent name pattern)
  (let ((value (variable name))
        (glob? (glob? pattern)))
    (if glob? (let* ((regexp (glob->regex pattern #:begin ""))
                     (match (regexp-exec regexp value)))
                (if match (substring value 0 (match:start match))
                    value))
        (if (string-suffix? pattern value) (substring value 0 (string-length pattern))
            value))))

(define (number o)
  o)

(define (pat o)
  o)

(define (str o)
  o)

(define* (variable-slash name pattern #:optional (replace ""))
  (let ((value (variable name))
        (glob? (glob? pattern)))
    (let ((match (if glob? (let ((regexp (glob->regex pattern #:begin "" #:end "")))
                             (regexp-exec regexp value))
                     (string-match pattern value))))
      (if match (string-append
                 (substring value 0 (match:start match))
                 replace
                 (substring value (match:end match)))
          value))))

(define (compound . o)
  (match o
    ((h ... t) t)
    (_ o)))

(define (delim o . rest)
  (match rest
    (() o)
    (((? string?) ...) (string-append o (string-join rest "")))
    ((((? string?) ...)) (string-append o (string-join (car rest) "")))))

(define (name o)
  o)

(define (regex-sep o)
  o)

(define (shift . o)
  (apply (shift-command) o))
