#lang racket/base

(require racket/contract
         racket/format
         racket/function
         racket/lazy-require
         racket/match
         racket/set
         racket/tcp
         "channel.rkt"
         "debug.rkt"
         "elisp.rkt"
         "interactions.rkt"
         "md5.rkt"
         "mod.rkt"
         "util.rkt")

(lazy-require
 ["commands/check-syntax.rkt" (check-syntax)]
 ["commands/coverage.rkt"     (get-uncovered)]
 ["commands/describe.rkt"     (describe type)]
 ["commands/find-module.rkt"  (find-module)]
 ["commands/help.rkt"         (doc)]
 ["commands/macro.rkt"        (macro-stepper macro-stepper/next)]
 ["commands/profile.rkt"      (get-profile)]
 ["commands/requires.rkt"     (requires/tidy requires/trim requires/base)]
 ["find.rkt"                 (find-definition)])

(provide start-command-server
         attach-command-server
         make-prompt-read)

(define drracket:submit-predicate/c (-> input-port? boolean? boolean?))

(define-struct/contract context
  ([ns          namespace?]
   [maybe-mod   (or/c #f mod?)]
   [md5         string?]
   [submit-pred (or/c #f drracket:submit-predicate/c)]))

(define command-server-context (context (make-base-namespace) #f "" #f))

(define/contract (attach-command-server ns maybe-mod)
  (-> namespace? (or/c #f mod?) any)
  (set-debug-repl-namespace! ns)
  (set! command-server-context
        (context ns
                 maybe-mod
                 (maybe-mod->md5 maybe-mod)
                 (get-repl-submit-predicate maybe-mod))))

(define (maybe-mod->md5 m)
  (define-values (dir file _) (maybe-mod->dir/file/rmp m))
  (if (and dir file)
      (file->md5 (build-path dir file))
      ""))

;; <https://docs.racket-lang.org/tools/lang-languages-customization.html#(part._.R.E.P.L_.Submit_.Predicate)>
(define/contract (get-repl-submit-predicate m)
  (-> (or/c #f mod?) (or/c #f drracket:submit-predicate/c))
  (define-values (dir file rmp) (maybe-mod->dir/file/rmp m))
  (define path (and dir file (build-path dir file)))
  (and path rmp
       (or (with-handlers ([exn:fail? (λ _ #f)])
            (match (with-input-from-file (build-path dir file) read-language)
              [(? procedure? get-info)
               (match (get-info 'drracket:submit-predicate #f)
                 [#f #f]
                 [v  v])]
              [_ #f]))
           (with-handlers ([exn:fail? (λ _ #f)])
             (match (module->language-info rmp #t)
               [(vector mp name val)
                (define get-info ((dynamic-require mp name) val))
                (get-info 'drracket:submit-predicate #f)]
               [_ #f])))))

;; The command server accepts a single TCP connection at a time.
;;
;; Immediately after connecting, the client must send us exactly the
;; same '(accept ,random-value) value that it gave us as a command
;; line argument when it started us. Else we exit. See issue #327.
;;
;; Normally Emacs will make only one connection to us, ever. If the
;; user exits the REPL, then our entire Racket process exits. (Just in
;; case, we have an accept-a-connection loop below. It handles any
;; exns -- like exn:network -- not handled during command processing.
;; It uses a custodian to clean up.)
;;
;; Command requests and responses "on the wire" are a subset of valid
;; Emacs Lisp s-expressions: See elisp-read and elisp-write.
;;
;; Command requests are (nonce command param ...).
;;
;; A thread is spun off to handle each request, so that a long-running
;; command won't block others. The nonce supplied with the request is
;; returned with the response, so that the client can match the
;; response with the request. The nonce needn't be random, just
;; unique; an increasing integer is fine.
;;
;; Command responses are either (nonce 'ok sexp ...+) or (nonce 'error
;; "message"). The latter normally can and should be displayed to the
;; user in Emacs via error or message. We handle exn:fail? up here;
;; generally we're fine letting Racket exceptions percolate up and be
;; shown to the user
(define (start-command-server port launch-token)
  (thread
   (thunk
    (define listener (tcp-listen port 4 #t "127.0.0.1"))
    (let accept-a-connection ()
      (define custodian (make-custodian))
      (parameterize ([current-custodian custodian])
        (with-handlers ([exn:fail? void]) ;just disconnect; see #327
          (define-values (in out) (tcp-accept listener))
          (unless (or (not launch-token)
                      (equal? launch-token (elisp-read in)))
            (display-commented "Authorization failed; exiting")
            (exit 1)) ;see #327
          (define response-channel (make-channel))
          (define ((do-command/put-response nonce sexp))
            (channel-put
             response-channel
             (cons
              nonce
              (with-handlers ([exn:fail? (λ (e) `(error ,(exn-message e)))])
                (parameterize ([current-namespace
                                (context-ns command-server-context)])
                  `(ok ,(command sexp command-server-context)))))))
          (define (get/write-response)
            (elisp-writeln (sync response-channel
                                 debug-notify-channel)
                           out)
            (flush-output out)
            (get/write-response))
          ;; With all the pieces defined, let's go:
          (thread get/write-response)
          (let read-a-command ()
            (match (elisp-read in)
              [(cons nonce sexp) (thread (do-command/put-response nonce sexp))
                                 (read-a-command)]
              [(? eof-object?)   (void)])))
        (custodian-shutdown-all custodian))
      (accept-a-connection))))
  (void))

(define/contract ((make-prompt-read m))
  (-> (or/c #f mod?) (-> any))
  (begin0 (get-interaction (maybe-mod->prompt-string m))
    (next-break 'all))) ;let debug-instrumented code break again

(define/contract (command sexpr the-context)
  (-> pair? context? any/c)
  (match-define (context _ns maybe-mod md5 submit-pred) the-context)
  (define-values (dir file mod-path) (maybe-mod->dir/file/rmp maybe-mod))
  (define path (and dir file (build-path dir file)))
  ;; Note: Intentionally no "else" match clause -- let caller handle
  ;; exn and supply a consistent exn response format.
  (match sexpr
    [`(run ,what ,mem ,pp? ,ctx ,args ,dbg) (run what mem pp? ctx args dbg)]
    [`(path+md5)                            (cons (or path 'top) md5)]
    [`(syms)                                (syms)]
    [`(def ,str)                            (find-definition str)]
    [`(mod ,sym)                            (find-module sym maybe-mod)]
    [`(describe ,str)                       (describe str)]
    [`(doc ,str)                            (doc str)]
    [`(type ,v)                             (type v)]
    [`(macro-stepper ,str ,into-base?)      (macro-stepper str into-base?)]
    [`(macro-stepper/next)                  (macro-stepper/next)]
    [`(requires/tidy ,reqs)                 (requires/tidy reqs)]
    [`(requires/trim ,path-str ,reqs)       (requires/trim path-str reqs)]
    [`(requires/base ,path-str ,reqs)       (requires/base path-str reqs)]
    [`(find-collection ,str)                (find-collection str)]
    [`(get-profile)                         (get-profile)]
    [`(get-uncovered)                       (get-uncovered path)]
    [`(check-syntax ,path-str)              (check-syntax path-str)]
    [`(eval ,v)                             (eval-command v)]
    [`(repl-submit? ,str ,eos?)             (repl-submit? submit-pred str eos?)]
    [`(debug-eval ,src ,l ,c ,p ,code)      (debug-eval src l c p code)]
    [`(debug-resume ,v)                     (debug-resume v)]
    [`(debug-disable)                       (debug-disable)]
    [`(exit)                                (exit)]))

;;; A few commands defined here

(define/contract (run what mem pp ctx args dbgs)
  (-> list? number? elisp-bool/c context-level? list? (listof path-string?)
      list?)
  (define ready-channel (make-channel))
  (channel-put message-to-main-thread-channel
               (rerun (->mod/existing what)
                      mem
                      (as-racket-bool pp)
                      ctx
                      (list->vector args)
                      (list->set (map string->path dbgs))
                      (λ () (channel-put ready-channel what))))
  ;; Waiting for this allows the command response to be used as the
  ;; all-clear for additional commands that need the module load to be
  ;; done and entering a REPL for that module. For example, to compose
  ;; run with get-profile or get-uncovered.
  (sync ready-channel))

(define/contract (repl-submit? submit-pred text eos)
  (-> (or/c #f drracket:submit-predicate/c) string? elisp-bool/c (or/c 'default #t #f))
  (if submit-pred
      (submit-pred (open-input-string text) (as-racket-bool eos))
      'default))

(define (syms)
  (sort (map symbol->string (namespace-mapped-symbols))
        string<?))

;;; eval-commmand

(define/contract (eval-command str)
  (-> string? string?)
  (define results
    (call-with-values (λ ()
                        ((current-eval) (string->namespace-syntax str)))
                      list))
  (~a (map ~v results) "\n"))

;;; find-collection

(define/contract (find-collection str)
  (-> path-string? (or/c 'find-collection-not-installed #f (listof string?)))
  (define fcd (with-handlers ([exn:fail:filesystem:missing-module?
                               (λ _ (error 'find-collection
                                           "For this to work, you need to `raco pkg install raco-find-collection`."))])
                (dynamic-require 'find-collection/find-collection
                                 'find-collection-dir)))
  (map path->string (fcd str)))
