module Data.Repa.Array.Auto.Convert
        ( -- * Int Conversion
          readIntFromOffset,    readIntFromOffset#

          -- * Double Conversion
          -- | Read and Show `Double`s for a reasonable runtime cost.
        , readDouble,           readDoubleFromBytes
        , showDouble,           showDoubleAsBytes
        , showDoubleFixed,      showDoubleFixedAsBytes)
where
import Data.Repa.Array.Auto.Base
import Data.Repa.Array.Generic.Convert
import System.IO.Unsafe
import Data.Word
import Data.Char
import GHC.Ptr
import GHC.Exts

import qualified Data.Repa.Array.Material.Auto          as A
import qualified Data.Repa.Array.Material.Foreign       as A
import qualified Data.Repa.Array.Meta                   as A
import qualified Data.Repa.Array.Generic                as A

import qualified Data.Double.Conversion.ByteString      as DC

import qualified Foreign.ForeignPtr                     as F
import qualified Foreign.Storable                       as F
import qualified Foreign.Marshal.Alloc                  as F
import qualified Foreign.Marshal.Utils                  as F


-------------------------------------------------------------------------------
-- | Try to read an `Int` from the given offset in an array.
-- 
--   If the conversion succeeded then you get the value, 
--   along with the index of the next character, 
--   otherwise `Nothing`.
--
readIntFromOffset  :: Array Char -> Int -> Maybe (Int, Int)
readIntFromOffset arr (I# ix0)
 = case readIntFromOffset# arr ix0 of
        (# 0#, _, _  #)  -> Nothing
        (# _ , n, ix #)  -> Just (I# n, I# ix)
{-# INLINE readIntFromOffset #-}


-- | Unboxed version of `readIntFromOffset`.
--
--   We still pay to unbox the input array, 
--   but avoid boxing the result by construction.
--
readIntFromOffset# :: Array Char -> Int# -> (# Int#, Int#, Int# #)
readIntFromOffset# !arr !ix0_
 = start ix0
 where

        !ix0    = I# ix0_
        !len    = A.length arr

        start !ix
         | ix >= len    = (# 0#, 0#, 0# #)
         | otherwise    = sign ix

        -- Check for explicit sign character,
        -- and encode what it was as an integer.
        sign !ix
         | !s   <- A.index arr 0
         = case s of
                '-'     -> loop 1 (ix + 1) 0
                '+'     -> loop 2 (ix + 1) 0
                _       -> loop 0  ix      0

        loop !(neg :: Int) !ix !n 
         -- We've hit the end of the array.
         | ix >= len   
         = end neg ix n

         | otherwise
         = case ord $ A.index arr ix of
               -- Current character is a digit, so add it to the accmulator.
             w |  w >= 0x30 && w <= 0x039
               -> loop neg (ix + 1) (n * 10 + (fromIntegral w - 0x30))

               -- Current character is not a digit.
               | otherwise
               -> end neg ix n

        end !neg !ix !n
         -- We didn't find any digits, and there was no explicit sign.
         | ix  == ix0
         , neg == 0  
         = (# 0#, 0#, 0# #)

         -- We didn't find any digits, but there was an explicit sign.
         | ix  == (ix0 + 1)
         , neg /= 0  
         = (# 0#, 0#, 0# #)

         -- Number was explicitly negated.
         | neg == 1                    
         , I# n'        <- negate n
         , I# ix'       <- ix
         = (# 1#, n', ix' #)

         -- Number was not negated.
         | otherwise
         , I# n'        <- n
         , I# ix'       <- ix
         = (# 1#, n', ix' #)
{-# NOINLINE readIntFromOffset# #-}


-------------------------------------------------------------------------------
-- | Convert a foreign vector of characters to a Double.
--
--   * The standard Haskell `Char` type is four bytes in length.
--     If you already have a vector of `Word8` then use `readDoubleFromBytes`
--     instead to avoid the conversion.
--
readDouble :: Array Char -> Double
readDouble vec
        = readDoubleFromBytes
        $ A.computeS A.A $ A.map (fromIntegral . ord) vec
{-# INLINE readDouble #-}


-- | Convert a foreign vector of bytes to a Double.
readDoubleFromBytes :: Array Word8 -> Double
readDoubleFromBytes 
   (A.toForeignPtr . convert -> (start, len, fptr :: F.ForeignPtr Word8))
 = unsafePerformIO
 $ F.allocaBytes (len + 1) $ \pBuf ->
   F.alloca                $ \pRes ->
   F.withForeignPtr fptr   $ \pIn  ->
    do
        -- Copy the data to our new buffer.
        F.copyBytes   pBuf (pIn `plusPtr` start) (fromIntegral len)

        -- Poke a 0 on the end to ensure it's null terminated.
        F.pokeByteOff pBuf len (0 :: Word8)

        -- Call the C strtod function
        let !d  = strtod pBuf pRes

        return d
{-# NOINLINE readDoubleFromBytes #-}


foreign import ccall unsafe
 strtod :: Ptr Word8 -> Ptr (Ptr Word8) -> Double


-------------------------------------------------------------------------------
-- | Convert a `Double` to ASCII text packed into a foreign `Vector`.
showDouble :: Double -> Array Char
showDouble !d
        = A.computeS A.A $ A.map (chr . fromIntegral)
        $ showDoubleAsBytes d
{-# INLINE showDouble #-}


-- | Convert a `Double` to ASCII text packed into a foreign `Vector`.
showDoubleAsBytes :: Double -> Array Word8
showDoubleAsBytes !d
        = convert
        $ A.fromByteString $ DC.toShortest d
{-# INLINE showDoubleAsBytes #-}


-- | Like `showDouble`, but use a fixed number of digits after
--   the decimal point.
showDoubleFixed :: Int -> Double -> Array Char
showDoubleFixed !prec !d
        = A.computeS A.A $ A.map (chr . fromIntegral)
        $ showDoubleFixedAsBytes prec d
{-# INLINE showDoubleFixed #-}


-- | Like `showDoubleAsBytes`, but use a fixed number of digits after
--   the decimal point.
showDoubleFixedAsBytes :: Int -> Double -> Array Word8
showDoubleFixedAsBytes !prec !d
        = convert
        $ A.fromByteString $ DC.toFixed prec d
{-# INLINE showDoubleFixedAsBytes #-}