{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE TypeFamilies          #-}
-- | Representation of a JSON number and its various components.
module Waargonaut.Types.JNumber
  (
    -- * Types
    JNumber (..)
  , HasJNumber (..)
  , E (..)
  , AsE (..)
  , Frac (..)
  , Exp (..)
  , HasExp (..)
  , JInt
  , JInt' (..)

    -- * Prisms
  , _JZero
  , _JIntInt
  , _JNumberInt
  , _JNumberScientific

    -- * Parser / Builder
  , jNumberBuilder
  , parseJNumber

    -- * Other
  , jNumberToScientific
  ) where

import           Prelude                    (Bool (..), Eq, Int, Integral, Ord,
                                             Show, abs, fromIntegral, maxBound,
                                             minBound, negate, (-), (<), (>),
                                             (||))

import           Data.Scientific            (Scientific)
import qualified Data.Scientific            as Sci

import           Control.Category           (id, (.))
import           Control.Lens               (Lens', Prism', Rewrapped,
                                             Wrapped (..), iso, prism, ( # ),
                                             (^?), _Just, _Wrapped)

import           Control.Applicative        (pure, (*>), (<$), (<$>), (<*>))
import           Control.Monad              (Monad, (=<<))

import           Control.Error.Util         (note)

import           Data.Either                (Either (..))
import           Data.Function              (const, ($))
import           Data.Functor               (fmap)
import           Data.Maybe                 (Maybe (..), fromMaybe, isJust,
                                             maybe)
import           Data.Monoid                (mappend, mempty)
import           Data.Semigroup             ((<>))
import           Data.Traversable           (traverse)
import           Data.Tuple                 (uncurry)

import           Data.List.NonEmpty         (NonEmpty ((:|)), some1)
import qualified Data.List.NonEmpty         as NE

import           Data.Foldable              (asum, foldMap, length)

import           Data.Digit                 (DecDigit)
import qualified Data.Digit                 as D

import           Text.Parser.Char           (CharParsing, char)
import           Text.Parser.Combinators    (many, optional)

import           Data.Text.Lazy.Builder     (Builder)
import qualified Data.Text.Lazy.Builder     as TB
import qualified Data.Text.Lazy.Builder.Int as TB

-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Control.Monad (return)
-- >>> import Prelude (read)
-- >>> import Data.Either(Either (..), isLeft)
-- >>> import Data.List.NonEmpty (NonEmpty ((:|)))
-- >>> import Data.Digit (DecDigit(..))
-- >>> import qualified Data.Digit as D
-- >>> import Waargonaut.Decode.Error (DecodeError)
-- >>> import Data.ByteString.Lazy (toStrict)
-- >>> import Data.Text.Lazy.Builder (toLazyText)
-- >>> import Utils

-- | Represent a JSON "int"
data JInt' digit
  = JZero
  | JIntInt digit [DecDigit]
  deriving (Eq, Ord, Show)

-- | Type alias to allow us to constrain the first 'digit' type.
type JInt = JInt' DecDigit

-- | Prism for JSON zeroes.
_JZero :: Prism' JInt ()
_JZero = prism (const JZero)
  (\case
      JZero -> Right ()
      x     -> Left x
  )

-- | Prism for JSON non-zero values.
_JIntInt :: Prism' (JInt' digit) (digit, [DecDigit])
_JIntInt = prism (uncurry JIntInt)
  (\case
      JIntInt d ds -> Right (d,ds)
      x -> Left x
  )
-- | The textual exponent character may be upper or lower case, we maintain this
-- fact using this type.
data E
  = EE
  | Ee
  deriving (Eq, Ord, Show)

-- | Typeclass for things that may represent a upper or lower case exponent character.
class AsE r where
  _E  :: Prism' r E
  _EE :: Prism' r ()
  _Ee :: Prism' r ()
  _EE = _E . _EE
  _Ee = _E . _Ee

instance AsE E where
  _E = id
  _EE = prism (const EE)
    (\x -> case x of
        EE -> Right ()
        _  -> Left x
    )
  _Ee = prism (const Ee)
    (\x -> case x of
        Ee -> Right ()
        _  -> Left x
    )

-- | The fractional component of a JSON numeric value
newtype Frac = Frac (NonEmpty DecDigit)
  deriving (Eq, Ord, Show)

instance Frac ~ t => Rewrapped Frac t
instance Wrapped Frac where
  type Unwrapped Frac = NonEmpty DecDigit
  _Wrapped' = iso (\ (Frac x) -> x) Frac

-- | The exponent part of a JSON numeric value
data Exp = Exp
  { _ex        :: E
  , _minusplus :: Maybe Bool
  , _expdigits :: NonEmpty DecDigit
  }
  deriving (Eq, Ord, Show)

-- | Typeclass for things that may have an 'Exp' component.
class HasExp c where
  exp :: Lens' c Exp
  ex :: Lens' c E
  {-# INLINE ex #-}
  expdigits :: Lens' c (NonEmpty DecDigit)
  {-# INLINE expdigits #-}
  minusplus :: Lens' c (Maybe Bool)
  {-# INLINE minusplus #-}
  ex = exp . ex
  expdigits = exp . expdigits
  minusplus = exp . minusplus

instance HasExp Exp where
  {-# INLINE ex #-}
  {-# INLINE expdigits #-}
  {-# INLINE minusplus #-}
  exp = id
  ex f (Exp x1 x2 x3) = fmap (\ y1 -> Exp y1 x2 x3) (f x1)
  expdigits f (Exp x1 x2 x3) = fmap (Exp x1 x2) (f x3)
  minusplus f (Exp x1 x2 x3) = fmap (\ y1 -> Exp x1 y1 x3) (f x2)

-- | JSON Number type.
data JNumber = JNumber
  { _minus     :: Bool
  , _numberint :: JInt
  , _frac      :: Maybe Frac
  , _expn      :: Maybe Exp
  }
  deriving (Eq, Ord, Show)

-- | Typeclass for things that may have a 'JNumber'.
class HasJNumber c where
  jNumber   :: Lens' c JNumber
  expn      :: Lens' c (Maybe Exp)
  {-# INLINE expn #-}
  frac      :: Lens' c (Maybe Frac)
  {-# INLINE frac #-}
  minus     :: Lens' c Bool
  {-# INLINE minus #-}
  numberint :: Lens' c JInt
  {-# INLINE numberint #-}
  expn      = jNumber . expn
  frac      = jNumber . frac
  minus     = jNumber . minus
  numberint = jNumber . numberint

instance HasJNumber JNumber where
  {-# INLINE expn #-}
  {-# INLINE frac #-}
  {-# INLINE minus #-}
  {-# INLINE numberint #-}
  jNumber = id
  expn f (JNumber x1 x2 x3 x4)      = fmap (JNumber x1 x2 x3) (f x4)
  frac f (JNumber x1 x2 x3 x4)      = fmap (\ y1 -> JNumber x1 x2 y1 x4) (f x3)
  minus f (JNumber x1 x2 x3 x4)     = fmap (\ y1 -> JNumber y1 x2 x3 x4) (f x1)
  numberint f (JNumber x1 x2 x3 x4) = fmap (\ y1 -> JNumber x1 y1 x3 x4) (f x2)

-- | Prism between a JNumber and a Haskell 'Int'. This prism will go via the
-- 'Scientific' type to handle the various exponent and fractional values before
-- attempting to convert it to a bounded integer.
_JNumberInt :: Prism' JNumber Int
_JNumberInt = prism jnumberToInt (\v -> note v $ Sci.toBoundedInteger =<< jNumberToScientific v)
  where
    jnumberToInt i = JNumber (i < 0) (mkjInt $ abs i) Nothing Nothing

mkjInt :: Integral a => a -> JInt' DecDigit
mkjInt 0 = JZero
mkjInt n = (\(h :| t) -> JIntInt h t) $ D._NaturalDigits # fromIntegral n

-- | Prism for trying to move between 'JNumber' and 'Scientific'
--
-- >>> _JNumberScientific # (read "-3.45e-2")
-- JNumber {_minus = True, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = DecDigit2 :| []})}
--
-- >>> _JNumberScientific # (read "-1.23456e-787")
-- JNumber {_minus = True, _numberint = JIntInt DecDigit1 [], _frac = Just (Frac (DecDigit2 :| [DecDigit3,DecDigit4,DecDigit5,DecDigit6])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = DecDigit7 :| [DecDigit8,DecDigit7]})}
--
-- >>> _JNumberScientific # (read "-1.23456e791")
-- JNumber {_minus = True, _numberint = JIntInt DecDigit1 [], _frac = Just (Frac (DecDigit2 :| [DecDigit3,DecDigit4,DecDigit5,DecDigit6])), _expn = Just (Exp {_ex = Ee, _minusplus = Just False, _expdigits = DecDigit7 :| [DecDigit9,DecDigit1]})}
--
_JNumberScientific :: Prism' JNumber Scientific
_JNumberScientific = prism toJNum (\v -> note v $ jNumberToScientific v)
  where
    toJNum s =
      let (is, e) = Sci.toDecimalDigits (abs s)
          sign = s < 0
      in case is of
        [] -> JNumber sign JZero Nothing Nothing
        [0] -> JNumber sign JZero Nothing Nothing
        [d] -> JNumber sign (mkjInt d) Nothing Nothing
        (d:ds) -> JNumber sign (mkjInt d)
          (fmap Frac $ NE.nonEmpty =<< traverse (^? D.integralDecimal) ds)
          (Just $ Exp Ee (Just $ e < 0) (D._NaturalDigits # fromIntegral (abs (e - 1))))

-- | Parse the integer component of a JSON number.
--
-- >>> testparse parseJInt "1"
-- Right (JIntInt DecDigit1 [])
--
-- >>> testparse parseJInt "9"
-- Right (JIntInt DecDigit9 [])
--
-- >>> testparse parseJInt "10"
-- Right (JIntInt DecDigit1 [DecDigit0])
--
-- >>> testparse parseJInt "39"
-- Right (JIntInt DecDigit3 [DecDigit9])
--
-- >>> testparse parseJInt "393564"
-- Right (JIntInt DecDigit3 [DecDigit9,DecDigit3,DecDigit5,DecDigit6,DecDigit4])
--
-- >>> testparse parseJInt "0"
-- Right JZero
--
-- >>> testparsethennoteof parseJInt "00"
-- Right JZero
--
-- >>> testparsethennoteof parseJInt "01"
-- Right JZero
--
-- >>> testparsetheneof parseJInt "1"
-- Right (JIntInt DecDigit1 [])
--
-- >>> testparsetheneof parseJInt "9"
-- Right (JIntInt DecDigit9 [])
--
-- >>> testparsetheneof parseJInt "10"
-- Right (JIntInt DecDigit1 [DecDigit0])
--
-- >>> testparsetheneof parseJInt "39"
-- Right (JIntInt DecDigit3 [DecDigit9])
--
-- >>> testparsetheneof parseJInt "393564"
-- Right (JIntInt DecDigit3 [DecDigit9,DecDigit3,DecDigit5,DecDigit6,DecDigit4])
--
-- >>> testparsetheneof parseJInt "0"
-- Right JZero
--
-- >>> isLeft (testparse parseJInt "x")
-- True
--
-- >>> isLeft (testparse parseJInt "")
-- True
parseJInt ::
  (Monad f, CharParsing f) =>
  f JInt
parseJInt =
  asum [
    JZero <$ char '0'
  , JIntInt <$> D.parseDecimalNoZero <*> many D.parseDecimal
  ]

-- | Parse the exponent portion of a JSON number.
--
-- >>> testparse parseE "e"
-- Right Ee
--
-- >>> testparse parseE "E"
-- Right EE
--
-- >>> testparsetheneof parseE "e"
-- Right Ee
--
-- >>> testparsetheneof parseE "E"
-- Right EE
--
-- >>> isLeft (testparsetheneof parseE "x")
-- True
--
-- >>> testparsethennoteof parseE "ea"
-- Right Ee
--
-- >>> testparsethennoteof parseE "Ea"
-- Right EE
parseE ::
  CharParsing f =>
  f E
parseE =
  asum [
    Ee <$ char 'e'
  , EE <$ char 'E'
  ]

eBuilder
  :: E
  -> Builder
eBuilder Ee = TB.singleton 'e'
eBuilder EE = TB.singleton 'E'

-- | Parse the fractional component of a JSON number.
--
-- >>> testparsetheneof parseFrac "1"
-- Right (Frac (DecDigit1 :| []))
--
-- >>> testparsetheneof parseFrac "9"
-- Right (Frac (DecDigit9 :| []))
--
-- >>> testparsetheneof parseFrac "10"
-- Right (Frac (DecDigit1 :| [DecDigit0]))
--
-- >>> testparsetheneof parseFrac "39"
-- Right (Frac (DecDigit3 :| [DecDigit9]))
--
-- >>> testparsetheneof parseFrac "393564"
-- Right (Frac (DecDigit3 :| [DecDigit9,DecDigit3,DecDigit5,DecDigit6,DecDigit4]))
--
-- >>> testparsetheneof parseFrac "0"
-- Right (Frac (DecDigit0 :| []))
--
-- >>> testparsetheneof parseFrac "00"
-- Right (Frac (DecDigit0 :| [DecDigit0]))
--
-- >>> testparsetheneof parseFrac "01"
-- Right (Frac (DecDigit0 :| [DecDigit1]))
--
-- >>> testparsethennoteof parseFrac "01x"
-- Right (Frac (DecDigit0 :| [DecDigit1]))
parseFrac ::
  (Monad f, CharParsing f) =>
  f Frac
parseFrac =
  Frac <$> some1 D.parseDecimal

-- | Builder for the fractional component.
fracBuilder
  :: Frac
  -> Builder
fracBuilder (Frac digs) =
  digitsBuilder digs

-- | Parse the full exponent portion of a JSON number.
--
-- >>> testparsethen parseExp "e10x"
-- Right (Exp {_ex = Ee, _minusplus = Nothing, _expdigits = DecDigit1 :| [DecDigit0]},'x')
--
-- >>> testparsethen parseExp "e+10x"
-- Right (Exp {_ex = Ee, _minusplus = Just False, _expdigits = DecDigit1 :| [DecDigit0]},'x')
--
-- >>> testparsethen parseExp "e-0x"
-- Right (Exp {_ex = Ee, _minusplus = Just True, _expdigits = DecDigit0 :| []},'x')
--
-- >>> testparsethen parseExp "E-1x"
-- Right (Exp {_ex = EE, _minusplus = Just True, _expdigits = DecDigit1 :| []},'x')
parseExp ::
  (Monad f, CharParsing f) =>
  f Exp
parseExp = Exp
  <$> parseE
  <*> optional (asum [False <$ char '+', True <$ char '-'])
  <*> some1 D.parseDecimal

-- | Helper to provide the right symbol for the sign of the exponent.
getExpSymbol
  :: Maybe Bool
  -> Builder
getExpSymbol (Just True)  = TB.singleton '-'
getExpSymbol (Just False) = TB.singleton '+'
getExpSymbol _            = mempty

-- | Builder for a list of digits.
digitsBuilder
  :: NonEmpty DecDigit
  -> Builder
digitsBuilder =
  foldMap (int8 . (D.integralDecimal #))
  where
    int8 :: Int -> Builder
    int8 = TB.decimal

-- | Builder for the exponent portion.
expBuilder
  :: Exp
  -> Builder
expBuilder (Exp e sign digs) =
  eBuilder e <> getExpSymbol sign <> digitsBuilder digs

-- | Parse a JSON numeric value.
--
-- >>> testparsethen parseJNumber "600x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit6 [DecDigit0,DecDigit0], _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "800x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit8 [DecDigit0,DecDigit0], _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "3x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit3 [], _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "-3x"
-- Right (JNumber {_minus = True, _numberint = JIntInt DecDigit3 [], _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "0x"
-- Right (JNumber {_minus = False, _numberint = JZero, _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "-0x"
-- Right (JNumber {_minus = True, _numberint = JZero, _frac = Nothing, _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "3.45x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "-3.45x"
-- Right (JNumber {_minus = True, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Nothing},'x')
--
-- >>> testparsethen parseJNumber "3.45e10x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Nothing, _expdigits = DecDigit1 :| [DecDigit0]})},'x')
--
-- >>> testparsethen parseJNumber "3e10x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit3 [], _frac = Nothing, _expn = Just (Exp {_ex = Ee, _minusplus = Nothing, _expdigits = DecDigit1 :| [DecDigit0]})},'x')
--
-- >>> testparsethen parseJNumber "3.45e+10x"
-- Right (JNumber {_minus = False, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just False, _expdigits = DecDigit1 :| [DecDigit0]})},'x')
--
-- >>> testparsethen parseJNumber "-3.45e-02x"
-- Right (JNumber {_minus = True, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (DecDigit4 :| [DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = DecDigit0 :| [DecDigit2]})},'x')
--
-- >>> isLeft (testparsethen parseJNumber "-3.45ex")
-- True
--
-- >>> isLeft (testparsethen parseJNumber "-.45e1x")
-- True
parseJNumber ::
  (Monad f, CharParsing f) =>
  f JNumber
parseJNumber = JNumber
  . isJust <$> optional (char '-')
  <*> parseJInt
  <*> optional (char '.' *> parseFrac)
  <*> optional parseExp

-- | Printing of JNumbers
--
-- >>> toLazyText $ jNumberBuilder (JNumber {_minus = False, _numberint = JIntInt D.DecDigit3 [], _frac = Just (Frac (D.DecDigit4 :| [D.DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just False, _expdigits = D.DecDigit1 :| [D.DecDigit0]})})
-- "3.45e+10"
--
-- >>> toLazyText $ jNumberBuilder (JNumber {_minus = True, _numberint = JIntInt D.DecDigit3 [], _frac = Just (Frac (D.DecDigit4 :| [D.DecDigit5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = D.DecDigit0 :| [D.x2]})})
-- "-3.45e-02"
--
-- >>> toLazyText $ jNumberBuilder (JNumber {_minus = False, _numberint = JIntInt D.DecDigit0 [D.DecDigit0], _frac = Nothing, _expn = Nothing})
-- "00"
--
jNumberBuilder
  :: JNumber
  -> Builder
jNumberBuilder (JNumber sign digs mfrac mexp) =
  s <> digits <> frac' <> expo
  where
    s      = if sign then TB.singleton '-' else mempty
    digits = digitsBuilder . jIntToDigits $ digs
    frac'  = foldMap (mappend (TB.singleton '.') . fracBuilder) mfrac
    expo   = foldMap expBuilder mexp

-- | Returns a normalised 'Scientific' value or Nothing if the exponent
--   is out of the range @[minBound,maxBound::Int]@
--
-- >>> jNumberToScientific JNumber {_minus = True, _numberint = JIntInt DecDigit3 [], _frac = Just (Frac (D.x4 :| [D.x5])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = D.x0 :| [D.x2]})}
-- Just -3.45e-2
--
-- >>> jNumberToScientific JNumber {_minus = True, _numberint = JIntInt D.x1 [D.x2, D.x3], _frac = Just (Frac (D.x4 :| [D.x5, D.x6])), _expn = Just (Exp {_ex = Ee, _minusplus = Just True, _expdigits = (D.x7 :| [D.x8, D.x9])})}
-- Just -1.23456e-787
--
-- >>> jNumberToScientific JNumber {_minus = True, _numberint = JIntInt D.x1 [D.x2, D.x3], _frac = Just (Frac (D.x4 :| [D.x5, D.x6])), _expn = Just (Exp {_ex = Ee, _minusplus = Just False, _expdigits = (D.x7 :| [D.x8, D.x9])})}
-- Just -1.23456e791
--
jNumberToScientific :: JNumber -> Maybe Scientific
jNumberToScientific (JNumber sign int mfrac mexp) =
  if expon > fromIntegral (maxBound :: Int) ||
     expon < fromIntegral (minBound :: Int)
  then Nothing
  else Sci.scientific <$> coeff <*> pure expon
  where
    natToNeg s = fmap ( neg s . fromIntegral )

    intDigs       = jIntToDigits int

    fracList      = mfrac ^? _Just . _Wrapped
    exponentShift = maybe 0 length fracList

    coeff         = natToNeg
                      (Just sign)
                      (D.digitsToNatural $ maybe intDigs (intDigs <>) fracList)

    expon         = fromMaybe 0 ( expval =<< mexp ) - fromIntegral exponentShift

    neg (Just True) = negate
    neg _           = id

    expval (Exp _ msign digs) = natToNeg msign (D.digitsToNatural digs)

-- | Helper to convert a 'JInt' to a 'NonEmpty' list of component digits.
jIntToDigits :: JInt -> NonEmpty DecDigit
jIntToDigits JZero          = D.x0 NE.:| []
jIntToDigits (JIntInt d ds) = d NE.:| ds