{-# OPTIONS -fno-warn-orphans #-}

module Utils.Csv
  ( typeCsv, typeCsv', extensionCsv
  , typeXlsx, extensionXlsx
  , pathPieceCsv
  , (.:??)
  , lsfHeaderTranslate
  , CsvRendered(..)
  , toCsvRendered
  , toDefaultOrderedCsvRendered
  , csvRenderedToXlsx, Xlsx, Xlsx.fromXlsx
  , CsvSemicolonList(..)
  ) where

import ClassyPrelude hiding (lookup)
import Settings.Mime

import Data.Csv hiding (Name)
import Data.Csv.Conduit (CsvParseError)

import Language.Haskell.TH (Name)
import Language.Haskell.TH.Lib

import Yesod.Core.Content

import qualified Data.Map as Map
import qualified Data.HashMap.Strict as HashMap

import Codec.Xlsx (Xlsx)
import qualified Codec.Xlsx as Xlsx

import Data.Monoid (Endo(..))

import Control.Lens

import Data.Default

import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.CaseInsensitive as CI

import qualified Data.Binary.Builder as Builder
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Attoparsec.ByteString as Attoparsec

import qualified Data.Csv.Parser as Csv
import qualified Data.Csv.Builder as Csv

import qualified Data.Vector as Vector

import Data.Char (ord)

import Control.Monad.Fail


deriving instance Typeable CsvParseError
instance Exception CsvParseError


typeCsv, typeCsv' :: ContentType
typeCsv = simpleContentType typeCsv'
typeCsv' = "text/csv; header=present"

typeXlsx :: ContentType
typeXlsx = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"

extensionCsv, extensionXlsx :: Extension
extensionCsv = fromMaybe "csv" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeCsv ]
extensionXlsx = fromMaybe "xlsx" $ listToMaybe [ ext | (ext, mime) <- Map.toList mimeMap, mime == typeXlsx ]


pathPieceCsv :: Name -> DecsQ
pathPieceCsv (conT -> t) =
  [d|
    instance ToField $(t) where
      toField = toField . toPathPiece
    instance FromField $(t) where
      parseField = maybe (fail "Could not unmarshal from PathPiece") return . fromPathPiece <=< parseField
  |]


(.:??) :: FromField (Maybe a) => NamedRecord -> ByteString -> Parser (Maybe a)
m .:?? name = lookup m name <|> return Nothing


lsfHeaderTranslate :: NamedRecord -> NamedRecord
lsfHeaderTranslate = HashMap.fromList . over (traverse . _1) lsfHeaderTranslate' . HashMap.toList
  where
    lsfHeaderTranslate' k = case CI.mk . Text.strip <$> Text.decodeUtf8' k of
      Right k'
        | k' == "mtknr"     -> "matriculation"
        | k' == "nachname"  -> "surname"
        | k' == "vorname"   -> "first-name"
        | k' == "bewertung" -> "exam-result"
      _other -> k


data CsvRendered = CsvRendered
  { csvRenderedHeader :: Header
  , csvRenderedData :: [NamedRecord]
  } deriving (Eq, Read, Show, Generic, Typeable)

toCsvRendered :: forall mono.
                 ( ToNamedRecord (Element mono)
                 , MonoFoldable mono
                 )
              => Header
              -> mono -> CsvRendered
toCsvRendered csvRenderedHeader (otoList -> csvs) = CsvRendered{..}
  where
    csvRenderedData   = map toNamedRecord csvs

toDefaultOrderedCsvRendered :: forall mono.
                               ( ToNamedRecord (Element mono)
                               , DefaultOrdered (Element mono)
                               , MonoFoldable mono
                               )
                            => mono -> CsvRendered
toDefaultOrderedCsvRendered = toCsvRendered $ headerOrder (error "headerOrder" :: Element mono)


csvRenderedToXlsx :: Text -- ^ Name of worksheet
                  -> CsvRendered -> Xlsx
csvRenderedToXlsx sheetName CsvRendered{..} = def & Xlsx.atSheet sheetName ?~ (def & appEndo (addHeader <> addValues))
  where
    addHeader = flip foldMap (zip [1..] $ toList csvRenderedHeader) $ \(c, bs) -> Endo $ Xlsx.cellValueAtRC (1, c) ?~ Xlsx.CellText (decodeUtf8 bs)
    addValues = flip foldMap (zip [2..] csvRenderedData) $ \(r, nr) -> flip foldMap (zip [1..] $ toList csvRenderedHeader) $ \(c, hBS) -> case HashMap.lookup hBS nr of
      Nothing -> mempty
      Just vBS -> Endo $ Xlsx.cellValueAtRC (r, c) ?~ Xlsx.CellText (decodeUtf8 vBS)


newtype CsvSemicolonList a = CsvSemicolonList { unCsvSemicolonList :: [a] }
  deriving stock (Read, Show, Generic, Typeable)
  deriving newtype (Eq, Ord)

instance ToField a => ToField (CsvSemicolonList a) where
  toField (CsvSemicolonList xs) = dropEnd 2 . LBS.toStrict . Builder.toLazyByteString $ Csv.encodeRecordWith encOpts fs
    where
      fs = map toField xs
      encOpts = defaultEncodeOptions
        { encDelimiter = fromIntegral $ ord ';'
        , encQuoting = case fs of
            [fStr] | null fStr -> QuoteAll
            _other             -> QuoteMinimal
        , encUseCrLf = True
        }

instance FromField a => FromField (CsvSemicolonList a) where
  parseField f
    | null f = pure $ CsvSemicolonList []
    | otherwise = fmap CsvSemicolonList . mapM parseField . Vector.toList <=< either fail return $ Attoparsec.parseOnly (Csv.record sep) f
    where
      sep = fromIntegral $ ord ';'
