#lang racket/base

(require xml/xml
         syntax/readerr
         racket/gui/base
         racket/class
         racket/list
         "syntax-property.rkt")

(provide xml-read-special
         xml-snip<%>
         scheme-read-special
         scheme-snip<%>)

(define (scheme-read-special snip source line col pos)
  (let ([text (send snip get-editor)]
        [splice? (send snip get-splice?)])
    (when (= 0 (send text last-position))
      (raise-read-error 
       (if splice?
           "read: bad syntax: empty scheme splice box"
           "read: bad syntax: empty scheme box")
       source line col pos 1))
    (let* ([source-name (get-source-name text)]
           [stx (read-syntax source-name
                             (open-input-text-editor text 0 'end values source-name))])
      (when (eof-object? stx)
        (raise-read-error
         (if splice?
             "read: bad syntax: empty scheme splice box"
             "read: bad syntax: empty scheme box")
         source-name 1 1 1 (send text last-position)))
      stx)))

(define (get-source-name text)
  (cond
    [(method-in-interface? 'get-port-name (object-interface text))
     (send text get-port-name)]
    [else
     (send text get-filename)]))

(define (xml-read-special eliminate-whitespace-in-empty-tags? snip source line col pos)
  (let ([editor (send snip get-editor)]
        [old-locked #f])
    (when (= 0 (send editor last-position))
      (raise-read-error "read: bad syntax: empty xml box"
                        source line col pos 1))
    (dynamic-wind
     (lambda () 
       (set! old-locked (send editor is-locked?))
       (send editor lock #t))
     (lambda ()
       (let* ([source-name (get-source-name editor)]
              [port (open-input-text-editor editor 0 'end (xml-snip-filter editor) source-name)]
              [xml (parameterize ([permissive-xexprs #t]) (read-xml port))]
              [xexpr (parameterize ([permissive-xexprs #t]) (xml->xexpr (document-element xml)))]
              [clean-xexpr (if eliminate-whitespace-in-empty-tags?
                               (eliminate-whitespace-in-empty-tags xexpr)
                               xexpr)]
              [expd-xexpr (expand-embedded clean-xexpr)]
              [qq-body (datum->syntax #f expd-xexpr (list editor #f #f #f #f))])
         (stepper-syntax-property (datum->syntax #f (list 'quasiquote qq-body))
                                  'stepper-xml-hint
                                  'from-xml-box)))
     (lambda () (send editor lock old-locked)))))

(define ((xml-snip-filter text) s)
  (cond
    [(is-a? s scheme-snip<%>)
     (let* ([position (send text get-snip-position s)]
            [line (send text position-paragraph position)]
            [col (- position (send text paragraph-start-position line))])
       (make-wrapped s text line col position))]
    [else s]))

(define scheme-snip<%>
  (interface ()
    get-splice?))

(define xml-snip<%>
  (interface ()))

;; eliminate-whitespace-in-empty-tags : xexpr -> xexpr
(define (eliminate-whitespace-in-empty-tags xexpr)
  (cond
    [(and (pair? xexpr)
          (symbol? (car xexpr)))
     (list* (car xexpr)
            (cadr xexpr)
            (map eliminate-whitespace-in-empty-tags
                 (eliminate-whitespace-in-list (cddr xexpr))))]
    [else xexpr]))

;; wrapped = (make-wraped sexp text number number number)
(define-struct wrapped (snip text line col pos))

;; expand-embedded : xexpr -> xexpr
;; constructs a new xexpr that has the embedded snips expanded 
;; and wrapped with unquotes
;; CRUCIAL INVARIANT: an expression must not receive both 'from-xml-box and 'from-scheme/splice-box tags.
(define (expand-embedded _xexpr)
  (let loop ([xexpr _xexpr])
    (cond
      [(pair? xexpr)
       (cons (loop (car xexpr))
             (loop (cdr xexpr)))]
      [(wrapped? xexpr)
       (let* ([snip (wrapped-snip xexpr)]
              [text (wrapped-text xexpr)]
              [pos (wrapped-pos xexpr)]
              [line (wrapped-line xexpr)]
              [col (wrapped-col xexpr)]
              [raw-stx (send snip read-special text line col pos)])
         (if (and (is-a? snip scheme-snip<%>)
                  (send snip get-splice?))
             (datum->syntax
              #f
              (list 'unquote-splicing
                    (stepper-syntax-property
                     (datum->syntax
                      #f
                      `(let ([lst ,raw-stx])
                         (if (list? lst)
                             lst
                             ,(datum->syntax
                               #f
                               '(error 'scheme-splice-box
                                       "expected a list, found: ~e" lst)
                               raw-stx))))
                     'stepper-xml-hint
                     'from-splice-box)))
             (datum->syntax
              #f
              (list 'unquote (stepper-syntax-property (datum->syntax #f (list 'begin raw-stx))
                                                      'stepper-xml-hint
                                                      'from-scheme-box)))))]
      [else xexpr])))

;; eliminate-whitespace-in-list (listof xexpr) -> (listof xexpr)
;; if each string in xexprs is a whitespace string, remove all strings
;; otherwise, return input.
(define (eliminate-whitespace-in-list xexprs)
  (cond
    [(andmap (lambda (x) (or (not (string? x))
                             (string-whitespace? x)))
             xexprs)
     (filter (lambda (x) (not (string? x))) xexprs)]
    [else xexprs]))

;; string-whitespace? : string -> boolean
;; return #t if the input string consists entirely of whitespace
(define (string-whitespace? str)
  (let loop ([i (string-length str)])
    (cond
      [(zero? i) #t]
      [(char-whitespace? (string-ref str (- i 1)))
       (loop (- i 1))]
      [else #f])))


;; transformable? : snip -> boolean
;; deteremines if a snip can be expanded here
(define (transformable? snip)
  (or (is-a? snip xml-snip<%>)
      (is-a? snip scheme-snip<%>)))
