{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE LambdaCase #-}

-- | Parsers for fractions.
module Data.Textual.Fractional
  (
  -- * Positional numeral systems
    PositionalSystem(..)
  , Binary(..)
  , Octal(..)
  , Decimal(..)
  , Hexadecimal(..)
  , UpHex(..)
  , LowHex(..)
  -- * Sign
  , Sign(..)
  , applySign
  , optMinus
  , optSign
  -- * Optionality characteristic
  , Optional(..)
  , isOptional
  , isRequired
  -- * Fraction parsers
  , optSlash
  , fraction'
  , fraction
  -- * s-fraction parsers
  , decExpSign
  , hexExpSign
  , fractional'
  , fractional
  ) where

import Data.Maybe (isJust)
import Data.Ratio ((%))
import Control.Applicative
import Text.Printer.Fractional (Optional(..), isOptional, isRequired)
import Text.Parser.Combinators ((<?>), unexpected)
import Text.Parser.Char (CharParsing)
import qualified Text.Parser.Char as PC
import Data.Textual.Integral

-- | Accept a slash and return 'Required'. Otherwise return 'Optional'.
optSlash  (Monad μ, CharParsing μ)  μ Optional
optSlash = maybe Optional (const Required) <$> optional (PC.char '/')

-- | Parse a fraction. The numerator and the denominator are expected to be
--   written in the specified positional numeral system.
fraction'  (PositionalSystem s, Fractional α, Monad μ, CharParsing μ)
           μ Sign -- ^ Sign parser
           s
           μ Optional -- ^ Numerator/denominator separator parser
           μ α
fraction' neg s den = (<?> "fraction") $ do
  n  number' neg s <?> "numerator"
  den >>= \case
    Optional 
      return $ fromInteger n
    Required  do
      d  (<?> "denominator") $ do
        d  nonNegative s
        if d == 0 then unexpected "zero denominator"
                  else return d
      return $ fromRational $ n % d

-- | A shorthand for 'fraction'' 'optMinus' 'Decimal' 'optSlash'.
fraction  (Fractional α, Monad μ, CharParsing μ)  μ α
fraction = fraction' optMinus Decimal optSlash

-- | Start of a decimal exponent. Accepts /'e'/ or /'E'/ followed by
--   an optional sign. Otherwise 'Nothing' is returned.
decExpSign  (Monad μ, CharParsing μ)  μ (Maybe Sign)
decExpSign = optional (PC.oneOf "eE") >>= \case
               Nothing  return Nothing
               Just _   Just <$> optSign

-- | Start of a hexadecimal exponent. Accepts /'p'/ or /'P'/ followed by
--   an optional sign. Otherwise 'Nothing' is returned.
hexExpSign  (Monad μ, CharParsing μ)  μ (Maybe Sign)
hexExpSign = optional (PC.oneOf "pP") >>= \case
               Nothing  return Nothing
               Just _   Just <$> optSign

-- | /s/-fraction parser.
fractional'  (PositionalSystem s, Fractional α, Monad μ, CharParsing μ)
             μ Sign -- ^ Sign parser.
             s
             Optional -- ^ Whether the integer part is optional.
             μ () -- ^ Dot parser.
             μ (Maybe Sign) -- ^ Exponent start parser.
             μ α
fractional' neg s ip dot eneg = (<?> (systemName s ++ "-fraction")) $ do
    sign  neg <?> "sign"
    (i, f, fDigits)  do
      let integral = do
            i  nonNegative s <?> "integer part"
            ((i, ) . isJust) <$> optional dot
      (i, hasF)  case ip of
        Optional  optional dot >>= \case
          Nothing  integral
          Just _  return (0, True)
        Required  integral
      (f, fDigits) 
        if hasF
        then do
          let go !ds !f = optional digit >>= \case
                            Just d   go (ds + 1) (f * radix + d)
                            Nothing  return (f, ds) 
          digit >>= go (1  Int) <?> "fractional part"
        else
          return (0, 0)
      return (i, f, fDigits)
    (<?> "exponent") $ eneg >>= \case
      Nothing | f == 0     return $ fromInteger $ applySign sign i
              | otherwise  return $ fromRational
                                   $ applySign sign
                                   $ fromInteger i + f % radix ^ fDigits
      Just esign  do
        e  nnBounded Decimal
        return $ applySign sign $ case esign of
          NonNegative  case e - fDigits of
            e₁ | e₁ >= 0    fromInteger $ i * radix ^ e + f * radix ^ e₁
               | otherwise  fromRational
                           $ fromInteger (i * radix ^ e)
                           + i % radix ^ negate e₁
          NonPositive  fromRational
                      $ i % (radix ^ e) + f % radix ^ (fDigits + e)
  where 
    radix = radixIn s
    digit = digitIn s

-- | Decimal fraction parser.
fractional  (Monad μ, Fractional α, CharParsing μ)  μ α
fractional = fractional' optMinus Decimal Required
                         (PC.char '.' *> pure ()) decExpSign