{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  $module
-- Copyright   :  (c) Laurent P. René de Cotret
-- License     :  MIT
-- Maintainer  :  laurent.decotret@outlook.com
-- Portability :  portable
--
-- This module contains functions to serialize/deserialize unboxed 'Series'
-- to/from bytes.
--
-- = Why use unboxed series?
--
-- Unboxed series can have much better performance, at the cost of less flexibility. For example,
-- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of 
-- `Functor` or `Foldable`.
--
-- If you are hesitating, you should prefer the series implementation in the "Data.Series" module (and therefore
-- the IO module "Data.Series.IO").
module Data.Series.Unboxed.IO (
    -- * Deserialize 'Series'
    readCSV,
    readCSVFromFile,

    -- * Serialize 'Series'
    writeCSV,
    writeCSVToFile,
) where


import           Control.Monad.IO.Class ( MonadIO )
import qualified Data.ByteString.Lazy   as BL
import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )
import           Data.Series.Unboxed    ( Series )
import qualified Data.Series.Generic.IO as Generic.IO
import           Data.Vector.Unboxed    ( Unbox )


{-|
Read a comma-separated value (CSV) bytestream into a series.

Consider the following bytestream read from a file:

@
latitude,longitude,city
48.856667,2.352222,Paris
40.712778,-74.006111,New York City
25.0375,121.5625,Taipei
-34.603333,-58.381667,Buenos Aires
@

We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
to do is to create a datatype representing the latitude and longitude information, and our index:

@
import qualified "Data.Vector.Unboxed"         as U
import qualified "Data.Vector.Unboxed.Mutable" as UM
import qualified "Data.Vector.Generic"         as G
import qualified "Data.Vector.Generic.Mutable" as GM

newtype Latitude = MkLatitude Double
    deriving (Show)

-- Special code to ensure that Latitude is unboxed
-- This is only required for unboxed 'Series'
newtype instance UM.MVector s Latitude = MV_Latitude (UM.MVector s Int)
newtype instance U.Vector Latitude = V_Latitude (U.Vector Int)
deriving instance GM.MVector UM.MVector Latitude
deriving instance G.Vector U.Vector Latitude 
instance U.Unbox Latitude

newtype City = MkCity String
    deriving ( Eq, Ord, Show )
@

Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:

@
import "Data.Csv" ( 'FromNamedRecord', '(.:)' )

instance 'FromNamedRecord' Latitude where
    'parseNamedRecord' r = MkLatitude \<$\> r .: "latitude"


instance 'FromNamedRecord' City where
    'parseNamedRecord' r = MkCity \<$\> r .: "city"
@

Finally, we're ready to read our stream:

@
import "Data.Series.Unboxed"
import "Data.Series.Unboxed.IO"

main :: IO ()
main = do
    stream <- (...) -- Read the bytestring from somewhere
    let (latitudes  :: 'Series' City Latitude) = either error id (`readCSV` stream)
    print latitudes
@
-}
readCSV :: (Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)
        => BL.ByteString
        -> Either String (Series k a)
readCSV :: forall k a.
(Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a) =>
ByteString -> Either String (Series k a)
readCSV = ByteString -> Either String (Series Vector k a)
forall (v :: * -> *) a k.
(Vector v a, Ord k, FromNamedRecord k, FromNamedRecord a) =>
ByteString -> Either String (Series v k a)
Generic.IO.readCSV


{-|
This is a helper function to read a CSV directly from a filepath.
See the documentation for 'readCSV' on how to prepare your types.
Then, for example, you can use 'readCSVFromFile' as:

@
import "Data.Series.Unboxed"
import "Data.Series.Unboxed.IO"

main :: IO ()
main = do
    let (latlongs  :: 'Series' City LatLong) = either error id \<$\> `readCSVFromFile` "somefile.csv"
    print latitudes
@
-}
readCSVFromFile :: (MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)
                => FilePath
                -> m (Either String (Series k a))
readCSVFromFile :: forall (m :: * -> *) k a.
(MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a,
 Unbox a) =>
String -> m (Either String (Series k a))
readCSVFromFile = String -> m (Either String (Series Vector k a))
forall (m :: * -> *) (v :: * -> *) a k.
(MonadIO m, Vector v a, Ord k, FromNamedRecord k,
 FromNamedRecord a) =>
String -> m (Either String (Series v k a))
Generic.IO.readCSVFromFile


-- | Serialize a 'Series' to bytes. 
writeCSV :: (ToNamedRecord k, ToNamedRecord a, Unbox a)
         => Series k a
         -> BL.ByteString
writeCSV :: forall k a.
(ToNamedRecord k, ToNamedRecord a, Unbox a) =>
Series k a -> ByteString
writeCSV = Series Vector k a -> ByteString
forall (v :: * -> *) a k.
(Vector v a, ToNamedRecord k, ToNamedRecord a) =>
Series v k a -> ByteString
Generic.IO.writeCSV


-- | This is a helper function to write a 'Series' directly to a file.
writeCSVToFile :: (MonadIO m, ToNamedRecord k, ToNamedRecord a, Unbox a)
               => FilePath
               -> Series k a
               -> m ()
writeCSVToFile :: forall (m :: * -> *) k a.
(MonadIO m, ToNamedRecord k, ToNamedRecord a, Unbox a) =>
String -> Series k a -> m ()
writeCSVToFile = String -> Series Vector k a -> m ()
forall (m :: * -> *) (v :: * -> *) a k.
(MonadIO m, Vector v a, ToNamedRecord k, ToNamedRecord a) =>
String -> Series v k a -> m ()
Generic.IO.writeCSVToFile