-- |
-- Module      : Foundation.Format.CSV.Types
-- License     : BSD-style
-- Maintainer  : Foundation
-- Stability   : experimental
-- Portability : portable
--

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

module Foundation.Format.CSV.Types
    (-- * CSV
      CSV
    , unCSV

    -- * Row
    , Row
    , unRow
    , Record(..)
    -- * Field
    , Field(..)
    , Escaping(..)
    , IsField(..)
    -- ** helpers
    , integral
    , float
    , string
    ) where

import           Basement.Imports
import           Basement.BoxedArray              (length, unsafeIndex)
import           Basement.NormalForm              (NormalForm(..))
import           Basement.From                    (Into, into)
import           Basement.String                  (any, elem, null, uncons)
import qualified Basement.String       as String (singleton)
import           Basement.Types.Word128           (Word128)
import           Basement.Types.Word256           (Word256)
import           Foundation.Collection.Element    (Element)
import           Foundation.Collection.Collection (Collection, nonEmpty_)
import           Foundation.Collection.Sequential (Sequential)
import           Foundation.Collection.Indexed    (IndexedCollection)
import           Foundation.Check.Arbitrary       (Arbitrary(..), frequency)
import           Foundation.String.Read (readDouble, readInteger)

-- | 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 IsField a where
    toField :: a -> Field
    fromField :: Field -> Either String a
instance IsField Field where
    toField = id
    fromField = pure
instance IsField a => IsField (Maybe a) where
    toField Nothing  = FieldString mempty NoEscape
    toField (Just a) = toField a
    fromField stuff@(FieldString p NoEscape)
        | null p = pure Nothing
        | otherwise = Just <$> fromField stuff
    fromField stuff = Just <$> fromField stuff

fromIntegralField :: Integral b => Field -> Either String b
fromIntegralField (FieldString str NoEscape) = case readInteger str of
    Nothing -> Left "Invalid integral field"
    Just v  -> pure $ fromInteger v
fromIntegralField (FieldInteger v) = pure (fromInteger v)
fromIntegralField _ = Left "Expected integral value"

fromDoubleField :: Field -> Either String Double
fromDoubleField (FieldString str NoEscape) = case readDouble str of
    Nothing -> Left "Invalid double field"
    Just v  -> pure v
fromDoubleField (FieldDouble v) = pure v
fromDoubleField _ = Left "Expected double value"

instance IsField Bool where
    toField = toField . show
    fromField (FieldString "True" NoEscape) = pure True
    fromField (FieldString "False" NoEscape) = pure False
    fromField _ = Left "not a boolean value"
instance IsField Int8 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Int16 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Int32 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Int64 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Int where
    toField = FieldInteger . into
    fromField = fromIntegralField

instance IsField Word8 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word16 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word32 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word64 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word128 where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField Word256 where
    toField = FieldInteger . into
    fromField = fromIntegralField

instance IsField Integer where
    toField = FieldInteger
    fromField = fromIntegralField
instance IsField Natural where
    toField = FieldInteger . into
    fromField = fromIntegralField

instance IsField Double where
    toField = FieldDouble
    fromField = fromDoubleField

instance IsField Char where
    toField = string . String.singleton
    fromField (FieldString str _) = case uncons str of
        Just (c, str') | null str' -> pure c
                       | otherwise -> Left "Expected a char, but received a String"
        Nothing -> Left "Expected a char"
    fromField _ = Left "Expected a char"

instance IsField (Offset a) where
    toField = FieldInteger . into
    fromField = fromIntegralField
instance IsField (CountOf a) where
    toField = FieldInteger . into
    fromField = fromIntegralField

instance IsField [Char] where
    toField = string . fromString
    fromField (FieldString str _) = pure $ toList str
    fromField _ = Left "Expected a Lazy String"
instance IsField String where
    toField = string
    fromField (FieldString str _) = pure str
    fromField _ = Left "Expected a UTF8 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
    g c = c == '\"'
    f 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 Record a where
    toRow :: a -> Row
    fromRow :: Row -> Either String a
instance Record Row where
    toRow = id
    fromRow = pure
instance (IsField a, IsField b) => Record (a,b) where
    toRow (a,b) = fromList [toField a, toField b]
    fromRow (Row row)
        | length row == 2 = (,) <$> fromField (row `unsafeIndex` 0) <*> fromField (row `unsafeIndex` 1)
        | otherwise       = Left (show row)
instance (IsField a, IsField b, IsField c) => Record (a,b,c) where
    toRow (a,b,c) = fromList [toField a, toField b, toField c]
    fromRow (Row row)
        | length row == 3 = (,,) <$> fromField (row `unsafeIndex` 0)
                                 <*> fromField (row `unsafeIndex` 1)
                                 <*> fromField (row `unsafeIndex` 2)
        | otherwise       = Left (show row)
instance (IsField a, IsField b, IsField c, IsField d) => Record (a,b,c,d) where
    toRow (a,b,c,d) = fromList [toField a, toField b, toField c, toField d]
    fromRow (Row row)
        | length row == 4 = (,,,) <$> fromField (row `unsafeIndex` 0)
                                  <*> fromField (row `unsafeIndex` 1)
                                  <*> fromField (row `unsafeIndex` 2)
                                  <*> fromField (row `unsafeIndex` 3)
        | otherwise       = Left (show row)
instance (IsField a, IsField b, IsField c, IsField d, IsField e) => Record (a,b,c,d,e) where
    toRow (a,b,c,d,e) = fromList [toField a, toField b, toField c, toField d, toField e]
    fromRow (Row row)
        | length row == 5 = (,,,,) <$> fromField (row `unsafeIndex` 0)
                                   <*> fromField (row `unsafeIndex` 1)
                                   <*> fromField (row `unsafeIndex` 2)
                                   <*> fromField (row `unsafeIndex` 3)
                                   <*> fromField (row `unsafeIndex` 4)
        | otherwise       = Left (show row)
instance (IsField a, IsField b, IsField c, IsField d, IsField e, IsField f) => Record (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]
    fromRow (Row row)
        | length row == 6 = (,,,,,) <$> fromField (row `unsafeIndex` 0)
                                    <*> fromField (row `unsafeIndex` 1)
                                    <*> fromField (row `unsafeIndex` 2)
                                    <*> fromField (row `unsafeIndex` 3)
                                    <*> fromField (row `unsafeIndex` 4)
                                    <*> fromField (row `unsafeIndex` 5)
        | otherwise       = Left (show row)

-- | 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
