#lang racket/base
(require racket/contract
         net/url)
(require web-server/managers/manager
         web-server/private/util
         web-server/private/servlet
         web-server/servlet/web-cells
         web-server/servlet/servlet-structs
         web-server/http)

(provide servlet-prompt)

;; ********************************************************************************
;; Parameter Embedding
(require web-server/private/url-param)

;; embed-ids: (list number number number) url -> string
(define (embed-ids v u)
  (url->string
   (insert-param 
    u "k" (write/string v))))

;; continuation-url?: url -> (or/c (list number number number) #f)
;; determine if this url encodes a continuation and extract the instance id and
;; continuation id.
(define (continuation-url? a-url)
  (parameterize ([read-accept-dot #t])
    (cond 
      [(extract-param a-url "k")
       => read/string]
      [else
       #f])))

(provide/contract
 [continuation-url? (url? . -> . (or/c false/c (list/c number? number? number?)))])

;; ********************************************************************************

(provide/contract
 [current-servlet-continuation-expiration-handler 
  (parameter/c 
   (or/c false/c
         (request? . -> . can-be-response?)))]
 [redirect/get (() (#:headers (listof header?)) . ->* . request?)]
 [redirect/get/forget (() (#:headers (listof header?)) . ->* . request?)]
 [adjust-timeout! (number? . -> . void?)]
 [clear-continuation-table! (-> void?)]
 [send/back (can-be-response? . -> . void?)]
 [send/finish (can-be-response? . -> . void?)]
 [send/forward ((string? . -> . can-be-response?) . -> . request?)]
 [send/suspend ((string? . -> . can-be-response?) . -> . request?)]
 [send/suspend/dispatch ((((request? . -> . any) . -> . string?) . -> . can-be-response?) . -> . any)]
 [send/suspend/url ((url? . -> . can-be-response?) . -> . request?)]
 [send/suspend/url/dispatch ((((request? . -> . any) . -> . url?) . -> . can-be-response?) . -> . any)])

;; ************************************************************
;; EXPORTS

;; current-servlet-continuation-expiration-handler : request -> response
(define current-servlet-continuation-expiration-handler
  (make-parameter #f))

;; adjust-timeout! : sec -> void
;; adjust the timeout on the servlet
(define (adjust-timeout! secs)
  ((manager-adjust-timeout! (current-servlet-manager)) (current-servlet-instance-id) secs))

;; ext:clear-continuations! -> void
(define (clear-continuation-table!)
  ((manager-clear-continuations! (current-servlet-manager)) (current-servlet-instance-id)))

;; send/back: response -> void
;; send a response and don't clear the continuation table
(define (send/back resp)
  (abort-current-continuation servlet-prompt (lambda () resp)))

;; send/finish: response -> void
;; send a response and clear the continuation table
(define (send/finish resp)
  (clear-continuation-table!)
  (send/back resp))

;; send/suspend: (url -> response) -> request
;; send a response and apply the continuation to the next request
(define (send/suspend response-generator)
  (define wcs (capture-web-cell-set))
  (begin0
    (call-with-composable-continuation
     (lambda (k)
       (define instance-id (current-servlet-instance-id))
       (define ctxt (current-execution-context))
       (define k-embedding ((manager-continuation-store! (current-servlet-manager))
                            instance-id
                            (make-custodian-box (current-custodian) k)
                            (current-servlet-continuation-expiration-handler)))
       (define k-url (embed-ids 
                      (list* instance-id k-embedding)
                      (request-uri (execution-context-request ctxt))))
       (send/back (response-generator k-url)))
     servlet-prompt)
    (restore-web-cell-set! wcs)))

(define (send/suspend/url response-generator)
  (send/suspend
   (lambda (k-url)
     (response-generator (string->url k-url)))))

;; send/forward: (url -> response) [(request -> response)] -> request
;; clear the continuation table, then behave like send/suspend
(define (send/forward response-generator)
  (clear-continuation-table!)
  (send/suspend response-generator))

;; send/suspend/dispatch : ((proc -> url) -> response) [(request -> response)] -> request
;; send/back a response generated from a procedure that may convert
;; procedures to continuation urls
(define (send/suspend/dispatch response-generator)
  ; This restores the tail position.
  ; Note: Herman's syntactic strategy would fail without the new-request capture.
  ;       (Moving this to the tail-position is not possible anyway, by the way.)
  (let ([thunk 
         (call-with-current-continuation
          (lambda (k0)
            (send/back
             (response-generator
              (lambda (proc)
                (let/ec k1 
                  ; This makes the second continuation captured by send/suspend smaller
                  (call-with-continuation-prompt
                   (lambda ()
                     (let ([new-request (send/suspend k1)])
                       (k0 (lambda () (proc new-request)))))
                   servlet-prompt))))))
          servlet-prompt)])
    (thunk)))

(define (send/suspend/url/dispatch response-generator)
  (send/suspend/dispatch
   (lambda (embed/url)
     (response-generator
      (lambda (proc)
        (string->url (embed/url proc)))))))

;; ************************************************************
;; HIGHER-LEVEL EXPORTS

(define ((make-redirect/get send/suspend) #:headers [hs null])
  (send/suspend (lambda (k-url) (redirect-to k-url see-other #:headers hs))))

; redirect/get : -> request
(define redirect/get (make-redirect/get send/suspend))
(define redirect/get/forget (make-redirect/get send/forward))

(define (with-errors-to-browser send/finish-or-back thunk)
  (with-handlers 
      ([exn:fail?
        (lambda (exn)
          (send/finish-or-back
           (response/xexpr
            #:code 500
            #:message #"Internal Server Error"
            `(html (head (title "Servlet Error"))
                   (body ([bgcolor "white"])
                         (p "The following error occured: "
                            (pre ,(exn->string exn))))))))])
    (thunk)))

(provide/contract
 [with-errors-to-browser
  ((can-be-response? . -> . request?)
   (-> any)
   . -> .
   any)])
