{-# LANGUAGE DefaultSignatures    #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE TypeOperators        #-}
{-# LANGUAGE UndecidableInstances #-}

module Text.Format.Class
  ( Formatter
  , FormatArg(..)
  , FormatType(..)
  , (:=) (..)
  ) where

import           Control.Applicative
import           Control.Monad.Catch
import           Data.Char
import           Data.Either
import           Data.Int
import           Data.List           ((!!))
import           Data.Map            hiding (map)
import           Data.Maybe
import           Data.Time.Format
import           Data.Word
import           GHC.Generics
import           Numeric
import           Numeric.Natural

import           Text.Format.ArgFmt
import           Text.Format.ArgKey
import           Text.Format.Error
import           Text.Format.Format

type Formatter = ArgKey -> ArgFmt -> Either SomeException String


{-| Typeclass of formatable values.

The 'formatArg' method takes a value, a key and a field format descriptor and
either fails due to a 'ArgError' or  produce a string as the result.
There is a default 'formatArg' for 'Generic' instances.

There are two reasons may cause formatting fail

  (1) Can not find argument for the given key.

  (2) The field format descriptor does not match the argument.

==== Extending to new types

Those format functions can be extended to format types other than those
provided by default. This is done by instantiating 'FormatArg'.

Examples

@
  \{\-\# LANGUAGE DeriveGeneric     \#\-\}
  \{\-\# LANGUAGE OverloadedStrings \#\-\}

  import           Control.Exception
  import           GHC.Generics
  import           Text.Format

  instance FormatArg () where
    formatArg x k fmt@(ArgFmt{fmtSpecs=\"U\"}) =
      let fmt' = fmt{fmtSpecs = \"\"}
      in  formatArg (show x) k fmt'
    formatArg _ _ _ = Left ArgFmtError

  data Color = Red | Yellow | Blue deriving Generic

  instance FormatArg Color

  data Triple = Triple String Int Double deriving Generic

  instance FormatArg Triple

  data Student = Student { no   :: Int
                         , name :: String
                         , age  :: Int
                         } deriving Generic

  instance FormatArg Student

  main :: IO ()
  main = do
    putStrLn $ format \"A unit {:U}\" ()
    putStrLn $ format \"I like {}.\" Blue
    putStrLn $ format \"Triple {0!0} {0!1} {0!2}\" $ Triple \"Hello\" 123 pi
    putStrLn $ format1 \"Student: {no} {name} {age}\" $ Student 1 \"neo\" 30
@
-}
class FormatArg a where
  formatArg :: a -> Formatter

  default formatArg :: (Generic a, GFormatArg (Rep a)) => a -> Formatter
  formatArg x = gformatArg (from x)

  -- | This method is used to get the key of a top-level argument.
  -- Top-level argument means argument that directly passed to format
  -- functions ('format', 'format1').
  keyOf :: a -> ArgKey
  keyOf _ = Index (-1)

-- | Default specs is \"%Y-%m-%dT%H:%M:%S\", see 'formatTime'.
instance {-# OVERLAPPABLE #-} FormatTime t => FormatArg t where
  formatArg = throwIfNest $ \x k fmt ->
    let specs = fmtSpecs fmt <|> "%Y-%m-%dT%H:%M:%S"
        x'    = formatTime defaultTimeLocale specs x
        fmt'  = fmt{fmtSpecs=""}
    in formatArg x' k fmt'

instance {-# OVERLAPPABLE #-} FormatArg a => FormatArg [a] where
  formatArg x (Nest _ k@(Index i))          = formatArg (x !! i) (Index (-1))
  formatArg x (Nest _ k@(Nest (Index i) _)) = formatArg (x !! i) k
  formatArg _ _                             = const $ throwM ArgKeyError

instance {-# OVERLAPPABLE #-} FormatArg a => FormatArg (Map String a) where
  formatArg x (Nest _ k@(Name n))          = formatArg (x ! n) (Index (-1))
  formatArg x (Nest _ k@(Nest (Name n) _)) = formatArg (x ! n) k
  formatArg _ _                            = const $ throwM ArgKeyError

instance {-# OVERLAPPABLE #-} FormatArg a => FormatArg (Map Int a) where
  formatArg x (Nest _ k@(Index i))          = formatArg (x ! i) (Index (-1))
  formatArg x (Nest _ k@(Nest (Index i) _)) = formatArg (x ! i) k
  formatArg _ _                             = const $ throwM ArgKeyError

instance FormatArg String where
  formatArg = throwIfNest formatString

instance FormatArg Char where
  formatArg = throwIfNest $ formatInteger False . toInteger . ord

instance FormatArg Int where
  formatArg = throwIfNest $ formatInteger True . toInteger

instance FormatArg Int8 where
  formatArg = throwIfNest $ formatInteger True . toInteger

instance FormatArg Int16 where
  formatArg = throwIfNest $ formatInteger True . toInteger

instance FormatArg Int32 where
  formatArg = throwIfNest $ formatInteger True . toInteger

instance FormatArg Int64 where
  formatArg = throwIfNest $ formatInteger True . toInteger

instance FormatArg Word where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Word8 where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Word16 where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Word32 where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Word64 where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Integer where
  formatArg = throwIfNest $ formatInteger True

instance FormatArg Natural where
  formatArg = throwIfNest $ formatInteger False . toInteger

instance FormatArg Float where
  formatArg = throwIfNest formatRealFloat

instance FormatArg Double where
  formatArg = throwIfNest formatRealFloat


--------------------------------------------------------------------------------
class GFormatArg f where
  gformatArg :: f p -> ArgKey -> ArgFmt -> Either SomeException String

-- Data type
instance GFormatArg f =>  GFormatArg (D1 c f) where
  gformatArg (M1 x) = gformatArg x

-- Choice between Sums
instance (GFormatArg f, GFormatArg g) => GFormatArg (f :+: g) where
  gformatArg (L1 x) = gformatArg x
  gformatArg (R1 x) = gformatArg x

-- Constructor
-- e.g. data GreetTo = Hello { name :: String } | Hi { name :: String }
--      data GreetTo = Hello String | Hi String
--      data Greet = Hello | Hi
instance (Constructor c, GFormatArg f) => GFormatArg (C1 c f) where
  gformatArg c@(M1 x) = gformatArg x

-- Constructor without arguments
-- e.g. data Greet = Hello | Hi
instance {-# OVERLAPPING  #-} Constructor c => GFormatArg (C1 c U1) where
  gformatArg _ (Nest _ _) = const $ throwM ArgKeyError
  gformatArg c k          = formatArg (conName c) k

-- Try Products one by one
instance (GFormatArg f, GFormatArg g) => GFormatArg (f :*: g) where
  gformatArg (x :*: y) k fmt =
      gformatArg x k fmt <|> gformatArg y (dec1 k) fmt
    where
      x <|> y = catchIf isArgKeyError x $ const y

      dec1 :: ArgKey -> ArgKey
      dec1 (Index i)                   = Index (i - 1)
      dec1 (Nest p (Index i))          = Nest p (Index (i - 1))
      dec1 (Nest p (Nest (Index i) k)) = Nest p $ Nest (Index (i - 1)) k
      dec1 k                           = k

-- Selector (record and none record)
-- e.g. data GreetTo = Hello String | Hi String
--      data GreetTo = Hello { name :: String } | Hi { name :: String }
instance (Selector c, GFormatArg f) => GFormatArg (S1 c f) where
  gformatArg s@(M1 x) (Nest _ (Index 0))
    | selName s == "" = gformatArg x (Index (-1))
  gformatArg s@(M1 x) (Nest _ k@(Nest (Index 0) _))
    | selName s == "" = gformatArg x k
  gformatArg s@(M1 x) (Nest _ (Name record))
    | selName s == record = gformatArg x (Index (-1))
  gformatArg s@(M1 x) (Nest _ k@(Nest (Name record) _))
    | selName s == record = gformatArg x k
  gformatArg _ _ = const $ throwM ArgKeyError

-- FormatArg instance
instance (FormatArg c) => GFormatArg (K1 i c) where
  gformatArg (K1 c) = formatArg c


--------------------------------------------------------------------------------
-- | A typeclass provides the variable arguments magic for 'format'
--
class FormatType t where
  sfmt :: Format -> Map ArgKey Formatter -> t

instance (FormatArg a, FormatType r) => FormatType (a -> r) where
  sfmt fmt args = \arg -> sfmt fmt $
      insert (fixIndex $ keyOf arg) (formatArg arg) args
    where
      fixIndex (Index (-1)) = Index $ length [n | Index n <- keys args]
      fixIndex k            = k

instance FormatType String where
  sfmt fmt args = formats (unFormat fmt)
    where
      formats :: [FmtItem] -> String
      formats = concat . (map formats1)

      onError :: (ArgKey, ArgFmt) -> SomeException -> String
      onError (key, fmt) = catchArgError (errorArgKey $ show key)
                                         (errorArgFmt $ prettyArgFmt $ fmt)

      formats1 :: FmtItem -> String
      formats1 (Lit cs)       = cs
      formats1 (Arg key ifmt) =
        either (onError (key, ifmt)) id $ (getFormatter key) key (fixArgFmt ifmt)

      fixArgFmt :: ArgFmt -> ArgFmt
      fixArgFmt ifmt@(ArgFmt{fmtWidth=(Right key)}) =
        fixArgFmt $ ifmt {fmtWidth = Left $ formatWidth key}
      fixArgFmt ifmt@(ArgFmt{fmtPrecision=(Right key)}) =
        fixArgFmt $ ifmt {fmtPrecision = Left $ formatPrecision key}
      fixArgFmt ifmt = ifmt

      formatWidth, formatPrecision :: ArgKey -> Int
      formatWidth key =
        let fmt = read "0.0d"
        in read $ either (onError (key, fmt)) id $ (getFormatter key) key fmt
      formatPrecision = formatWidth

      getFormatter :: ArgKey -> Formatter
      getFormatter (Nest key _)  = getFormatter key
      getFormatter key = fromMaybe (\_ _ -> throwM ArgKeyError) $ args !? key


--------------------------------------------------------------------------------
-- | A type represents the top-level named key argument.
data (:=) a = String := a
infixr 6 :=

instance FormatArg a => FormatArg ((:=) a) where
  formatArg (_ := x) (Nest _ k) = formatArg x k
  formatArg (_ := x) _          = formatArg x (Index (-1))

  keyOf (ks := _) = Name ks


--------------------------------------------------------------------------------
formatString :: String -> Formatter
formatString x _ fmt@(ArgFmt{fmtSpecs = ""})  = Right $ formatText fmt x
formatString x _ fmt@(ArgFmt{fmtSpecs = "s"}) = Right $ formatText fmt x
formatString _ _ _                            = throwM ArgFmtError

formatInteger :: Bool -> Integer -> Formatter
formatInteger signed x _ fmt@ArgFmt{fmtSpecs=specs} =
    formatNumber fmt signed (sepw specs) (flag specs) <$> (showx specs x)
  where
    sepw :: String -> Int
    sepw "b" = 4
    sepw "o" = 4
    sepw "x" = 4
    sepw "X" = 4
    sepw _   = 3

    flag :: String -> Maybe Char
    flag "b" = Just 'b'
    flag "o" = Just 'o'
    flag "x" = Just 'x'
    flag "X" = Just 'X'
    flag _   = Nothing

    encodeSign :: [Char] -> [Char]
    encodeSign "+" = "++"
    encodeSign "-" = "--"
    encodeSign cs  = cs

    showx :: String -> Integer -> Either SomeException String
    showx specs x
      | x < 0 = ('-' :) <$> showx specs (-x)
    showx "" x    = showx "d" x
    showx "b" x   = Right $ showIntAtBase 2 intToDigit x ""
    showx "c" x   = Right $ encodeSign $ [chr $ fromInteger x]
    showx "d" x   = Right $ show x
    showx "o" x   = Right $ showIntAtBase 8 intToDigit x ""
    showx "x" x   = Right $ showIntAtBase 16 intToDigit x ""
    showx "X" x   = map toUpper <$> showx "x" x
    showx _ _     = throwM ArgFmtError

formatRealFloat :: RealFloat a => a -> Formatter
formatRealFloat x _ fmt@ArgFmt{fmtSpecs=specs, fmtPrecision=prec} =
    formatNumber fmt True 3 Nothing <$> showx specs prec1 x
  where
    prec1 = either (\i -> Just $ if i < 0 then 6 else i) (const $ Just 0) prec

    showx :: RealFloat a
          => String -> Maybe Int -> a -> Either SomeException String
    showx specs p x
      | x < 0 = ('-' :) <$> showx specs p (-x)
    showx "" p x    = showx "g" p x
    showx "e" p x   = Right $ showEFloat p x ""
    showx "E" p x   = map toUpper <$> showx "e" p x
    showx "f" p x   = Right $ showFFloat p x ""
    showx "F" p x   = map toUpper <$> showx "f" p x
    showx "g" p x   = Right $ showGFloat p x ""
    showx "G" p x   = map toUpper <$> showx "g" p x
    showx "%" p x   = (++ "%") <$> (showx "f" p (x * 100))
    showx _ _ _     = throwM ArgFmtError


throwIfNest :: (a -> Formatter) -> a -> Formatter
throwIfNest _ _ (Nest _ _) _ = throwM ArgKeyError
throwIfNest f x k fmt        = f x k fmt