{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE KindSignatures        #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}

-- | A @CSV@ empty datatype with `MimeRender` and `MimeUnrender` instances for
-- @cassava@'s encoding and decoding classes.
--
-- >>> type Eg = Get '[CSV' 'HasHeader MyEncodeOptions] [(Int, String)]
--
-- Default encoding and decoding options are also provided, along with the
-- @CSV@ type synonym that uses them.
--
-- >>> type EgDefault = Get '[CSV] [(Int, String)]
--
module Servant.CSV.Cassava ( module Servant.CSV.Cassava
                           , HasHeader(..)
                           ) where

import           Prelude ()
import           Prelude.Compat

import           Data.Csv
import           Data.ByteString.Lazy (ByteString)
import           Data.Proxy           (Proxy (..))
import           Data.Typeable        (Typeable)
import           Data.Vector          (Vector, toList)
import           GHC.Generics         (Generic)
import qualified Network.HTTP.Media   as M
import           Servant.API          (Accept (..), MimeRender (..),
                                      MimeUnrender (..))

data CSV' (hasHeader :: HasHeader) opt deriving (Typeable)
type CSV = CSV' 'HasHeader DefaultOpts

-- | 'HasHeader singleton.
data SHasHeader (hasHeader :: HasHeader) where
    SHasHeader  :: SHasHeader 'HasHeader
    SNoHeader   :: SHasHeader 'NoHeader

-- | Class to provide 'SHasHeader' implicitly.
class SHasHeaderI (hasHeader :: HasHeader) where shasheader :: SHasHeader hasHeader
instance SHasHeaderI 'HasHeader where shasheader = SHasHeader
instance SHasHeaderI 'NoHeader  where shasheader = SNoHeader

shasheaderToBool :: SHasHeader hasHeader -> Bool
shasheaderToBool SHasHeader = True
shasheaderToBool SNoHeader  = False

lowerSHasHeader :: SHasHeader hasHeader -> HasHeader
lowerSHasHeader SHasHeader = HasHeader
lowerSHasHeader SNoHeader  = NoHeader

-- | Default options, instances providing 'defaultDecodeOptions' and 'defaultEncodeOptions', and content type @text/csv;charset=utf-8@
data DefaultOpts deriving (Typeable, Generic)

-- | Options that work for tab delimited data, with content type @text/tab-separated-values;charset=utf-8@
data TabSeparatedOpts deriving (Typeable, Generic)

-- | Content type can be determined to coincide with encode opts.
instance EncodeOpts opt => Accept (CSV' hasHeader opt) where
    contentType _ = csvContentType (Proxy :: Proxy opt)

-- * Encoding

-- ** Instances

-- | Encode with 'encodeByNameWith'. The 'Header' param is used for determining
-- the order of headers and fields.
instance ( ToNamedRecord a, EncodeOpts opt, SHasHeaderI hasHeader
         ) => MimeRender (CSV' hasHeader opt) (Header, [a]) where
    mimeRender _ (hdr, vals) = encodeByNameWith opts hdr vals
      where
        opts = encodeOpts' (Proxy :: Proxy opt) (Proxy :: Proxy hasHeader)

-- | A class to determine how to encode a list of elements
--
-- * 'HasHeader' encode with 'encodeDefaultOrderedByNameWith'
--
-- * 'NoHeader' encode with 'encodeWith'
--
-- Currently, it's not possible to encode without headers using 'encodeDefaultOrderedByNameWith'.
--
class EncodeList (hasHeader :: HasHeader) a where
    encodeList :: Proxy hasHeader -> EncodeOptions -> [a] -> ByteString

-- | 'encodeDefaultOrderedByNameWith'
instance (DefaultOrdered a, ToNamedRecord a) => EncodeList 'HasHeader a where
    encodeList _ opts vals = encodeDefaultOrderedByNameWith opts { encIncludeHeader = True } vals

-- | 'encodeWith'
instance (ToRecord a) => EncodeList 'NoHeader a where
    encodeList _ opts vals = encodeWith opts { encIncludeHeader = False } vals

instance ( EncodeOpts opt, EncodeList hasHeader a
         ) => MimeRender (CSV' hasHeader opt) [a] where
    mimeRender _ = encodeList (Proxy :: Proxy hasHeader) opts
      where
        opts = encodeOpts (Proxy :: Proxy opt)

-- | Encode with 'encodeByNameWith'. The 'Header' param is used for determining
-- the order of headers and fields.
instance ( ToNamedRecord a, EncodeOpts opt, SHasHeaderI hasHeader
         ) => MimeRender (CSV' hasHeader opt) (Header, Vector a) where
    mimeRender _ (hdr, vals) = encodeByNameWith opts hdr (toList vals)
      where
        opts = encodeOpts' (Proxy :: Proxy opt) (Proxy :: Proxy hasHeader)

instance ( EncodeOpts opt, EncodeList hasHeader a
         ) => MimeRender (CSV' hasHeader opt) (Vector a) where
    mimeRender _ = encodeList (Proxy :: Proxy hasHeader) opts . toList
      where
        opts = encodeOpts (Proxy :: Proxy opt)

-- ** Encode/Decode Options

class EncodeOpts opt where
    encodeOpts :: Proxy opt -> EncodeOptions

    decodeOpts :: Proxy opt -> DecodeOptions
    decodeOpts p = DecodeOptions
        { decDelimiter = encDelimiter e
        }
      where
        e = encodeOpts p

    csvContentType :: Proxy opt -> M.MediaType
    csvContentType p = case encDelimiter (encodeOpts p) of
        -- ord '\t' = 9
        9 -> "text" M.// "tab-separated-values" M./: ("charset", "utf-8")
        _ -> "text" M.// "csv" M./: ("charset", "utf-8")

encodeOpts'
    :: forall opt hasHeader.  (EncodeOpts opt, SHasHeaderI hasHeader)
    => Proxy opt -> Proxy hasHeader -> EncodeOptions
encodeOpts' p _ = (encodeOpts p)
    { encIncludeHeader = shasheaderToBool (shasheader :: SHasHeader hasHeader)
    }

instance EncodeOpts DefaultOpts where
    encodeOpts _ = defaultEncodeOptions
    decodeOpts _ = defaultDecodeOptions

instance EncodeOpts TabSeparatedOpts where
    -- ord '\t' = 9
    encodeOpts _ = defaultEncodeOptions { encDelimiter = 9 }
    decodeOpts _ = defaultDecodeOptions { decDelimiter = 9 }


-- * Decoding

-- ** Instances

-- | Decode with 'decodeByNameWith'.
instance ( FromNamedRecord a, EncodeOpts opt
         ) => MimeUnrender (CSV' 'HasHeader opt) (Header, [a]) where
    mimeUnrender _ bs = fmap toList <$> decodeByNameWith (decodeOpts p) bs
      where p = Proxy :: Proxy opt

-- | Decode with 'decodeWith'.
instance ( FromRecord a, EncodeOpts opt, SHasHeaderI hasHeader
         ) => MimeUnrender (CSV' hasHeader opt) [a] where
    mimeUnrender _  = fmap toList . decodeWith (decodeOpts p) (lowerSHasHeader sh)
      where
        p = Proxy :: Proxy opt
        sh = shasheader :: SHasHeader hasHeader

instance ( FromNamedRecord a, EncodeOpts opt
         ) => MimeUnrender (CSV' 'HasHeader opt) (Header, Vector a) where
    mimeUnrender _ = decodeByNameWith (decodeOpts p)
      where p = Proxy :: Proxy opt

-- | Decode with 'decodeWith'.
instance ( FromRecord a, EncodeOpts opt, SHasHeaderI hasHeader
         ) => MimeUnrender (CSV' hasHeader opt) (Vector a) where
    mimeUnrender _ = decodeWith (decodeOpts p) (lowerSHasHeader sh)
      where
        p = Proxy :: Proxy opt
        sh = shasheader :: SHasHeader hasHeader