-- |
-- Module      : Text.PercentFormat
-- Copyright   : (c) 2016-2018 Rudy Matela
-- License     : 3-Clause BSD  (see the file LICENSE)
-- Maintainer  : Rudy Matela <rudy@matela.com.br>
--
-- The "Text.PercentFormat" library provides printf-style string formatting.
-- It provides a '%' operator (as in Ruby or Python)
-- and uses the old C-printf-style format you know and love.
--
-- This library differs from "Text.Printf" in that it does not rely on custom
-- typeclasses -- it works on anything that is a 'Show' instance that produces
-- output in the supported formats.
--
--
-- Formatting one value with '-%':
--
-- > > "Hello %s!" -% "World"
-- > "Hello World!"
--
--
-- Formatting three values, tuple style, with '-%%%':
--
-- > > "load average: %1.2f %1.2f %1.2f" -%%% (0.00, 0.066, 0.11)
-- > "load average: 0.00 0.07 0.11"
--
--
-- Formatting three values, chain style, with '%' and '-%':
--
-- > > "load average: %1.2f %1.2f %1.2f" % 0.00 % 0.066 -% 0.11
-- > "load average: 0.00 0.07 0.11"
--
--
-- To produce a string with a percent sign (@%@),
-- use two percent signs (@%%@):
--
-- > > "memory usage: %i%%" -% 13
-- > "memory usage: 13%"
--
--
-- Percent signs are duplicated when using the '%' operator to allow chaining
-- (further formats):
--
-- > > "percent sign: %s, memory usage: %i%%" % "%" % 87
-- > "percent sign: %%, memory usage: 87%%"
--
-- /Always/ use the '-%' operator when formatting the /last value/
-- to remove duplicate @%@ signs:
--
-- > > "percent sign: %s, memory usage: %i%%" % "%" -% 87
-- > "percent sign: %, memory usage: 87%"
--
-- To print, just prefix you format expression with "@putStrLn $@":
--
-- > > putStrLn $ "Hello %s!" -% "World"
-- > Hello World!
--
--
-- == Supported formats
--
-- * /r/ -- 'show' representation as-is (including quotes for strings).
--
--     > > "%r" % "string"
--     > "\"string\""
--
--     > > "%r" % Just 10
--     > "Just 10"
--
-- * /s/ -- string.  If the argument is 'show'ed as a 'String', intersperse it,
--          otherwise include representation in whole.
--
--     > > "%s" % "string"
--     > "string"
--
--     > > "%s" % 10
--     > "10"
--
--     > > "%s" % Just "string"
--     > "Just \"string\""
--
-- * /c/ -- Argument is converted to a single character.
--   Accepts arguments that when 'show'ed are represented as 'Char's.
--
--     > > "%c" % 'a'
--     > "a"
--
-- * /i/ -- Argument is converted to the nearest decimal integer.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%i" % 5040
--     > 5040
--
--     > > "%i" % 3.141
--     > 3
--
-- * /d/ -- Argument is converted to a decimal integer.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%d" % 5040
--     > 5040
--
--     > > "%i" % 3.141
--     > 3.141
--
-- * /x/ -- Argument is converted to hexadecimal format with lowercase letters.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%x" % 5040
--     > "13b0"
--
--     Differently from C's @printf@, negative integers are printed prefixed with
--     a minus (@-@) sign:
--
--     > > "%x" % (-5040)
--     > "-13b0"
--
--     Differently from C's @printf@, this library is able to show hexadecimal
--     fractional parts:
--
--     > > "%.6x" % pi
--     > "3.243f6b"
--
-- * /X/ -- Argument is converted to hexadecimal format with capital letters.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%X" % 5040
--     > "13B0"
--
-- * /o/ -- Argument is converted to octal format.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%o" % 5040
--     > "11660"
--
--     > > "%.6o" % pi
--     > "3.110376"
--
-- * /b/ -- Argument is converted to binary format.
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%b" % 5040
--     > "1001110110000"
--
--     > > "%.6b" % pi
--     > "11.001001"
--
-- * /f/ -- Argument is converted to decimal format with a fractional part
--   (even when the given argument is an integer).
--   Accepts arguments that when 'show'ed are represented as either
--   'Integer's, 'Rational's or 'Double's.
--
--     > > "%f" % 5040
--     > "5040.0"
--
--     > > "%f" % pi
--     > "3.141592653589793"
--
-- * /e/ -- Argument is converted to scientific notation.
--       __This does not work yet.  To be added in a future version.__
--
-- * /q/ -- Argument is converted to a rational number.
--       __This does not work yet.  To be added in a future version.__
--
--
-- == Supported flag charaters
--
-- * /0/ -- the numeric value should be padded by zeros.
--
--     > > "%08i" % 5040
--     > "00005040"
--
-- * /-/ -- left adjusted values.
--
--     > > "%-8i" % 5040
--     > "5040    "
--
-- * / / -- leave a blank before a positive number.
--
--     > > "% i" % 5040
--     > " 5040"
--
--     > > "% i" % (-5040)
--     > "-5040"
--
-- * /+/ -- leave a plus sign before a positive number.
--
--     > > "%+i" % 5040
--     > "+5040"
--
--     > > "%+i" % (-5040)
--     > "-5040"
--
-- * /[1-9][0-9]*/ -- minimum field width.
--
--     > > "%8i" % 5040
--     > "    5040"
--
-- * /.[0-9][0-9]*/ -- precision.
--
--     > > "%.2i" % 5040
--     > "5040.00"
--
--     > > "%9.2i" % 5040
--     > "  5040.00"
--
--
-- == How does it work?
--
-- "Text.PercentFormat" works on values that are 'Show' instances producing
-- results in the expected format.  Take for example the following number type:
--
-- > data Digit = Zero | One | Two | Three
-- > instance Show Digit where
-- >   show Zero   =  "0"
-- >   show One    =  "1"
-- >   show Two    =  "2"
-- >   show Three  =  "3"
--
-- "Text.PercentFormat" works fine on it:
--
-- > > "%d %i %f %.2f" Zero One Two Three
-- > "0 1 2 3.00"
--
-- Because when 'show'ed, values of this @Digit@ type are represented as 'Integer's.
--
--
-- == Error Handling
--
-- This library is designed to avoid raising errors.
-- If conversion cannot be performed an exclamation mark (@!@) is produced.
-- If there are missing format strings an interrogation mark (@?@) is produced.
-- For example:
--
-- > > "%d %d" -% "Ten"
-- > "! ?"
--
-- The only two instances where errors are raised are:
--
-- 1. the argument values contain errors themselves:
--
--     > > "Hello %s!" % error "err"
--     > *** Exception err
--
--     > > error "err" % "World"
--     > *** Exception err
--
-- 2. the format string is not supported:
--
--     > > "%j" % 10
--     > *** Exception: unknown format string `j'
--
--
-- == Known bugs
--
-- * @"%x" % 3.1415926@ takes too long to run.
--
-- * @"%x" % pi@ takes /very very long/ to run.
module Text.PercentFormat
  ( (%)
  , (-%)
  , (/%)

  , (%%)
  , (%%%)
  , (%%%%)
  , (%%%%%)
  , (%%%%%%)

  , (-%%)
  , (-%%%)
  , (-%%%%)
  , (-%%%%%)
  , (-%%%%%%)

  , (+%)
  )
where

import Data.Maybe (listToMaybe, fromMaybe)
import Data.Char (isDigit, toUpper)
import Text.PercentFormat.Spec  as S
import Text.PercentFormat.Utils hiding (align)
import Text.PercentFormat.Quotient (maybeReadQ, digits, Quotient, infinity, nan)
import qualified Text.PercentFormat.Quotient as Q
import qualified Text.PercentFormat.Utils as U
import Prelude hiding (showString, showChar)

-- | Formats a single value into a string without finalizing:
--   leaving duplicate percent signs & remaining format sequences.
--
-- > > "Hello %s!" % "World"
-- > "Hello World!"
--
-- > > "processor usage: %d%%" % 67
-- > "processor usage: 67%%"
--
-- > > "load avg: %.2f %.2f %.2f" % 0.666
-- > "load avg: %0.67 %.2f %.2f"
--
-- Please use '-%' when formatting the last value into a string so that
-- duplicate percent signs are removed.
(%) :: Show a => String -> a -> String
('%':s) % x =
  case ty sp of
  Percent     -> '%':'%': s' % x
  ReprSpec    -> (duplicatePercents $ showRepr    sp x) ++ s'
  StringSpec  -> (duplicatePercents $ showString  sp x) ++ s'
  CharSpec    -> (duplicatePercents $ showChar    sp x) ++ s'
  NumberSpec  -> (duplicatePercents $ showDigits  sp x) ++ s'
  where
  (sp,s') = parseSpec s
(c:s)       % x = c : s % x
""          % x = ""
infixl 9 %

showRepr :: Show a => Spec -> a -> String
showRepr spec = align spec . show

showString :: Show a => Spec -> a -> String
showString spec s =
  case maybeRead (show s) of
  Nothing -> align spec (show s)
  Just s  -> align spec s

showChar :: Show a => Spec -> a -> String
showChar spec c =
  case maybeRead (show c) of
  Nothing -> err '!' spec
  Just c  -> align spec (c:"")

-- TODO: refactor showDigits (currently very hacky)
showDigits :: Show a => Spec -> a -> String
showDigits spec x =
  case maybeReadQ (show x) of
  Nothing -> err '!' spec
  Just q -> applyWhen (padWith spec /= ' ') (signal q ++)
          . align' q
          . applyWhen (padWith spec == ' ') (signal q ++)
          . either id (\(ids,fds,pds) -> capitalize $ showds ids fds pds (precision spec))
          . digits (base spec)
          . round' (base spec) (precision spec)
          $ q
  where
  capitalize = applyWhen (capitalizeDigits spec) (map toUpper)
  signal q | q >= 0 = positivePrefix spec
           | q <  0 = "-"
  align' :: Quotient -> String -> String
  align' q = if padWith spec == ' '
               then align spec
               else align spec{width = width spec - length (signal q)}
  round' :: Int -> Maybe Int -> Quotient -> Quotient
  round' _ _ q | Q.isInfinite q = q
  round' _ _ q | Q.isNaN q      = q
  round' _ Nothing  q = q
  round' b (Just p) q = round (q * fromIntegral b ^ p) Q.% fromIntegral b ^ p
  showds :: [Int] -> [Int] -> [Int] -> Maybe Int -> String
  showds ids fds []  Nothing  | length fds < minPrecision spec
                            = showds ids (fds ++ replicate (minPrecision spec - length fds) 0) [] Nothing
  showds ids []  _   Nothing  = intsToDigits ids
  showds ids fds pds (Just 0) = intsToDigits ids
  showds ids fds pds Nothing  = intsToDigits ids ++ "."
                             ++ intsToDigits fds ++ showPeriod pds
  showds ids fds pds (Just pr) = intsToDigits ids ++ "."
                              ++ intsToDigits (take pr (fds ++ loop pds ++ repeat 0))
  showPeriod [] = ""
  showPeriod xs = intsToDigits xs
               ++ intsToDigits xs
               ++ intsToDigits xs
               ++ "..."

err :: Char -> Spec -> String
err c spec = align spec{padWith=c} (c:"")

-- | Formats the last value into a string.
--   This finalizes formatting, removing duplicate percent signs and replacing
--   remaining format sequences with interrogation marks.
--
-- > > "Hello %s!" -% "World"
-- > "Hello World!"
--
-- > > "processor usage: %d%%" -% 67
-- > "processor usage: 67%"
--
-- > > "load avg: %.2f %.2f %.2f" % 0.666
-- > "load avg: %0.67 ? ?"
--
-- Please use '%' if you intend to further format values (chaining).
(-%) :: Show a => String -> a -> String
s -% x = s % x /% '?'
infixl 9 -%

-- | Replaces "%%" by "%".  Any remaining occurrences of format strings are
--   replaced by the given error character.  Field width is respected when
--   possible.
--
-- > > "100%% %i" /% '?'
-- > "100% ?"
--
-- > > "100%% %03i" /% '?'
-- > "100% ???"
(/%) :: String -> Char -> String
s /% errChar = depercent s
  where
  depercent ('%':s) = let (spec,s') = parseSpec s
                          s'' = case ty spec of
                                Percent     -> "%"
                                _           -> err errChar spec
                      in s'' ++ depercent s'
  depercent (c:s) = c : depercent s
  depercent "" = ""

-- | Aligns a string following a given spec.
--
-- > align spec{width=1} "asdf"
-- "asdf"
-- > align spec{width=5} "asdf"
-- " asdf"
-- > align spec{width=5, leftAlign=True} "asdf"
-- "asdf "
align :: Spec -> String -> String
align spec = U.align (S.leftAlign spec) (padWith spec) (width spec)

duplicatePercents :: String -> String
duplicatePercents ('%':s) = '%':'%':duplicatePercents s
duplicatePercents (c:s)   = c:duplicatePercents s
duplicatePercents ""      = ""

-- | Formats two values into a string without finalizing:
--   leaving duplicate percent signs & remaining format sequences.
--
-- > > "%s %s!" %% ("Hello","World")
-- > "Hello World!"
--
-- > > "load avg: %.2f %.2f %.2f" %% (0.666,0.333)
-- > "load avg: %0.67 %0.33 %.2f"
--
-- In general:
--
-- > s %% (x,y) == s % x % y
--
-- Please use '-%%' if you don't intend to format values into a string any further.
(%%) :: (Show a, Show b) => String -> (a,b) -> String
s %% (x,y) = s % x % y

-- | Formats three values into a string without finalizing.
--
-- > > "load avg: %.2f %.2f %.2f" %%% (0.666,0.333,0.1)
-- > "load avg: %0.67 %0.33 %0.10"
(%%%) :: (Show a, Show b, Show c) => String -> (a,b,c) -> String
s %%% (x,y,z) = s % x % y % z

-- | Formats four values into a string without finalizing.
(%%%%) :: (Show a, Show b, Show c, Show d) => String -> (a,b,c,d) -> String
s %%%% (x,y,z,w) = s % x % y % z % w

-- | Formats five values into a string without finalizing.
(%%%%%) :: (Show a, Show b, Show c, Show d, Show e)
        => String -> (a,b,c,d,e) -> String
s %%%%% (x,y,z,w,v) = s % x % y % z % w % v

-- | Formats six values into a string without finalizing.
(%%%%%%) :: (Show a, Show b, Show c, Show d, Show e, Show f)
         => String -> (a,b,c,d,e,f) -> String
s %%%%%% (x,y,z,w,v,u) = s % x % y % z % w % v % u

-- | Formats two values into a string and finalizes it:
--   removing duplicate percent signs & replacing remaining format sequences
--   with interrogation marks.
--
-- > > "%s %s!" -%% ("Hello","World")
-- > "Hello World!"
--
-- > > "load avg: %.2f %.2f %.2f" -%% (0.666,0.333)
-- > "load avg: %0.67 %0.33 ?"
--
-- In general:
--
-- > s -%% (x,y) == s % x -% y
--
-- Please use '%%' if you intend to further format values.
(-%%) :: (Show a, Show b) => String -> (a,b) -> String
s -%% t = s %% t /% '?'

-- | Formats three values into a string and finalizes it.
--
-- > > "load avg: %.2f %.2f %.2f" -%%% (0.666,0.333,0.1)
-- > "load avg: %0.67 %0.33 %0.10"
(-%%%) :: (Show a, Show b, Show c) => String -> (a,b,c) -> String
s -%%% t = s %%% t /% '?'

-- | Formats four values into a string and finalizes it.
(-%%%%) :: (Show a, Show b, Show c, Show d) => String -> (a,b,c,d) -> String
s -%%%% t = s %%%% t /% '?'

-- | Formats five values into a string and finalizes it.
(-%%%%%) :: (Show a, Show b, Show c, Show d, Show e)
        => String -> (a,b,c,d,e) -> String
s -%%%%% t = s %%%%% t /% '?'

-- | Formats six values into a stirng and finalizes it.
(-%%%%%%) :: (Show a, Show b, Show c, Show d, Show e, Show f)
         => String -> (a,b,c,d,e,f) -> String
s -%%%%%% t = s %%%%%% t /% '?'

-- | Just an alias to '%' for use whenever "Data.Ratio" is in scope.
--
-- > import Data.Ratio
-- > import Text.PercentFormat hiding ((%))
-- > "..." +% 1 -% 2
(+%) :: Show a => String -> a -> String
(+%) = (%)