-- |
-- Module      : Foundation.Format.CSV
-- License     : BSD-style
-- Maintainer  : Foundation
-- Stability   : experimental
-- Portability : portable
--
-- Provies the support for Comma Separated Value

{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE FlexibleInstances          #-}

module Foundation.Format.CSV
    (-- * CSV
      CSV

    -- ** Builder
    -- ** String Bulider
    , csvStringBuilder
    , rowStringBuilder
    , fieldStringBuilder
    -- ** Block Builder
    , csvBlockBuilder
    , rowBlockBuilder
    , fieldBlockBuilder
    -- ** Conduit
    , rowC
    -- * Row
    , Row
    , ToRow(..)
    -- * Field
    , Field(..)
    , Escaping(..)
    , ToField(..)
    -- ** helpers
    , integral
    , float
    , string
    ) where

import           Basement.Imports -- hiding (throw)
import           Basement.BoxedArray              (Array)
import           Basement.NormalForm              (NormalForm(..))
import           Basement.From                    (Into, into)
import           Basement.String                  (String, replace, any, elem)
import qualified Basement.String        as String (singleton)
import           Basement.Types.Word128           (Word128)
import           Basement.Types.Word256           (Word256)
import           Basement.Types.OffsetSize        (Offset, CountOf)
import           Foundation.Collection.Element    (Element)
import           Foundation.Collection.Collection (Collection, nonEmpty_)
import           Foundation.Collection.Sequential (Sequential(intersperse))
import           Foundation.Collection.Indexed    (IndexedCollection)
import           Foundation.Check.Arbitrary       (Arbitrary(..), frequency)
import           Foundation.Conduit.Internal

import qualified Foundation.String.Builder as String
import           Basement.Block              (Block)
import qualified Basement.Block.Builder    as Block

import           GHC.ST (runST)

-- | CSV field
data Field
    = FieldInteger Integer
    | FieldDouble  Double
    | FieldString  String  Escaping
  deriving (Eq, Show, Typeable)
instance NormalForm Field where
    toNormalForm (FieldInteger i) = toNormalForm i
    toNormalForm (FieldDouble  d) = toNormalForm d
    toNormalForm (FieldString  s e) = toNormalForm s `seq` toNormalForm e
instance Arbitrary Field where
    arbitrary = frequency $ nonEmpty_ [ (1, FieldInteger <$> arbitrary)
                                      , (1, FieldDouble <$> arbitrary)
                                      , (3, string <$> arbitrary)
                                      ]

data Escaping = NoEscape | Escape | DoubleEscape
  deriving (Eq, Ord, Enum, Bounded, Show, Typeable)
instance NormalForm Escaping where
    toNormalForm !_ = ()

class ToField a where
    toField :: a -> Field
instance ToField Field where
    toField = id
instance ToField a => ToField (Maybe a) where
    toField Nothing  = FieldString mempty NoEscape
    toField (Just a) = toField a

instance ToField Int8 where
    toField = FieldInteger . into
instance ToField Int16 where
    toField = FieldInteger . into
instance ToField Int32 where
    toField = FieldInteger . into
instance ToField Int64 where
    toField = FieldInteger . into
instance ToField Int where
    toField = FieldInteger . into

instance ToField Word8 where
    toField = FieldInteger . into
instance ToField Word16 where
    toField = FieldInteger . into
instance ToField Word32 where
    toField = FieldInteger . into
instance ToField Word64 where
    toField = FieldInteger . into
instance ToField Word where
    toField = FieldInteger . into
instance ToField Word128 where
    toField = FieldInteger . into
instance ToField Word256 where
    toField = FieldInteger . into

instance ToField Integer where
    toField = FieldInteger
instance ToField Natural where
    toField = FieldInteger . into

instance ToField Double where
    toField = FieldDouble

instance ToField Char where
    toField = string . String.singleton

instance ToField (Offset a) where
    toField = FieldInteger . into
instance ToField (CountOf a) where
    toField = FieldInteger . into

instance ToField [Char] where
    toField = string . fromString
instance ToField String where
    toField = string

-- | helper function to create a `FieldInteger`
--
integral :: Into Integer a => a -> Field
integral = FieldInteger . into

float :: Double -> Field
float = FieldDouble

-- | heler function to create a FieldString.
--
-- This function will findout automatically if an escaping is needed.
-- if you wish to perform the escaping manually, do not used this function
--
string :: String -> Field
string s = FieldString s encoding
  where
    encoding
        | any g s   = DoubleEscape
        | any f s   = Escape
        | otherwise = NoEscape
    f c = c == '\"'
    g c = c `elem` ",\r\n"

-- | CSV Row
--
newtype Row = Row { unRow :: Array Field }
  deriving (Eq, Show, Typeable, Semigroup, Monoid, Collection, NormalForm, Sequential, IndexedCollection)

type instance Element Row = Field
instance IsList Row where
    type Item Row = Field
    toList = toList . unRow
    fromList = Row . fromList

class ToRow a where
    toRow :: a -> Row
instance ToRow Row where
    toRow = id
instance (ToField a, ToField b) => ToRow (a,b) where
    toRow (a,b) = fromList [toField a, toField b]
instance (ToField a, ToField b, ToField c) => ToRow (a,b,c) where
    toRow (a,b,c) = fromList [toField a, toField b, toField c]
instance (ToField a, ToField b, ToField c, ToField d) => ToRow (a,b,c,d) where
    toRow (a,b,c,d) = fromList [toField a, toField b, toField c, toField d]
instance (ToField a, ToField b, ToField c, ToField d, ToField e) => ToRow (a,b,c,d,e) where
    toRow (a,b,c,d,e) = fromList [toField a, toField b, toField c, toField d, toField e]
instance (ToField a, ToField b, ToField c, ToField d, ToField e, ToField f) => ToRow (a,b,c,d,e,f) where
    toRow (a,b,c,d,e,f) = fromList [toField a, toField b, toField c, toField d, toField e, toField f]

-- | CSV Type
newtype CSV = CSV { unCSV :: Array Row }
  deriving (Eq, Show, Typeable, Semigroup, Monoid, Collection, NormalForm, Sequential, IndexedCollection)

type instance Element CSV = Row

instance IsList CSV where
    type Item CSV = Row
    toList = toList . unCSV
    fromList = CSV . fromList

-- | serialise the CSV document into a UTF8 string
csvStringBuilder :: CSV -> String.Builder
csvStringBuilder = String.unsafeStringBuilder . csvBlockBuilder

rowStringBuilder :: Row -> String.Builder
rowStringBuilder = String.unsafeStringBuilder . rowBlockBuilder

fieldStringBuilder :: Field -> String.Builder
fieldStringBuilder = String.unsafeStringBuilder . fieldBlockBuilder

-- | serialise the CSV document into a UTF8 encoded (Block Word8)
csvBlockBuilder :: CSV -> Block.Builder
csvBlockBuilder = mconcat . intersperse (Block.emitString "\r\n") . fmap rowBlockBuilder . toList . unCSV

rowBlockBuilder :: Row -> Block.Builder
rowBlockBuilder = mconcat . intersperse (Block.emitUTF8Char ',') . fmap fieldBlockBuilder . toList . unRow

fieldBlockBuilder :: Field -> Block.Builder
fieldBlockBuilder (FieldInteger i) = Block.emitString $ show i
fieldBlockBuilder (FieldDouble  d) = Block.emitString $ show d
fieldBlockBuilder (FieldString  s e) = case e of
    NoEscape     -> Block.emitString s
    Escape       -> Block.emitUTF8Char '"' <> Block.emitString s <> Block.emitUTF8Char '"'
    DoubleEscape -> Block.emitUTF8Char '"' <> Block.emitString (replace "\"" "\"\"" s) <> Block.emitUTF8Char '"'

rowC :: (ToRow row, Monad m) => Conduit row (Block Word8) m ()
rowC = await >>= go
  where
    go Nothing  = pure ()
    go (Just r) =
      let bytes = runST (Block.run $ rowBlockBuilder (toRow r) <> Block.emitString "\r\n")
         in yield bytes >> await >>= go