{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}

-- |
-- Module      : Formatting.Formatters
-- Copyright   : (c) 2013 Chris Done, 2013 Shachaf Ben-Kiki
-- License     : BSD3
-- Maintainer  : alex@farfromthere.net
-- Stability   : experimental
-- Portability : GHC
--
-- Formatting functions.

module Formatting.Formatters
  (
  -- * Text/string types
  text,
  stext,
  string,
  shown,
  char,
  builder,
  fconst,
  -- * Numbers
  int,
  float,
  fixed,
  sci,
  scifmt,
  shortest,
  groupInt,
  commas,
  ords,
  plural,
  asInt,
  -- * Padding
  left,
  right,
  center,
  fitLeft,
  fitRight,
  -- * Bases
  base,
  bin,
  oct,
  hex,
  prefixBin,
  prefixOct,
  prefixHex,
  bytes,
  -- * Buildables
  build,
  Buildable,
  ) where

import           Formatting.Internal

import           Data.Char (chr, ord)
#if !(MIN_VERSION_base(4,11,0))
import           Data.Monoid ((<>))
#endif
import           Data.Scientific
import qualified Data.Text as S
import qualified Data.Text as T
import           Formatting.Buildable (Buildable)
import qualified Formatting.Buildable as B (build)
import qualified Data.Text.Format as T
import           Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as LT
import           Data.Text.Lazy.Builder (Builder)
import qualified Data.Text.Lazy.Builder as T
import           Data.Text.Lazy.Builder.Scientific
import           Numeric (showIntAtBase)

-- | Output a lazy text.
text :: Format r (Text -> r)
text = later T.fromLazyText

-- | Output a strict text.
stext :: Format r (S.Text -> r)
stext = later T.fromText

-- | Output a string.
string :: Format r (String -> r)
string = later (T.fromText . T.pack)

-- | Output a showable value (instance of 'Show') by turning it into
-- 'Text':
--
-- >>> format ("Value number " % shown % " is " % shown % ".") 42 False
-- "Value number 42 is False."
shown :: Show a => Format r (a -> r)
shown = later (T.fromText . T.pack . show)

-- | Output a character.
char :: Format r (Char -> r)
char = later B.build

-- | Build a builder.
builder :: Format r (Builder -> r)
builder = later id

-- | Like `const` but for formatters.
fconst :: Builder -> Format r (a -> r)
fconst m = later (const m)

-- | Build anything that implements the "Buildable" class.
build :: Buildable a => Format r (a -> r)
build = later B.build

-- | Render an integral e.g. 123 -> \"123\", 0 -> \"0\".
int :: Integral a => Format r (a -> r)
int = base 10

-- | Render some floating point with the usual notation, e.g. 123.32 => \"123.32\"
float :: Real a => Format r (a -> r)
float = later T.shortest

-- | Render a floating point number using normal notation, with the
-- given number of decimal places.
fixed :: Real a => Int -> Format r (a -> r)
fixed i = later (T.fixed i)

-- | Render a floating point number using the smallest number of
-- digits that correctly represent it. Note that in the case of whole
-- numbers it will still add one decimal place, e.g. "1.0".
shortest :: Real a => Format r (a -> r)
shortest = later T.shortest

-- | Render a scientific number.
sci :: Format r (Scientific -> r)
sci = later scientificBuilder

-- | Render a scientific number with options.
scifmt :: FPFormat -> Maybe Int -> Format r (Scientific -> r)
scifmt f i = later (formatScientificBuilder f i)

-- | Shows the Int value of Enum instances using 'fromEnum'.
--
-- >>> format ("Got: " % char % " (" % asInt % ")") 'a' 'a'
-- "Got: a (97)"
asInt :: Enum a => Format r (a -> r)
asInt = later (T.shortest . fromEnum)

-- | Pad the left hand side of a string until it reaches k characters
-- wide, if necessary filling with character c.
left :: Buildable a => Int -> Char -> Format r (a -> r)
left i c = later (T.left i c)

-- | Pad the right hand side of a string until it reaches k characters
-- wide, if necessary filling with character c.
right :: Buildable a => Int -> Char -> Format r (a -> r)
right i c = later (T.right i c)

-- | Pad the left & right hand side of a string until it reaches k characters
-- wide, if necessary filling with character c.
center :: Buildable a => Int -> Char -> Format r (a -> r)
center i c = later centerT where
  centerT = T.fromLazyText . LT.center (fromIntegral i) c . T.toLazyText . B.build

-- | Group integral numbers, e.g. groupInt 2 '.' on 123456 -> \"12.34.56\".
groupInt :: (Buildable n,Integral n) => Int -> Char -> Format r (n -> r)
groupInt 0 _ = later B.build
groupInt i c =
  later
    (\n ->
       if n < 0
         then "-" <> commaize (negate n)
         else commaize n)
  where
    commaize =
      T.fromLazyText .
      LT.reverse .
      foldr merge "" .
      LT.zip (zeros <> cycle' zeros') . LT.reverse . T.toLazyText . B.build
    zeros = LT.replicate (fromIntegral i) (LT.singleton '0')
    zeros' = LT.singleton c <> LT.tail zeros
    merge (f, c') rest
      | f == c = LT.singleton c <> LT.singleton c' <> rest
      | otherwise = LT.singleton c' <> rest
    cycle' xs = xs <> cycle' xs

-- | Fit in the given length, truncating on the left.
fitLeft :: Buildable a => Int -> Format r (a -> r)
fitLeft size = later (fit (fromIntegral size)) where
  fit i = T.fromLazyText . LT.take i . T.toLazyText . B.build

-- | Fit in the given length, truncating on the right.
fitRight :: Buildable a => Int -> Format r (a -> r)
fitRight size = later (fit (fromIntegral size)) where
  fit i = T.fromLazyText .
          (\t -> LT.drop (LT.length t - i) t)
          . T.toLazyText
          . B.build

-- | Add commas to an integral, e.g 12000 -> \ "12,000".
commas :: (Buildable n,Integral n) => Format r (n -> r)
commas = groupInt 3 ','

-- | Add a suffix to an integral, e.g. 1st, 2nd, 3rd, 21st.
ords :: Integral n => Format r (n -> r)
ords = later go
  where go n
          | tens > 3 && tens < 21 = T.fixed 0 n <> "th"
          | otherwise =
            T.fixed 0 n <>
            case n `mod` 10 of
              1 -> "st"
              2 -> "nd"
              3 -> "rd"
              _ -> "th"
          where tens = n `mod` 100

-- | English plural suffix for an integral.
--
-- For example:
--
-- >>> set -XOverloadedStrings
-- >>> formatPeople = format (int % " " <> plural "person" "people" % ".") :: Int -> Data.Text.Lazy.Text
-- >>> formatPeople 1
-- "1 person."
-- >>> formatPeople 3
-- "3 people."
plural :: (Num a, Eq a) => Text -> Text -> Format r (a -> r)
plural s p = later (\i -> if i == 1 then B.build s else B.build p)

-- | Render an integral at base n.
base :: Integral a => Int -> Format r (a -> r)
base numBase = later (B.build . atBase numBase)

-- | Render an integer using binary notation. (No leading 0b is
-- added.) Defined as @bin = 'base' 2@.
bin :: Integral a => Format r (a -> r)
bin = base 2
{-# INLINE bin #-}

-- | Render an integer using octal notation. (No leading 0o is
-- added.) Defined as @oct = 'base' 8@.
oct :: Integral a => Format r (a -> r)
oct = base 8
{-# INLINE oct #-}

-- | Render an integer using hexadecimal notation. (No leading 0x is
-- added.) Has a specialized implementation.
hex :: Integral a => Format r (a -> r)
hex = later T.hex
{-# INLINE hex #-}

-- | Render an integer using binary notation with a leading 0b.
--
-- See also 'Formatting.Combinators.binPrefix' for fixed-width formatting.
prefixBin :: Integral a => Format r (a -> r)
prefixBin = "0b" % bin
{-# INLINE prefixBin #-}

-- | Render an integer using octal notation with a leading 0o.
--
-- See also 'Formatting.Combinators.octPrefix' for fixed-width formatting.
prefixOct :: Integral a => Format r (a -> r)
prefixOct = "0o" % oct
{-# INLINE prefixOct #-}

-- | Render an integer using hexadecimal notation with a leading 0x.
--
-- See also 'Formatting.Combinators.hexPrefix' for fixed-width formatting.
prefixHex :: Integral a => Format r (a -> r)
prefixHex = "0x" % hex
{-# INLINE prefixHex #-}

-- The following code is mostly taken from `Numeric.Lens.' (from
-- `lens') and modified.

-- | Internal function that converts a number to a base base-2 through
-- base-36.
atBase :: Integral a => Int -> a -> String
atBase b _ | b < 2 || b > 36 = error ("base: Invalid base " ++ show b)
atBase b n =
  showSigned' (showIntAtBase (toInteger b) intToDigit') (toInteger n) ""
{-# INLINE atBase #-}

-- | A simpler variant of 'Numeric.showSigned' that only prepends a dash and
-- doesn't know about parentheses
showSigned' :: Real a => (a -> ShowS) -> a -> ShowS
showSigned' f n
  | n < 0     = showChar '-' . f (negate n)
  | otherwise = f n

-- | Like 'Data.Char.intToDigit', but handles up to base-36
intToDigit' :: Int -> Char
intToDigit' i
  | i >= 0  && i < 10 = chr (ord '0' + i)
  | i >= 10 && i < 36 = chr (ord 'a' + i - 10)
  | otherwise = error ("intToDigit': Invalid int " ++ show i)

-- | Renders a given byte count using an appropiate decimal binary suffix:
--
-- >>> format (bytes shortest) 1024
-- "1KB"
--
-- >>> format (bytes (fixed 2 % " ")) (1024*1024*5)
-- "5.00 MB"
--
bytes :: (Ord f,Integral a,Fractional f)
      => Format Builder (f -> Builder) -- ^ formatter for the decimal part
      -> Format r (a -> r)
bytes d = later go
  where go bs =
          bprint d (fromIntegral (signum bs) * dec) <> bytesSuffixes !!
          i
          where (dec,i) = getSuffix (abs bs)
        getSuffix n =
          until p
                (\(x,y) -> (x / 1024,y + 1))
                (fromIntegral n,0)
          where p (n',numDivs) =
                  n' < 1024 || numDivs == (length bytesSuffixes - 1)
        bytesSuffixes =
          ["B","KB","MB","GB","TB","PB","EB","ZB","YB"]