{-# LANGUAGE DataKinds                 #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE KindSignatures            #-}
{-# LANGUAGE MagicHash                 #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE PolyKinds                 #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE TypeApplications          #-}
{-# LANGUAGE TypeFamilies              #-}
{-# LANGUAGE TypeInType                #-}
{-# LANGUAGE TypeOperators             #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Numeric.DataFrame.IO
-- Copyright   :  (c) Artem Chirkin
-- License     :  BSD3
--
-- Maintainer  :  chirkin@arch.ethz.ch
--
-- Mutable DataFrames living in IO.
--
-----------------------------------------------------------------------------

module Numeric.DataFrame.IO
    ( IODataFrame (XIOFrame), SomeIODataFrame (..)
    , newDataFrame, newPinnedDataFrame
    , copyDataFrame, copyMutableDataFrame
    , freezeDataFrame, unsafeFreezeDataFrame
    , thawDataFrame, thawPinDataFrame, unsafeThawDataFrame
    , writeDataFrame, writeDataFrameOff
    , readDataFrame, readDataFrameOff
    , withDataFramePtr, isDataFramePinned
    ) where


import           GHC.Base
import           GHC.IO                                 (IO (..))
import           GHC.Ptr                                (Ptr (..))

import           Numeric.DataFrame.Family
import           Numeric.DataFrame.Internal.Array.Class
import           Numeric.DataFrame.Internal.Mutable
import           Numeric.Dimensions
import           Numeric.PrimBytes


-- | Mutable DataFrame that lives in IO.
--   Internal representation is always a MutableByteArray.
data family IODataFrame (t :: Type) (ns :: [k])

-- | Pure wrapper on a mutable byte array
newtype instance IODataFrame t (ns :: [Nat]) = IODataFrame (MDataFrame RealWorld t (ns :: [Nat]))

-- | Data frame with some dimensions missing at compile time.
--   Pattern-match against its constructor to get a Nat-indexed mutable data frame.
data instance IODataFrame t (xs :: [XNat])
  = forall (ns :: [Nat]) . Dimensions ns
  => XIOFrame (IODataFrame t ns)

-- | Mutable DataFrame of unknown dimensionality
data SomeIODataFrame (t :: Type)
  = forall (ns :: [Nat]) . Dimensions ns => SomeIODataFrame (IODataFrame t ns)

-- | Create a new mutable DataFrame.
newDataFrame :: forall t (ns :: [Nat])
              . ( PrimBytes t, Dimensions ns)
             => IO (IODataFrame t ns)
newDataFrame = IODataFrame <$> IO (newDataFrame# @t @ns)
{-# INLINE newDataFrame #-}


-- | Create a new mutable DataFrame.
newPinnedDataFrame :: forall t (ns :: [Nat])
                    . ( PrimBytes t, Dimensions ns)
                   => IO (IODataFrame t ns)
newPinnedDataFrame = IODataFrame <$> IO (newPinnedDataFrame# @t @ns)
{-# INLINE newPinnedDataFrame #-}


-- | Copy one DataFrame into another mutable DataFrame at specified position.
copyDataFrame :: forall (t :: Type) (as :: [Nat]) (b' :: Nat) (b :: Nat)
                                    (bs :: [Nat]) (asbs :: [Nat])
               . ( PrimBytes t
                 , PrimBytes (DataFrame t (as +: b'))
                 , ConcatList as (b :+ bs) asbs
                 , Dimensions (b :+ bs)
                 )
               => DataFrame t (as +: b') -> Idxs (b :+ bs) -> IODataFrame t asbs -> IO ()
copyDataFrame df ei (IODataFrame mdf) = IO (copyDataFrame# df ei mdf)
{-# INLINE copyDataFrame #-}

-- | Copy one mutable DataFrame into another mutable DataFrame at specified position.
copyMutableDataFrame :: forall (t :: Type) (as :: [Nat]) (b' :: Nat) (b :: Nat)
                               (bs :: [Nat]) (asbs :: [Nat])
                      . ( PrimBytes t
                        , ConcatList as (b :+ bs) asbs
                        , Dimensions (b :+ bs)
                        )
                     => IODataFrame t (as +: b') -> Idxs (b :+ bs)
                     -> IODataFrame t asbs -> IO ()
copyMutableDataFrame (IODataFrame mdfA) ei (IODataFrame mdfB)
    = IO (copyMDataFrame# mdfA ei mdfB)
{-# INLINE copyMutableDataFrame #-}


-- | Make a mutable DataFrame immutable, without copying.
unsafeFreezeDataFrame :: forall (t :: Type) (ns :: [Nat])
                       . PrimArray t (DataFrame t ns)
                      => IODataFrame t ns -> IO (DataFrame t ns)
unsafeFreezeDataFrame (IODataFrame mdf) = IO (unsafeFreezeDataFrame# mdf)
{-# INLINE unsafeFreezeDataFrame #-}


-- | Copy content of a mutable DataFrame into a new immutable DataFrame.
freezeDataFrame :: forall (t :: Type) (ns :: [Nat])
                 . PrimArray t (DataFrame t ns)
                => IODataFrame t ns -> IO (DataFrame t ns)
freezeDataFrame (IODataFrame mdf) = IO (freezeDataFrame# mdf)
{-# INLINE freezeDataFrame #-}

-- | Create a new mutable DataFrame and copy content of immutable one in there.
thawDataFrame :: forall (t :: Type) (ns :: [Nat])
               . (PrimBytes (DataFrame t ns), PrimBytes t)
              => DataFrame t ns -> IO (IODataFrame t ns)
thawDataFrame df = IODataFrame <$> IO (thawDataFrame# df)
{-# INLINE thawDataFrame #-}

-- | Create a new mutable DataFrame and copy content of immutable one in there.
--   The result array is pinned and aligned.
thawPinDataFrame :: forall (t :: Type) (ns :: [Nat])
                  . (PrimBytes (DataFrame t ns), PrimBytes t)
                 => DataFrame t ns -> IO (IODataFrame t ns)
thawPinDataFrame df = IODataFrame <$> IO (thawPinDataFrame# df)
{-# INLINE thawPinDataFrame #-}

-- | UnsafeCoerces an underlying byte array.
unsafeThawDataFrame :: forall (t :: Type) (ns :: [Nat])
                     . (PrimBytes (DataFrame t ns), PrimBytes t)
                    => DataFrame t ns -> IO (IODataFrame t ns)
unsafeThawDataFrame df = IODataFrame <$> IO (unsafeThawDataFrame# df)
{-# INLINE unsafeThawDataFrame #-}


-- | Write a single element at the specified index
writeDataFrame :: forall t (ns :: [Nat])
                . ( PrimBytes t, Dimensions ns )
               => IODataFrame t ns -> Idxs ns -> DataFrame t ('[] :: [Nat]) -> IO ()
writeDataFrame (IODataFrame mdf) ei = IO . writeDataFrame# mdf ei . unsafeCoerce#
{-# INLINE writeDataFrame #-}


-- | Read a single element at the specified index
readDataFrame :: forall (t :: Type) (ns :: [Nat])
               . ( PrimBytes t, Dimensions ns )
              => IODataFrame t ns -> Idxs ns -> IO (DataFrame t ('[] :: [Nat]))
readDataFrame (IODataFrame mdf) = unsafeCoerce# . IO . readDataFrame# mdf
{-# INLINE readDataFrame #-}


-- | Write a single element at the specified element offset
writeDataFrameOff :: forall (t :: Type) (ns :: [Nat])
                   . PrimBytes t
               => IODataFrame t ns -> Int -> DataFrame t ('[] :: [Nat])  -> IO ()
writeDataFrameOff (IODataFrame mdf) (I# i)
  = IO . writeDataFrameOff# mdf i . unsafeCoerce#
{-# INLINE writeDataFrameOff #-}


-- | Read a single element at the specified element offset
readDataFrameOff :: forall (t :: Type) (ns :: [Nat])
                  . PrimBytes t
               => IODataFrame t ns -> Int -> IO (DataFrame t ('[] :: [Nat]))
readDataFrameOff (IODataFrame mdf) (I# i)
  = unsafeCoerce# (IO (readDataFrameOff# mdf i))
{-# INLINE readDataFrameOff #-}


-- | Allow arbitrary IO operations on a pointer to the beginning of the data
--   keeping the data from garbage collecting until the arg function returns.
--
--   Warning: do not let @Ptr t@ leave the scope of the arg function,
--            the data may be garbage-collected by then.
--
--   Warning: use this function on a pinned DataFrame only;
--            otherwise, the data may be relocated before the arg fun finishes.
withDataFramePtr :: forall (t :: Type) (ns :: [k]) (r :: Type)
                  . (PrimBytes t, KnownDimKind k)
                 => IODataFrame t ns
                 -> ( Ptr t -> IO r )
                 -> IO r
withDataFramePtr df k = case dimKind @k of
    DimNat -> case df of
      IODataFrame x
        -> IO $ withDataFramePtr# x (unsafeCoerce# k)
    DimXNat -> case df of
      XIOFrame (IODataFrame x)
        -> IO $ withDataFramePtr# x (unsafeCoerce# k)


-- | Check if the byte array wrapped by this DataFrame is pinned,
--   which means cannot be relocated by GC.
isDataFramePinned :: forall (t :: Type) (ns :: [k])
                   . KnownDimKind k
                  => IODataFrame t ns -> Bool
isDataFramePinned df = case dimKind @k of
    DimNat -> case df of
      IODataFrame x -> isDataFramePinned# x
    DimXNat -> case df of
      XIOFrame (IODataFrame x) -> isDataFramePinned# x