{-# LANGUAGE OverloadedStrings #-}

{-| Minimalist implementation of type-safe formatted strings, borrowing heavily
    from the implementation of the @formatting@ package.

    Example use of this module:

>>> :set -XOverloadedStrings
>>> import Turtle.Format
>>> format ("This is a "%s%" string that takes "%d%" arguments") "format" 2
"This is a format string that takes 2 arguments"

    A `Format` string that takes no arguments has this type:

> "I take 0 arguments" :: Format r r
>
> format "I take 0 arguments" :: Text

>>> format "I take 0 arguments"
"I take 0 arguments"

    A `Format` string that takes one argument has this type:

> "I take "%d%" arguments" :: Format r (Int -> r)
>
> format ("I take "%d%" argument") :: Int -> Text

>>> format ("I take "%d%" argument") 1
"I take 1 argument"

    A `Format` string that takes two arguments has this type:

> "I "%s%" "%d%" arguments" :: Format r (Text -> Int -> r)
>
> format ("I "%s%" "%d%" arguments") :: Text -> Int -> Text

>>> format ("I "%s%" "%d%" arguments") "take" 2
"I take 2 arguments"
-}

{-# LANGUAGE TypeFamilies #-}

module Turtle.Format (
    -- * Format
      Format
    , (%)
    , format
    , printf
    , eprintf
    , makeFormat

    -- * Parameters
    , w
    , d
    , u
    , o
    , x
    , f
    , e
    , g
    , s
    , l
    , fp
    , utc

    -- * Utilities
    , repr
    ) where

import Control.Category (Category(..))
import Control.Monad.IO.Class (MonadIO(..))
import Data.Monoid ((<>))
import Data.String (IsString(..))
import Data.Text (Text, pack)
import Data.Time (UTCTime)
import Data.Word
import Filesystem.Path.CurrentOS (FilePath, toText)
import Numeric (showEFloat, showFFloat, showGFloat, showHex, showOct)
import Prelude hiding ((.), id, FilePath)
import qualified System.IO as IO
import Turtle.Line (Line)

import qualified Data.Text.IO as Text
import qualified Turtle.Line

-- | A `Format` string
newtype Format a b = Format { (>>-) :: (Text -> a) -> b }

instance Category Format where
    id = Format (\return_ -> return_ "")

    fmt1 . fmt2 = Format (\return_ ->
        fmt1 >>- \str1 ->
        fmt2 >>- \str2 ->
        return_ (str1 <> str2) )

-- | Concatenate two `Format` strings
(%) :: Format b c -> Format a b -> Format a c
(%) = (.)

instance (a ~ b) => IsString (Format a b) where
    fromString str = Format (\return_ -> return_ (pack str))

{-| Convert a `Format` string to a print function that takes zero or more typed
    arguments and returns a `Text` string
-}
format :: Format Text r -> r
format fmt = fmt >>- id

{-| Print a `Format` string to standard output (without a trailing newline)

>>> printf ("Hello, "%s%"!\n") "world"
Hello, world!
-}
printf :: MonadIO io => Format (io ()) r -> r
printf fmt = fmt >>- (liftIO . Text.putStr)

{-| Print a `Format` string to standard err (without a trailing newline)

>>> eprintf ("Hello, "%s%"!\n") "world"
Hello, world!
-}
eprintf :: MonadIO io => Format (io ()) r -> r
eprintf fmt = fmt >>- (liftIO . Text.hPutStr IO.stderr)

-- | Create your own format specifier
makeFormat :: (a -> Text) -> Format r (a -> r)
makeFormat k = Format (\return_ -> \a -> return_ (k a))

{-| `Format` any `Show`able value

>>> format w True
"True"
-}
w :: Show a => Format r (a -> r)
w = makeFormat (pack . show)

{-| `Format` an `Integral` value as a signed decimal

>>> format d 25
"25"
>>> format d (-25)
"-25"
-}
d :: Integral n => Format r (n -> r)
d = makeFormat (pack . show . toInteger)

{-| `Format` a `Word` value as an unsigned decimal

>>> format u 25
"25"
-}
u :: Format r (Word -> r)
u = w

{-| `Format` a `Word` value as an unsigned octal number

>>> format o 25
"31"
-}
o :: Format r (Word -> r)
o = makeFormat (\n -> pack (showOct n ""))

{-| `Format` a `Word` value as an unsigned hexadecimal number (without a
    leading \"0x\")

>>> format x 25
"19"
-}
x :: Format r (Word -> r)
x = makeFormat (\n -> pack (showHex n ""))

{-| `Format` a `Double` using decimal notation with 6 digits of precision

>>> format f 25.1
"25.100000"
-}
f :: Format r (Double -> r)
f = makeFormat (\n -> pack (showFFloat (Just 6) n ""))

{-| `Format` a `Double` using scientific notation with 6 digits of precision

>>> format e 25.1
"2.510000e1"
-}
e :: Format r (Double -> r)
e = makeFormat (\n -> pack (showEFloat (Just 6) n ""))

{-| `Format` a `Double` using decimal notation for small exponents and
    scientific notation for large exponents

>>> format g 25.1
"25.100000"
>>> format g 123456789
"1.234568e8"
>>> format g 0.00000000001
"1.000000e-11"
-}
g :: Format r (Double -> r)
g = makeFormat (\n -> pack (showGFloat (Just 6) n ""))

{-| `Format` that inserts `Text`

>>> format s "ABC"
"ABC"
-}
s :: Format r (Text -> r)
s = makeFormat id

{-| `Format` that inserts a `Line`

>>> format l "ABC"
"ABC"
-}
l :: Format r (Line -> r)
l = makeFormat Turtle.Line.lineToText

-- | `Format` a `Filesystem.Path.CurrentOS.FilePath` into `Text`
fp :: Format r (FilePath -> r)
fp = makeFormat (\fpath -> either id id (toText fpath))

-- | `Format` a `UTCTime` into `Text`
utc :: Format r (UTCTime -> r)
utc = w

{-| Convert a `Show`able value to any type that implements `IsString` (such as
    `Text`)

>>> repr (1,2)
"(1,2)"
-}
repr :: (Show a, IsString text) => a -> text
repr = fromString . show