{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}

-- | You want to add formatting support for your custom type. This is the right module.
--
-- In PyF, formatters are in three categories:
--
-- - Integral numbers, which are numbers without fractional part
-- - Fractional numbers, which are numbers with a fractional part
-- - String, which represents text.
--
-- The formatting can be either explicit or implicit. For example:
--
-- >>> let x = 10 in [fmt|{x}|]
-- 10
--
-- Is an implicit formatting to number, but:
--
-- >>> let x = 10 in [fmt|{x:d}|]
--
-- Is an explicit formatting to Integral numbers, using @d@.
--
-- Implicit formatting will only format to either Integral, Fractional or text,
-- and this choice is done by the (open) type family `PyFCategory'.
--
-- This modules also provides 3 type class for formatting.
--
-- - 'PyfFormatFractional' and 'PyfFormatIntegral' are responsible for
-- formatting integral and fractional numbers. Default instances are provided
-- respectively for 'Real' and 'Integral'. 'PyFToString' is in charge of
-- formatting text.
module PyF.Class where

import qualified Data.ByteString
import qualified Data.ByteString.Lazy
import Data.Char (ord)
import Data.Int
import qualified Data.Ratio
import qualified Data.Text as SText
import qualified Data.Text.Encoding as E
import qualified Data.Text.Lazy as LText
import qualified Data.Time
import Data.Word
import Numeric.Natural
import PyF.Formatters

-- * Default formatting classification

-- | The three categories of formatting in 'PyF'
data PyFCategory
  = -- | Format as an integral, no fractional part, precise value
    PyFIntegral
  | -- | Format as a fractional, approximate value with a fractional part
    PyFFractional
  | -- | Format as a string
    PyFString

-- | Classify a type to a 'PyFCategory'
--   This classification will be used to decide which formatting to
--   use when no type specifier in provided.
type family PyFClassify t :: PyFCategory

type instance PyFClassify Integer = 'PyFIntegral

type instance PyFClassify Int = 'PyFIntegral

type instance PyFClassify Int8 = 'PyFIntegral

type instance PyFClassify Int16 = 'PyFIntegral

type instance PyFClassify Int32 = 'PyFIntegral

type instance PyFClassify Int64 = 'PyFIntegral

type instance PyFClassify Natural = 'PyFIntegral

type instance PyFClassify Word = 'PyFIntegral

type instance PyFClassify Word8 = 'PyFIntegral

type instance PyFClassify Word16 = 'PyFIntegral

type instance PyFClassify Word32 = 'PyFIntegral

type instance PyFClassify Word64 = 'PyFIntegral

-- Float numbers

type instance PyFClassify Float = 'PyFFractional

type instance PyFClassify Double = 'PyFFractional

type instance PyFClassify Data.Time.DiffTime = 'PyFFractional

type instance PyFClassify Data.Time.NominalDiffTime = 'PyFFractional

type instance PyFClassify (Data.Ratio.Ratio i) = 'PyFFractional

-- String

type instance PyFClassify String = 'PyFString

type instance PyFClassify LText.Text = 'PyFString

type instance PyFClassify SText.Text = 'PyFString

type instance PyFClassify Data.ByteString.ByteString = 'PyFString

type instance PyFClassify Data.ByteString.Lazy.ByteString = 'PyFString

type instance PyFClassify Char = 'PyFString

-- * String formatting

-- | Convert a type to string
-- This is used for the string formatting.
class PyFToString t where
  pyfToString :: t -> String

instance PyFToString String where pyfToString :: String -> String
pyfToString = String -> String
forall a. a -> a
id

instance PyFToString LText.Text where pyfToString :: Text -> String
pyfToString = Text -> String
LText.unpack

instance PyFToString SText.Text where pyfToString :: Text -> String
pyfToString = Text -> String
SText.unpack

instance PyFToString Data.ByteString.ByteString where pyfToString :: ByteString -> String
pyfToString = Text -> String
SText.unpack (Text -> String) -> (ByteString -> Text) -> ByteString -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Text
E.decodeUtf8

instance PyFToString Data.ByteString.Lazy.ByteString where pyfToString :: ByteString -> String
pyfToString = ByteString -> String
forall t. PyFToString t => t -> String
pyfToString (ByteString -> String)
-> (ByteString -> ByteString) -> ByteString -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
Data.ByteString.Lazy.toStrict

instance PyFToString Char where pyfToString :: Char -> String
pyfToString Char
c = [Char
c]

-- | Default instance. Convert any type with a 'Show instance.
instance {-# OVERLAPPABLE #-} Show t => PyFToString t where pyfToString :: t -> String
pyfToString = t -> String
forall t. Show t => t -> String
show

-- * Real formatting (with optional fractional part)

-- | Apply a fractional formatting to any type.
--
-- A default instance for any 'Real' is provided which internally converts to
-- 'Double', which may not be efficient or results in rounding errors.
--
-- You can provide your own instance and internally use 'formatFractional'
-- which does have the same signatures as 'pyfFormatFractional' but with a
-- 'RealFrac' constraint.
class PyfFormatFractional a where
  pyfFormatFractional ::
    (Integral paddingWidth, Integral precision) =>
    Format t t' 'Fractional ->
    -- | Sign formatting
    SignMode ->
    -- | Padding
    Maybe (paddingWidth, AlignMode k, Char) ->
    -- | Grouping
    Maybe (Int, Char) ->
    -- | Precision
    Maybe precision ->
    a ->
    String

-- | Default instance working for any 'Real'. Internally it converts the type to 'Double'.
instance {-# OVERLAPPABLE #-} Real t => PyfFormatFractional t where
  pyfFormatFractional :: Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> t
-> String
pyfFormatFractional Format t t' 'Fractional
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g Maybe precision
prec t
v = Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> Double
-> String
forall f paddingWidth precision (t :: AltStatus)
       (t' :: UpperStatus) (k :: AlignForString).
(RealFloat f, Integral paddingWidth, Integral precision) =>
Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> f
-> String
formatFractional Format t t' 'Fractional
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g Maybe precision
prec (t -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac @t @Double t
v)

-- | This instance does not do any conversion.
instance PyfFormatFractional Double where pyfFormatFractional :: Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> Double
-> String
pyfFormatFractional = Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> Double
-> String
forall f paddingWidth precision (t :: AltStatus)
       (t' :: UpperStatus) (k :: AlignForString).
(RealFloat f, Integral paddingWidth, Integral precision) =>
Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> f
-> String
formatFractional

-- | This instance does not do any conversion.
instance PyfFormatFractional Float where pyfFormatFractional :: Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> Float
-> String
pyfFormatFractional = Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> Float
-> String
forall f paddingWidth precision (t :: AltStatus)
       (t' :: UpperStatus) (k :: AlignForString).
(RealFloat f, Integral paddingWidth, Integral precision) =>
Format t t' 'Fractional
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Maybe precision
-> f
-> String
formatFractional

-- * Integral formatting

-- | Apply an integral formatting to any type.
--
-- A default instance for any 'Integral' is provided.
--
-- You can provide your own instance and internally use 'formatIntegral'
-- which does have the same signatures as 'pyfFormatIntegral' but with an
-- 'Integral' constraint.
class PyfFormatIntegral i where
  pyfFormatIntegral ::
    Integral paddingWidth =>
    Format t t' 'Integral ->
    -- | Sign formatting
    SignMode ->
    -- | Padding
    Maybe (paddingWidth, AlignMode k, Char) ->
    -- | Grouping
    Maybe (Int, Char) ->
    i ->
    String

-- | Default instance for any 'Integral'.
instance {-# OVERLAPPABLE #-} Integral t => PyfFormatIntegral t where
  pyfFormatIntegral :: Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> t
-> String
pyfFormatIntegral Format t t' 'Integral
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g t
v = Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> t
-> String
forall paddingWidth i (t :: AltStatus) (t' :: UpperStatus)
       (k :: AlignForString).
(Integral paddingWidth, Integral i) =>
Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> i
-> String
formatIntegral Format t t' 'Integral
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g t
v

-- | Returns the numerical value of a 'Char'
-- >>> [fmt|{'a':d}|]
-- 97
instance PyfFormatIntegral Char where
  pyfFormatIntegral :: Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Char
-> String
pyfFormatIntegral Format t t' 'Integral
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g Char
v = Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> Int
-> String
forall paddingWidth i (t :: AltStatus) (t' :: UpperStatus)
       (k :: AlignForString).
(Integral paddingWidth, Integral i) =>
Format t t' 'Integral
-> SignMode
-> Maybe (paddingWidth, AlignMode k, Char)
-> Maybe (Int, Char)
-> i
-> String
formatIntegral Format t t' 'Integral
f SignMode
s Maybe (paddingWidth, AlignMode k, Char)
p Maybe (Int, Char)
g (Char -> Int
ord Char
v)