module Data.Sv.Encode (
  Encode (..)
, mkEncodeBS
, mkEncodeWithOpts
, unsafeBuilder
, encode
, encodeToHandle
, encodeToFile
, encodeBuilder
, encodeRow
, encodeRowBuilder
, encodeSv
, module Data.Sv.Encode.Options
, const
, show
, nop
, empty
, orEmpty
, char
, int
, integer
, float
, double
, boolTrueFalse
, booltruefalse
, boolyesno
, boolYesNo
, boolYN
, bool10
, string
, text
, byteString
, lazyByteString
, row
, (?>)
, (<?)
, (?>>)
, (<<?)
, encodeOf
, encodeOfMay
, unsafeString
, unsafeText
, unsafeByteString
, unsafeLazyByteString
, unsafeByteStringBuilder
, unsafeConst
) where
import qualified Prelude as P
import Prelude hiding (const, show)
import Control.Applicative ((<$>), (<**>))
import Control.Lens (Getting, preview, review, view)
import Control.Monad (join)
import qualified Data.Bool as B (bool)
import qualified Data.ByteString as Strict
import qualified Data.ByteString.Builder as BS
import qualified Data.ByteString.Lazy as LBS
import Data.Foldable (fold, foldMap, toList)
import Data.Functor.Contravariant (Contravariant (contramap))
import Data.Functor.Contravariant.Divisible (Divisible (conquer), Decidable (choose))
import Data.List.NonEmpty (NonEmpty, nonEmpty)
import Data.Monoid (Monoid (mempty), First, (<>), mconcat)
import Data.Sequence (Seq, ViewL (EmptyL, (:<)), viewl, (<|))
import qualified Data.Sequence as Seq
import qualified Data.Sequence as S (singleton, empty)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import System.IO (BufferMode (BlockBuffering), Handle, hClose, hSetBinaryMode, hSetBuffering, openFile, IOMode (WriteMode))
import Data.Sv.Encode.Options (EncodeOptions (..), HasEncodeOptions (..), HasSeparator (..), defaultEncodeOptions)
import Data.Sv.Encode.Type (Encode (Encode, getEncode))
import Data.Sv.Syntax.Field (Field (Unquoted), SpacedField, unescapedField)
import Data.Sv.Syntax.Record (Record (Record), Records (EmptyRecords), emptyRecord, mkRecords, recordNel)
import Data.Sv.Syntax.Sv (Sv (Sv), Header (Header))
import qualified Data.Vector.NonEmpty as V
import Text.Escape (Escaper, Escaper', Unescaped (Unescaped), escapeChar, escapeString, escapeText, escapeUtf8, escapeUtf8Lazy)
import Text.Newline (newlineToString)
import Text.Space (Spaced (Spaced), spacesString)
import Text.Quote (quoteChar)
mkEncodeBS :: (a -> LBS.ByteString) -> Encode a
mkEncodeBS = unsafeBuilder . fmap BS.lazyByteString
mkEncodeWithOpts :: (EncodeOptions -> a -> BS.Builder) -> Encode a
mkEncodeWithOpts = Encode . fmap (fmap pure)
unsafeBuilder :: (a -> BS.Builder) -> Encode a
unsafeBuilder b = Encode (\_ a -> pure (b a))
encode :: Encode a -> EncodeOptions -> [a] -> LBS.ByteString
encode enc opts = BS.toLazyByteString . encodeBuilder enc opts
encodeToHandle :: Encode a -> EncodeOptions -> [a] -> Handle -> IO ()
encodeToHandle enc opts as h =
  BS.hPutBuilder h (encodeBuilder enc opts as)
encodeToFile :: Encode a -> EncodeOptions -> [a] -> FilePath -> IO ()
encodeToFile enc opts as fp = do
  h <- openFile fp WriteMode
  hSetBuffering h (BlockBuffering Nothing)
  hSetBinaryMode h True
  encodeToHandle enc opts as h
  hClose h
encodeBuilder :: Encode a -> EncodeOptions -> [a] -> BS.Builder
encodeBuilder e opts as =
  let enc = encodeRowBuilder e opts
      nl  = newlineToString (_newline opts)
      terminal = if _terminalNewline opts then nl else mempty
  in  case as of
    [] -> terminal
    (a:as') -> enc a <> mconcat [nl <> enc a' | a' <- as'] <> terminal
encodeRow :: Encode a -> EncodeOptions -> a -> LBS.ByteString
encodeRow e opts = BS.toLazyByteString . encodeRowBuilder e opts
encodeRowBuilder :: Encode a -> EncodeOptions -> a -> BS.Builder
encodeRowBuilder e opts =
  let addSeparators = intersperseSeq (BS.charUtf8 (view separator opts))
      quotep = foldMap (BS.charUtf8 . review quoteChar) (view quote opts)
      addQuotes x = quotep <> x <> quotep
      mkSpaces optic = BS.stringUtf8 . review spacesString . view optic $ opts
      bspaces = mkSpaces spacingBefore
      aspaces = mkSpaces spacingAfter
      addSpaces x = bspaces <> x <> aspaces
  in  fold . addSeparators . fmap (addSpaces . addQuotes) . getEncode e opts
encodeSv :: Encode a -> EncodeOptions -> Maybe (NonEmpty Strict.ByteString) -> [a] -> Sv Strict.ByteString
encodeSv e opts headerStrings as =
  let encoded :: [Seq BS.Builder]
      encoded = getEncode e opts <$> as
      nl = view newline opts
      sep = view separator opts
      mkSpaced = Spaced (_spacingBefore opts) (_spacingAfter opts)
      mkField = maybe Unquoted unescapedField (_quote opts)
      mkHeader r = Header r nl
      mkRecord :: NonEmpty z -> Record z
      mkRecord = recordNel . fmap (mkSpaced . mkField)
      header :: Maybe (Header Strict.ByteString)
      header = mkHeader . mkRecord <$> headerStrings
      rs :: Records Strict.ByteString
      rs = l2rs (b2r <$> encoded)
      l2rs = maybe EmptyRecords (mkRecords nl) . nonEmpty 
      terminal = if _terminalNewline opts then [nl] else []
      b2f :: BS.Builder -> SpacedField Strict.ByteString
      b2f = mkSpaced . mkField . LBS.toStrict . BS.toLazyByteString
      b2r :: Seq BS.Builder -> Record Strict.ByteString
      b2r = maybe emptyRecord (Record . V.fromNel) . nonEmpty . toList . fmap b2f
  in  Sv sep header rs terminal
const :: Strict.ByteString -> Encode a
const b = contramap (pure b) byteString
show :: Show a => Encode a
show = contramap P.show string
nop :: Encode a
nop = conquer
empty :: Encode a
empty = Encode (pure (pure (pure mempty)))
orEmpty :: Encode a -> Encode (Maybe a)
orEmpty = choose (maybe (Left ()) Right) empty
(?>) :: Encode a -> Encode () -> Encode (Maybe a)
(?>) = flip (<?)
(<?) :: Encode () -> Encode a -> Encode (Maybe a)
(<?) = choose (maybe (Left ()) Right)
(?>>) :: Encode a -> Strict.ByteString -> Encode (Maybe a)
(?>>) a s = a ?> const s
(<<?) :: Strict.ByteString -> Encode a -> Encode (Maybe a)
(<<?) = flip (?>>)
row :: Encode s -> Encode [s]
row enc = Encode $ \opts list -> join $ Seq.fromList $ fmap (getEncode enc opts) list
char :: Encode Char
char = escaped escapeChar BS.charUtf8 BS.stringUtf8
int :: Encode Int
int = unsafeBuilder BS.intDec
integer :: Encode Integer
integer = unsafeBuilder BS.integerDec
float :: Encode Float
float = unsafeBuilder BS.floatDec
double :: Encode Double
double = unsafeBuilder BS.doubleDec
string :: Encode String
string = escaped' escapeString BS.stringUtf8
text :: Encode T.Text
text = escaped' escapeText (BS.byteString . T.encodeUtf8)
byteString :: Encode Strict.ByteString
byteString = escaped' escapeUtf8 BS.byteString
lazyByteString :: Encode LBS.ByteString
lazyByteString = escaped' escapeUtf8Lazy BS.lazyByteString
escaped :: Escaper s t -> (s -> BS.Builder) -> (t -> BS.Builder) -> Encode s
escaped esc sb tb = mkEncodeWithOpts $ \opts s ->
  case _quote opts of
    Nothing -> sb s
    Just q -> tb $ esc (review quoteChar q) (Unescaped s)
escaped' :: Escaper' s -> (s -> BS.Builder) -> Encode s
escaped' escaper = join (escaped escaper)
boolTrueFalse :: Encode Bool
boolTrueFalse = mkEncodeBS $ B.bool "False" "True"
booltruefalse :: Encode Bool
booltruefalse = mkEncodeBS $ B.bool "false" "true"
boolyesno :: Encode Bool
boolyesno = mkEncodeBS $ B.bool "no" "yes"
boolYesNo :: Encode Bool
boolYesNo = mkEncodeBS $ B.bool "No" "Yes"
boolYN :: Encode Bool
boolYN = mkEncodeBS $ B.bool "N" "Y"
bool10 :: Encode Bool
bool10 = mkEncodeBS $ B.bool "0" "1"
encodeOf :: Getting (First a) s a -> Encode a -> Encode s
encodeOf g = encodeOfMay g . choose (maybe (Left ()) Right) conquer
encodeOfMay :: Getting (First a) s a -> Encode (Maybe a) -> Encode s
encodeOfMay g x = contramap (preview g) x
unsafeString :: Encode String
unsafeString = unsafeBuilder BS.stringUtf8
unsafeText :: Encode T.Text
unsafeText = unsafeBuilder (BS.byteString . T.encodeUtf8)
unsafeByteStringBuilder :: Encode BS.Builder
unsafeByteStringBuilder = unsafeBuilder id
unsafeByteString :: Encode Strict.ByteString
unsafeByteString = unsafeBuilder BS.byteString
unsafeLazyByteString :: Encode LBS.ByteString
unsafeLazyByteString = unsafeBuilder BS.lazyByteString
unsafeConst :: Strict.ByteString -> Encode a
unsafeConst b = contramap (pure b) unsafeByteString
intersperseSeq :: a -> Seq a -> Seq a
intersperseSeq y xs = case viewl xs of
  EmptyL -> S.empty
  p :< ps -> p <| (ps <**> (pure y <| S.singleton id))