{-# LANGUAGE OverloadedStrings #-}

module Data.Text.Format.Heavy.Build
  (format, formatEither,
   makeBuilder,
   -- * Formatters building utilities
   align, applySign, applySharp, convertText,
   formatInt, formatStr, formatFloat, formatBool
  ) where

import Control.Monad
import Data.Monoid
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as B
import Data.Text.Lazy.Builder.Int (decimal, hexadecimal)
import Data.Text.Lazy.Builder.RealFloat

import Data.Text.Format.Heavy.Types
import Data.Text.Format.Heavy.Formats

makeBuilder :: VarContainer c => Format -> c -> Either String B.Builder
makeBuilder (Format items) vars = mconcat `fmap` mapM go items
  where
    go (FString s) = Right $ B.fromLazyText s
    go (FVariable name fmt) =
      case lookupVar name vars of
        Nothing -> Left $ "Parameter not found: " ++ TL.unpack name
        Just var -> formatVar fmt var
{-# INLINE makeBuilder #-}

-- | The main formatting function.
-- This function throws @error@ if some error detected during format string parsing or formatting itself.
format :: VarContainer vars => Format -> vars -> TL.Text
format fmt vars = either error id $ formatEither fmt vars

-- | The main formatting function.
-- This version returns @Left@ value with error description in case of error in 
-- format string or error during formatting.
formatEither :: VarContainer vars => Format -> vars -> Either String TL.Text
formatEither fmt vars = either Left (Right . B.toLazyText) $ makeBuilder fmt vars

align' :: Int -> Align -> Char -> B.Builder -> B.Builder
align' width AlignLeft fill text =
  B.fromLazyText $ TL.justifyLeft (fromIntegral width) fill $ B.toLazyText text
align' width AlignRight fill text =
  B.fromLazyText $ TL.justifyRight (fromIntegral width) fill $ B.toLazyText text
align' width AlignCenter fill text =
  B.fromLazyText $ TL.center (fromIntegral width) fill $ B.toLazyText text

-- | Align text within available width according to format
align :: GenericFormat -> B.Builder -> B.Builder
align fmt text =
  case (gfAlign fmt, gfWidth fmt) of
    (Just a, Just w) -> align' w a (gfFillChar fmt) text
    _ -> text

-- | Add @+/-@ sign to the number representation, if required
applySign :: (Num a, Ord a) => Sign -> a -> B.Builder -> B.Builder
applySign Always x text =
  if x >= 0
    then B.singleton '+' <> text
    else B.singleton '-' <> text
applySign OnlyNegative x text =
  if x >= 0
    then text
    else B.singleton '-' <> text
applySign SpaceForPositive x text =
  if x >= 0
    then B.singleton ' ' <> text
    else B.singleton '-' <> text

-- | Add @0x@ to the number representation, if required
applySharp :: Bool -> Radix -> B.Builder -> B.Builder
applySharp False _ text = text
applySharp True Decimal text = text
applySharp True Hexadecimal text = B.fromLazyText "0x" <> text

-- | Apply text conversion.
convertText :: Maybe Conversion -> B.Builder -> B.Builder
convertText Nothing builder = builder
convertText (Just conv) builder = B.fromLazyText $ converter $ B.toLazyText builder
  where
    converter = case conv of
                  UpperCase -> TL.toUpper
                  LowerCase -> TL.toLower
                  TitleCase -> TL.toTitle

-- | Format integer number according to GenericFormat
formatInt :: Integral a => GenericFormat -> a -> B.Builder
formatInt fmt x = align fmt $ applySign (gfSign fmt) x $ applySharp (gfLeading0x fmt) radix $ inRadix
  where
   radix = fromMaybe Decimal (gfRadix fmt)
   inRadix = case radix of
               Decimal -> decimal (abs x)
               Hexadecimal -> hexadecimal (abs x)

-- | Format floating-point number according to GenericFormat
formatFloat :: RealFloat a => GenericFormat -> a -> B.Builder
formatFloat fmt x =
    align fmt
    $ applySign (gfSign fmt) x
    $ formatRealFloat Fixed (gfPrecision fmt)
    $ abs x

-- | Format Text according to GenericFormat.
formatStr :: GenericFormat -> TL.Text -> B.Builder
formatStr fmt text = convertText (gfConvert fmt) $ align fmt $ B.fromLazyText text

-- | Format boolean value.
formatBool :: BoolFormat -> Bool -> B.Builder
formatBool fmt True = B.fromLazyText $ bfTrue fmt
formatBool fmt False = B.fromLazyText $ bfFalse fmt