{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveGeneric #-}

{-|
Module      : Text.Escape
Copyright   : (C) CSIRO 2017-2018
License     : BSD3
Maintainer  : George Wilson <george.wilson@data61.csiro.au>
Stability   : experimental
Portability : non-portable

Quote characters can be escaped in CSV documents by using two quote characters
instead of one. sv's parser will unescape these sequences as it parses them, so
it wraps them in the newtype 'Unescaped'

Encoding requires you to provide an 'Escaper', which is a function to escape
strings on the way out.
-}

module Text.Escape (
  Unescaped (Unescaped, getRawUnescaped)
  , Escaper
  , Escaper'
  , escapeString
  , escapeText
  , escapeUtf8
  , escapeUtf8Lazy
  , escapeChar
) where

import Control.DeepSeq (NFData)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Lazy.UTF8 as UTF8L
import Data.Foldable (Foldable)
import Data.Functor (Functor)
import Data.Monoid (Monoid)
import Data.Semigroup (Semigroup ((<>)))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Traversable (Traversable)
import GHC.Generics (Generic)

-- | Wrapper for text that is known to be in an unescaped form
newtype Unescaped a =
  Unescaped { getRawUnescaped :: a }
  deriving (Eq, Ord, Show, Semigroup, Monoid, Functor, Foldable, Traversable, Generic)

instance NFData a => NFData (Unescaped a)

-- | A function that, given a char, escapes all occurrences of that char.
--
-- This version allows the escaping to be type-changing. For example, escaping
-- a single char can result in a string with two characters.
type Escaper s t = Char -> Unescaped s -> t

-- | A function that, given a char, escapes all occurrences of that char.
type Escaper' a = Char -> Unescaped a -> a

-- | Replaces all occurrences of the given character with two occurrences of that
-- character, non-recursively, in the given 'String'.
--
-- >>> escapeString ''' "hello 'string'"
-- "hello ''string''"
--
escapeString :: Escaper' String
escapeString c = concatMap (doubleChar c) . getRawUnescaped

-- | Replaces all occurrences of the given character with two occurrences of that
-- character in the given 'Text'
--
-- @
-- {- LANGUAGE OverloadedStrings -}
--
-- >>> escapeText ''' "hello 'text'"
-- "hello ''text''"
-- @
escapeText :: Escaper' Text
escapeText c =
  let ct = Text.singleton c
  in  Text.replace ct (ct <> ct) . getRawUnescaped

-- | Replaces all occurrences of the given character with two occurrences of that
-- character in the given ByteString, which is assumed to be UTF-8 compatible.
--
-- @
-- {- LANGUAGE OverloadedStrings -}
-- >>> escapeUtf8 ''' "hello 'bytestring'"
-- "hello ''bytestring''"
-- @
escapeUtf8 :: Escaper' B.ByteString
escapeUtf8 c =
  UTF8.fromString . concatMap (doubleChar c) . UTF8.toString . getRawUnescaped

-- | Replaces all occurrences of the given character with two occurrences of that
-- character in the given lazy ByteString, which is assumed to be UTF-8 compatible.
--
-- @
-- {- LANGUAGE OverloadedStrings -}
--
-- >>> escapeUtf8Lazy ''' "hello 'lazy bytestring'"
-- "hello ''lazy bytestring''"
-- @
escapeUtf8Lazy :: Escaper' L.ByteString
escapeUtf8Lazy c =
  UTF8L.fromString . concatMap (doubleChar c) . UTF8L.toString . getRawUnescaped

-- | Escape a character, which must return a string.
--
-- >>> escapeChar ''' '''
-- "''"
--
-- >>> escapeChar ''' 'z'
-- "z"
--
escapeChar :: Escaper Char String
escapeChar c = doubleChar c . getRawUnescaped

doubleChar :: Char -> Char -> String
doubleChar q z = if z == q then [q,q] else [z]