#lang racket/base

(require (for-syntax racket/base)
         ffi/unsafe
         ffi/unsafe/define
         racket/runtime-path)

(provide
 _mp_size_t
 _mp_limb_t
 _mp_bitcnt_t
 sizeof-mp_limb_t
 gmp-limb-bits
 _mpz
 _mpz-pointer
 (struct-out mpz)
 new-mpz
 integer->mpz
 mpz->integer
 ;; Low-level
 gmp-lib)

;; ===================================================================================================
;; Setup

(define-runtime-path libgmp-so 
  (case (system-type)
    [(macosx) '(so "libgmp.10.dylib")]
    [(windows) '(so "libgmp-10.dll")]
    [else '(so "libgmp")]))

(define gmp-lib (ffi-lib libgmp-so '("10" "3" "") #:fail (λ () #f)))

(define (get-gmp-fun name type)
  (get-ffi-obj name gmp-lib type (make-not-available name)))

;; ===================================================================================================
;; Types

(define 64-bit? (= 8 (ctype-sizeof _intptr)))
(define win64? (and 64-bit? (eq? (system-type) 'windows)))

;; The Win64 build of libgmp is compiled using mingw64, for which GMP's configure script defines
;; LONG_LONG_LIMB
(define _mp_limb_t (if win64? _ullong _ulong))
(define _mp_size_t _long)  ; used for limb counts
(define _mp_bitcnt_t _ulong)

(define sizeof-mp_limb_t (ctype-sizeof _mp_limb_t))

;; Number of bits in a limb
(define gmp-limb-bits (* 8 sizeof-mp_limb_t))
;; We don't support "nail" builds, which haven't worked since before GMP 4.3 anyway (a "nail" takes
;; two bits from each limb, and is supposed to speed up carries between limbs on certain systems)

;; GMP's big integers
(define-cstruct _mpz ([alloc _int] [size _int] [limbs _pointer]))

;; ALWAYS let GMP manage the memory pointed at by the limbs field in an _mpz. It likes to reallocate
;; limbs, particularly when the _mpz is an output argument.

;; This means that _mpz instances take up memory that Racket can't account for, so they should only
;; be used as temporary values. Possibly permanent values should be either instances of _mpfr or
;; Racket's own exact integers.

(define mpz-init (get-gmp-fun '__gmpz_init (_fun _mpz-pointer -> _void)))
(define mpz-init2 (get-gmp-fun '__gmpz_init2 (_fun _mpz-pointer _mp_bitcnt_t -> _void)))
(define mpz-clear (get-gmp-fun '__gmpz_clear (_fun _mpz-pointer -> _void)))

;; new-mpz : [_mp_bitcnt_t] -> _mpz-pointer
;; Creates an _mpz that is managed by the garbage collector, but whose limbs are not, and which is
;; not relocatable. These are always safe to pass to mpz_* functions.
(define (new-mpz [bits 0])
  (define z (make-mpz 0 0 #f))
  (if (= bits 0) (mpz-init z) (mpz-init2 z bits))
  (register-finalizer z (λ (z) (mpz-clear z)))
  z)

;; ===================================================================================================
;; Conversions

(define mpz-get-si (get-gmp-fun '__gmpz_get_si (_fun _mpz-pointer -> _long)))
(define mpz-fits-long? (get-gmp-fun '__gmpz_fits_slong_p (_fun _mpz-pointer -> _int)))

;; integer->mpz : integer -> _mpz
;; Converts a Racket integer to an _mpz.
(define (integer->mpz n)
  (define abs-n (abs n))
  (define z (new-mpz (integer-length abs-n)))
  (define len (mpz-alloc z))
  (define limbs (mpz-limbs z))
  (let loop ([i 0])
    (when (i . < . len)
      (define bit (* i gmp-limb-bits))
      (ptr-set! limbs _mp_limb_t i (bitwise-bit-field abs-n bit (+ bit gmp-limb-bits)))
      (loop (+ i 1))))
  (define size (ceiling (/ (integer-length abs-n) gmp-limb-bits)))
  (set-mpz-size! z (if (< n 0) (- size) size))
  z)

;; size+limbs->integer : integer (listof integer) -> integer
;; Converts a size (which may be negative) and a limb list into an integer.
(define (size+limbs->integer size limbs)
  (define len (abs size))
  (define num
    (let loop ([i 0] [res  0])
      (cond [(i . < . len)
             (define v (ptr-ref limbs _mp_limb_t i))
             (loop (+ i 1) (bitwise-ior res (arithmetic-shift v (* i gmp-limb-bits))))]
            [else  res])))
  (if (negative? size) (- num) num))

;; mpz->integer : _mpz -> integer
;; Converts an mpz_t to a Racket integer.
(define (mpz->integer z)
  (if (zero? (mpz-fits-long? z))
      (begin0
        (size+limbs->integer (mpz-size z) (mpz-limbs z))
        (void/reference-sink z))
      (mpz-get-si z)))

;; ===================================================================================================
;; Number Theoretic Functions
;; http://gmplib.org/manual/Number-Theoretic-Functions.html#Number-Theoretic-Functions

(define mpz-probab-prime-p 
  (get-gmp-fun '__gmpz_probab_prime_p  (_fun _mpz-pointer _int -> _int)))

(define (probably-prime n [repetitions 10])
  (case (mpz-probab-prime-p (integer->mpz n) repetitions)
    [(0) 'composite]
    [(1) 'probably-prime]
    [(2) 'prime]))

(define (prime? n)
  (case (probably-prime n)
    [(probably-prime prime) #t]
    [else #f]))

(define mpz-nextprime
  (get-gmp-fun '__gmpz_nextprime  (_fun _mpz-pointer _mpz-pointer -> _void)))

(define (next-prime op)
  (define result (new-mpz))
  (mpz-nextprime result (integer->mpz op))
  (mpz->integer result))

(define mpz-invert
  (get-gmp-fun '__gmpz_invert  (_fun _mpz-pointer _mpz-pointer _mpz-pointer -> _void)))

(define (mod-inverse n modulus)
  (define result (new-mpz))
  (mpz-invert result (integer->mpz n) (integer->mpz modulus))
  (mpz->integer result))

(define mpz-jacobi
  (get-gmp-fun '__gmpz_jacobi  (_fun _mpz-pointer _mpz-pointer -> _int)))

(define (jacobi a b)
  (mpz-jacobi (integer->mpz a) (integer->mpz b)))

(define mpz-legendre
  (get-gmp-fun '__gmpz_legendre  (_fun _mpz-pointer _mpz-pointer -> _int)))

(define (legendre a b)
  (mpz-legendre (integer->mpz a) (integer->mpz b)))

(define mpz-remove
  (get-gmp-fun '__gmpz_remove  (_fun _mpz-pointer _mpz-pointer _mpz-pointer -> _int)))

(define (remove-factor z f)
  (define result (new-mpz))
  (define n (mpz-remove result (integer->mpz z) (integer->mpz f)))
  (define z/f (mpz->integer result))
  (values z/f n))
