#lang racket/base
(require ffi/unsafe
         ffi/unsafe/define
         "ffi-constants.rkt")
(provide (all-from-out "ffi-constants.rkt"))
(provide (protect-out (all-defined-out)))

(define-cpointer-type _sqlhandle)

(define-cpointer-type _sqlhenv)
(define-cpointer-type _sqlhdbc)
(define-cpointer-type _sqlhstmt)

(define _sqllen _long)
(define _sqlulen _ulong)

(define _sqlsmallint _sshort)
(define _sqlusmallint _ushort)
(define _sqlinteger _sint)
(define _sqluinteger _uint)
(define _sqlreturn _sqlsmallint)

(define-ffi-definer define-mz #f)

(define-mz scheme_utf16_to_ucs4
  (_fun (src srcstart srcend) ::
        (src : _bytes)
        (srcstart : _intptr)
        (srcend : _intptr)
        (#f : _pointer)   ;; No buffer so it'll allocate for us.
        (0 : _intptr)
        (clen : (_ptr o _intptr))
        (1 : _intptr)
        -> (out : _gcpointer)
        -> (begin (ptr-set! out _int32 clen 0)
                  (values out clen))))

(define-mz scheme_ucs4_to_utf16
  (_fun (src srcstart srcend) ::
        (src : _string/ucs-4)
        (srcstart : _intptr)
        (srcend : _intptr)
        (#f : _pointer)   ;; No buffer so it'll allocate for us.
        (0 : _intptr)
        (clen : (_ptr o _intptr))
        (1 : _intptr)
        -> (out : _gcpointer)
        -> (begin (ptr-set! out _int16 clen 0)
                  (values out clen))))

(define-mz scheme_make_sized_char_string
  (_fun (chars clen copy?) ::
        (chars : _gcpointer)
        (clen  : _intptr)
        (copy? : _bool)
        -> _racket))

(define scheme_make_sized_byte_string/string
  (get-ffi-obj 'scheme_make_sized_byte_string #f
               (_fun (buf len) ::
                     (buf : _string/ucs-4)
                     (len  : _intptr)
                     (#t : _bool)
                     -> _racket)))

;; For dealing with param buffers, which must not be moved by GC

(define (copy-buffer buffer)
  (let* ([buffer (if (string? buffer) (string->bytes/utf-8 buffer) buffer)]
         [n (bytes-length buffer)]
         [rawcopy (malloc (add1 n) 'atomic-interior)]
         [copy (make-sized-byte-string rawcopy n)])
    (memcpy copy buffer n)
    (ptr-set! rawcopy _byte n 0)
    copy))

(define (int->buffer n)
  (let ([copy (make-sized-byte-string (malloc 4 'atomic-interior) 4)])
    (integer->integer-bytes n 4 #t (system-big-endian?) copy 0)
    copy))

(define (cpstr2 str)
  (let-values ([(shorts slen) (scheme_ucs4_to_utf16 str 0 (string-length str))])
    (let* ([n (* slen 2)]
           [rawcopy (malloc (add1 n) 'atomic-interior)]
           [copy (make-sized-byte-string rawcopy n)])
      (memcpy copy shorts n)
      (ptr-set! rawcopy _byte n 0)
      copy)))

(define (cpstr4 str)
  (copy-buffer (scheme_make_sized_byte_string/string str (* (string-length str) 4))))

(define (mkstr2 buf len fresh?)
  (let-values ([(chars clen) (scheme_utf16_to_ucs4 buf 0 (quotient len 2))])
    (scheme_make_sized_char_string chars clen #f)))

(define (mkstr4 buf len fresh?)
  (scheme_make_sized_char_string buf (quotient len 4) (not fresh?)))

;; ========================================

;; Used in connection.rkt; silly hack to keep optimizer from eliminating ref to
;; things that shouldn't be GC'd. Depends on no cross-module inlining.
(define (strong-void x) (void))

;; ========================================

#|
Docs at http://msdn.microsoft.com/en-us/library/ms712628%28v=VS.85%29.aspx
|#

(define odbc-lib
  (case (system-type)
    ((windows) (ffi-lib "odbc32.dll"))
    ((macosx)  (ffi-lib "libiodbc" '("2" #f)))
    ((unix)    (ffi-lib "libodbc" '("1" #f)))))

(define WCHAR-SIZE
  (case (system-type)
    ((windows)
     ;; Windows ODBC defines wchar_t, thus WCHAR, thus SQLWCHAR, as 16-bit
     2)
    ((macosx)
     ;; MacOSX uses iodbc, which defines SQLWCHAR as wchar_t, as 32-bit
     4)
    ((unix)
     ;; unixodbc defines WCHAR as 16-bit for compat w/ Windows
     ;; (even though Linux wchar_t is 32-bit)
     2)))

(define-ffi-definer define-odbc odbc-lib)

(define (ok-status? n)
  (or (= n SQL_SUCCESS)
      (= n SQL_SUCCESS_WITH_INFO)))

(define-odbc SQLAllocHandle
  (_fun (type : _sqlsmallint)
        (parent : _sqlhandle/null)
        (handle : (_ptr o _sqlhandle/null))
        -> (status : _sqlreturn)
        -> (values status
                   (cond [handle
                          (cpointer-push-tag! handle
                                              (cond [(= type SQL_HANDLE_ENV) sqlhenv-tag]
                                                    [(= type SQL_HANDLE_DBC) sqlhdbc-tag]
                                                    [(= type SQL_HANDLE_STMT) sqlhstmt-tag]
                                                    [else sqlhandle-tag]))
                          handle]
                         [else handle]))))

;; SQLSetEnvAttr
;; must set odbc version env attr before making connection

(define-odbc SQLSetEnvAttr
  (_fun (env : _sqlhenv)
        (attr : _sqlinteger)
        (value-buf : _sqlinteger) ;; (the one case we care about takes int, not ptr)
        (_sqlinteger = 0)
        -> _sqlreturn))

(define-odbc SQLGetInfo
  (_fun (handle info) ::
        (handle : _sqlhdbc)
        (info : _sqlusmallint)
        (value : (_ptr o _sqluinteger)) ;; the one case we care about is uint, not char
        (0 : _sqlsmallint)
        (#f : _pointer)
        -> (status : _sqlreturn)
        -> (values status value)))

(define SQLGetInfo-string
  (get-ffi-obj "SQLGetInfo" odbc-lib
               (_fun (handle info) ::
                     (handle : _sqlhdbc)
                     (info : _sqlusmallint)
                     (value : _bytes = (make-bytes 250))
                     (250 : _sqlsmallint)
                     (len : (_ptr o _sqlsmallint))
                     -> (status : _sqlreturn)
                     -> (values status
                                (and (ok-status? status)
                                     (bytes->string/utf-8 value #f 0 len))))))

(define-odbc SQLGetFunctions
  (_fun (handle : _sqlhdbc)
        (function-id : _sqlusmallint)
        (supported? : (_ptr o _sqlusmallint))
        -> (status : _sqlreturn)
        -> (values status (positive? supported?))))

(define-odbc SQLConnect
  (_fun (handle server user auth) ::
        (handle : _sqlhdbc)
        (server : _string)
        ((string-utf-8-length server) : _sqlsmallint)
        (user : _string)
        ((if user (string-utf-8-length user) 0) : _sqlsmallint)
        (auth : _string)
        ((if auth (string-utf-8-length auth) 0) : _sqlsmallint)
        -> _sqlreturn))

(define-odbc SQLDriverConnect
  (_fun (handle connection driver-completion) ::
        (handle : _sqlhdbc)
        (#f : _pointer)
        (connection : _string)
        ((if connection (string-utf-8-length connection) 0) : _sqlsmallint)
        (#f : _bytes)
        (0 : _sqlsmallint)
        (out-length : (_ptr o _sqlsmallint))
        (driver-completion : _sqlusmallint)
        -> (status : _sqlreturn)
        -> status))

(define-odbc SQLBrowseConnect
  (_fun (handle in-conn-string) ::
        (handle : _sqlhdbc)
        (in-conn-string : _string)
        ((if in-conn-string (string-utf-8-length in-conn-string) 0) : _sqlsmallint)
        (out-buf : _bytes = (make-bytes 1024))
        ((bytes-length out-buf) : _sqlsmallint)
        (out-len : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status
                   (and (ok-status? status)
                        (bytes->string/utf-8 out-buf #f 0 out-len)))))

(define-odbc SQLDataSources
  (_fun (handle direction server-buf descr-buf) ::
        (handle : _sqlhenv)
        (direction : _sqlusmallint)
        (server-buf : _bytes)
        ((bytes-length server-buf) : _sqlsmallint)
        (server-length : (_ptr o _sqlsmallint))
        (descr-buf : _bytes)
        ((bytes-length descr-buf) : _sqlsmallint)
        (descr-length : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status
                   (and (ok-status? status)
                        (bytes->string/utf-8 server-buf #f 0 server-length))
                   (and (ok-status? status)
                        (bytes->string/utf-8 descr-buf #f 0 descr-length)))))

(define-odbc SQLDrivers
  (_fun (handle direction driver-buf attrs-buf) ::
        (handle : _sqlhenv)
        (direction : _sqlusmallint)
        (driver-buf : _bytes)
        ((bytes-length driver-buf) : _sqlsmallint)
        (driver-length : (_ptr o _sqlsmallint))
        (attrs-buf : _bytes)
        ((if attrs-buf (bytes-length attrs-buf) 0) : _sqlsmallint)
        (attrs-length : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (if (ok-status? status)
               (values status
                       (bytes->string/utf-8 driver-buf #f 0 driver-length)
                       attrs-length)
               (values status #f #f))))

(define-odbc SQLPrepare
  (_fun (handle stmt) ::
        (handle : _sqlhstmt)
        (stmt : _string)
        ((string-utf-8-length stmt) : _sqlinteger)
        -> _sqlreturn))

(define-odbc SQLBindParameter
  (_fun (handle param-num iomode c-type sql-type column-size digits value len-or-ind) ::
        (handle : _sqlhstmt)
        (param-num : _sqlusmallint)
        (iomode : _sqlsmallint)
        (c-type : _sqlsmallint)
        (sql-type : _sqlsmallint)
        (column-size : _sqlulen)
        (digits : _sqlsmallint)
        (value : _pointer) ;; must be pinned until after SQLExecute called
        ((if (bytes? value) (bytes-length value) 0) : _sqllen) ;; ignored for fixed-length data
        (len-or-ind : _pointer) ;; _sqllen-pointer)
        -> _sqlreturn))

(define-odbc SQLExecute
  (_fun (handle : _sqlhstmt)
        -> _sqlreturn))

(define-odbc SQLNumParams
  (_fun (handle : _sqlhstmt)
        (count : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status count)))

(define-odbc SQLDescribeParam
  (_fun (handle : _sqlhstmt)
        (parameter : _sqlusmallint)
        (data-type : (_ptr o _sqlsmallint))
        (size : (_ptr o _sqlulen))
        (digits : (_ptr o _sqlsmallint))
        (nullable : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status data-type size digits nullable)))

(define-odbc SQLNumResultCols
  (_fun (handle : _sqlhstmt)
        (count : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status count)))

(define-odbc SQLDescribeCol
  (_fun (handle column column-buf) ::
        (handle : _sqlhstmt)
        (column : _sqlusmallint)
        (column-buf : _bytes)
        ((bytes-length column-buf) : _sqlsmallint)
        (column-len : (_ptr o _sqlsmallint))
        (data-type : (_ptr o _sqlsmallint))
        (size : (_ptr o _sqlulen))
        (digits : (_ptr o _sqlsmallint))
        (nullable : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status
                   (and (ok-status? status)
                        (bytes->string/utf-8 column-buf #f 0 column-len))
                   data-type size digits nullable)))

(define-odbc SQLFetch
  (_fun _sqlhstmt
        -> _sqlreturn))

(define-odbc SQLGetData
  (_fun (handle column target-type buffer start) ::
        (handle : _sqlhstmt)
        (column : _sqlusmallint)
        (target-type : _sqlsmallint)
        ((ptr-add buffer start) : _gcpointer)
        ((- (bytes-length buffer) start) : _sqllen)
        (len-or-ind : (_ptr o _sqllen))
        -> (status : _sqlreturn)
        -> (values status len-or-ind)))

(define-odbc SQLFreeStmt
  (_fun (handle : _sqlhstmt)
        (option : _sqlusmallint)
        -> _sqlreturn))

(define-odbc SQLCloseCursor
  (_fun (handle : _sqlhstmt)
        -> _sqlreturn))

(define-odbc SQLDisconnect
  (_fun (handle : _sqlhdbc)
        -> _sqlreturn))

(define-odbc SQLFreeHandle
  (_fun (handle-type : _sqlsmallint)
        (handle : _sqlhandle)
        -> _sqlreturn))

(define-odbc SQLGetDiagRec
  (_fun (handle-type handle rec-number) ::
        (handle-type : _sqlsmallint)
        (handle : _sqlhandle)
        (rec-number : _sqlsmallint)
        (sql-state-buf : _bytes = (make-bytes 6))
        (native-errcode : (_ptr o _sqlinteger))
        (message-buf : _bytes = (make-bytes 1024))
        ((bytes-length message-buf) : _sqlsmallint)
        (message-len : (_ptr o _sqlsmallint))
        -> (status : _sqlreturn)
        -> (values status
                   (and (ok-status? status)
                        (bytes->string/utf-8 sql-state-buf #\? 0 5))
                   native-errcode
                   (and (ok-status? status)
                        (bytes->string/utf-8 message-buf #\? 0 message-len)))))

(define-odbc SQLEndTran
  (_fun (handle completion-type) ::
        (_sqlsmallint = SQL_HANDLE_DBC)
        (handle : _sqlhandle)
        (completion-type : _sqlsmallint)
        -> _sqlreturn))

(define-odbc SQLGetConnectAttr
  (_fun (handle attr) ::
        (handle : _sqlhdbc)
        (attr : _sqlinteger)
        (value : (_ptr o _sqluinteger)) ;; the attrs we care about have uint value
        (buflen : _sqlinteger = 0) ;; ignored
        (#f : _pointer)
        -> (status : _sqlreturn)
        -> (values status value)))

(define-odbc SQLSetConnectAttr
  (_fun (handle attr value) ::
        (handle : _sqlhdbc)
        (attr : _sqlinteger)
        (value : _sqluinteger) ;; the attrs we care about have uint value
        (_sqlinteger = 0)
        -> _sqlreturn))

(define-odbc SQLTables
  (_fun (handle catalog schema table) ::
        (handle : _sqlhstmt)
        (catalog : _string)
        (_sqlsmallint = (if catalog (string-utf-8-length catalog) 0))
        (schema : _string)
        (_sqlsmallint = (if schema (string-utf-8-length schema) 0))
        (table : _string)
        (_sqlsmallint = (if table (string-utf-8-length table) 0))
        (_bytes = #f)
        (_sqlsmallint = 0)
        -> _sqlreturn))
