module Drasil.Data.Formats.CSV.Core
  ( -- * CSVs
    CSV,
    ColumnCount,
    RowCount,
    header,
    rows,
    columnCount,
    rowCount,

    -- ** Constructors
    mkCSV,
  )
where

import Data.List (find)
import Data.Text (Text)
import Numeric.Natural (Natural)

-- | The number of columns a CSV has.
type ColumnCount = Natural

-- | The number of rows a CSV has (excluding its reader).
type RowCount = Natural

-- | A CSV file representation containing an optional header and a list of rows.
--
-- Caches the column and row counts for future potential reference.
data CSV = CSV
  { CSV -> Maybe [Text]
_header :: Maybe [Text],
    CSV -> [[Text]]
_rows :: [[Text]],
    CSV -> Natural
_columnCount :: ColumnCount,
    CSV -> Natural
_rowCount :: RowCount
  }
  deriving (Int -> CSV -> ShowS
[CSV] -> ShowS
CSV -> String
(Int -> CSV -> ShowS)
-> (CSV -> String) -> ([CSV] -> ShowS) -> Show CSV
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CSV -> ShowS
showsPrec :: Int -> CSV -> ShowS
$cshow :: CSV -> String
show :: CSV -> String
$cshowList :: [CSV] -> ShowS
showList :: [CSV] -> ShowS
Show, CSV -> CSV -> Bool
(CSV -> CSV -> Bool) -> (CSV -> CSV -> Bool) -> Eq CSV
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CSV -> CSV -> Bool
== :: CSV -> CSV -> Bool
$c/= :: CSV -> CSV -> Bool
/= :: CSV -> CSV -> Bool
Eq)

-- | Get the header row of a 'CSV'.
header :: CSV -> Maybe [Text]
header :: CSV -> Maybe [Text]
header = CSV -> Maybe [Text]
_header
{-# INLINE header #-}

-- | Get all rows of a 'CSV' (excludes header).
rows :: CSV -> [[Text]]
rows :: CSV -> [[Text]]
rows = CSV -> [[Text]]
_rows
{-# INLINE rows #-}

-- | Get the number of columns in a 'CSV'.
columnCount :: CSV -> ColumnCount
columnCount :: CSV -> Natural
columnCount = CSV -> Natural
_columnCount
{-# INLINE columnCount #-}

-- | Get the number of rows in a 'CSV' (excludes header).
rowCount :: CSV -> RowCount
rowCount :: CSV -> Natural
rowCount = CSV -> Natural
_rowCount
{-# INLINE rowCount #-}

-- | Create a 'CSV'. Expects all rows and the header to have the same length. If
-- the expected column count is not provided (the first parameter), then the
-- number of columns in the header is used as the expected column count. If the
-- header does not exist, the length of the first row is used. If the data is
-- also empty, you will have an empty CSV with no columns and no rows.
mkCSV :: Maybe ColumnCount -> Maybe [Text] -> [[Text]] -> Either String CSV
mkCSV :: Maybe Natural -> Maybe [Text] -> [[Text]] -> Either String CSV
mkCSV Maybe Natural
mcols Maybe [Text]
mhr [[Text]]
rs = Either String CSV
-> (String -> Either String CSV)
-> Maybe String
-> Either String CSV
forall b a. b -> (a -> b) -> Maybe a -> b
maybe (CSV -> Either String CSV
forall a b. b -> Either a b
Right (CSV -> Either String CSV) -> CSV -> Either String CSV
forall a b. (a -> b) -> a -> b
$ Maybe [Text] -> [[Text]] -> Natural -> Natural -> CSV
CSV Maybe [Text]
mhr [[Text]]
rs Natural
cc ([[Text]] -> Natural
forall n a. Integral n => [a] -> n
len [[Text]]
rs)) String -> Either String CSV
forall a b. a -> Either a b
Left ((Natural, String) -> Maybe [Text] -> [[Text]] -> Maybe String
saneLengths (Natural
cc, String
src) Maybe [Text]
mhr [[Text]]
rs)
  where
    (Natural
cc, String
src) = Maybe Natural -> Maybe [Text] -> [[Text]] -> (Natural, String)
expectedColumnCount Maybe Natural
mcols Maybe [Text]
mhr [[Text]]
rs

-- | Internal: Check if the header and all rows have the same length as the
-- expected number of columns. Returns 'Nothing' if the CSV is valid, or 'Just'
-- the error message otherwise.
saneLengths :: (Natural, String) -> Maybe [Text] -> [[Text]] -> Maybe String
saneLengths :: (Natural, String) -> Maybe [Text] -> [[Text]] -> Maybe String
saneLengths (Natural
expLen, String
expLenSrc) Maybe [Text]
mhr [[Text]]
rs =
  case Maybe [Text]
mhr of
    Just [Text]
hdr | let l :: Natural
l = [Text] -> Natural
forall n a. Integral n => [a] -> n
len [Text]
hdr, Natural
l Natural -> Natural -> Bool
forall a. Eq a => a -> a -> Bool
/= Natural
expLen -> String -> Maybe String
forall a. a -> Maybe a
Just (String -> Maybe String) -> String -> Maybe String
forall a b. (a -> b) -> a -> b
$ String -> Natural -> String
forall {a}. Show a => String -> a -> String
formatErr String
"Header" Natural
l
    Maybe [Text]
_ -> (Natural, Natural) -> String
forall {a} {a}. (Show a, Show a) => (a, a) -> String
format ((Natural, Natural) -> String)
-> Maybe (Natural, Natural) -> Maybe String
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ((Natural, Natural) -> Bool)
-> [(Natural, Natural)] -> Maybe (Natural, Natural)
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find ((Natural -> Natural -> Bool
forall a. Eq a => a -> a -> Bool
/= Natural
expLen) (Natural -> Bool)
-> ((Natural, Natural) -> Natural) -> (Natural, Natural) -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Natural, Natural) -> Natural
forall a b. (a, b) -> b
snd) ([Natural] -> [Natural] -> [(Natural, Natural)]
forall a b. [a] -> [b] -> [(a, b)]
zip [Natural
1 :: Natural ..] (([Text] -> Natural) -> [[Text]] -> [Natural]
forall a b. (a -> b) -> [a] -> [b]
map [Text] -> Natural
forall n a. Integral n => [a] -> n
len [[Text]]
rs))
  where
    formatErr :: String -> a -> String
formatErr String
target a
actualLen = [String] -> String
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat [
        String
target, String
" has ", a -> String
forall a. Show a => a -> String
show a
actualLen, String
" columns, but expected ",
        Natural -> String
forall a. Show a => a -> String
show Natural
expLen, String
" (based on ", String
expLenSrc, String
")"
      ]
    format :: (a, a) -> String
format (a
i, a
l) = String -> a -> String
forall {a}. Show a => String -> a -> String
formatErr (String
"Row " String -> ShowS
forall a. [a] -> [a] -> [a]
++ a -> String
forall a. Show a => a -> String
show a
i) a
l

-- | Internal: Find the expected number of columns for a CSV, along with a
-- description of the source of the expectation.
expectedColumnCount :: Maybe ColumnCount -> Maybe [Text] -> [[Text]] -> (Natural, String)
expectedColumnCount :: Maybe Natural -> Maybe [Text] -> [[Text]] -> (Natural, String)
expectedColumnCount (Just Natural
cols) Maybe [Text]
_ [[Text]]
_ = (Natural
cols, String
"expected columns input")
expectedColumnCount Maybe Natural
_ (Just [Text]
header') [[Text]]
_ = ([Text] -> Natural
forall n a. Integral n => [a] -> n
len [Text]
header', String
"header length")
expectedColumnCount Maybe Natural
_ Maybe [Text]
_ ([Text]
fr : [[Text]]
_) = ([Text] -> Natural
forall n a. Integral n => [a] -> n
len [Text]
fr, String
"first row length")
expectedColumnCount Maybe Natural
_ Maybe [Text]
_ [[Text]]
_ = (Natural
0, String
"empty data")

-- | Internal: Output polymorphic 'length'.
len :: (Integral n) => [a] -> n
len :: forall n a. Integral n => [a] -> n
len = Int -> n
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Int -> n) -> ([a] -> Int) -> [a] -> n
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [a] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length
{-# INLINE len #-}