{-|
Module      : What4.Protocol.PolyRoot
Description : Representation for algebraic reals
Copyright   : (c) Galois Inc, 2016-2020
License     : BSD3
Maintainer  : jhendrix@galois.com

Defines a numeric data-type where each number is represented as the root of a
polynomial over a single variable.

This currently only defines operations for parsing the roots from the format
generated by Yices, and evaluating a polynomial over rational coefficients
to the rational derived from the closest double.
-}

{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
module What4.Protocol.PolyRoot
  ( Root
  , approximate
  , fromYicesText
  , parseYicesRoot
  ) where

import           Control.Applicative
import           Control.Lens
import qualified Data.Attoparsec.Text as Atto
import qualified Data.Map as Map
import           Data.Ratio
import           Data.Text (Text)
import qualified Data.Text as Text

import qualified Data.Vector as V
import           Prettyprinter as PP

atto_angle :: Atto.Parser a -> Atto.Parser a
atto_angle p = Atto.char '<' *> p <* Atto.char '>'

atto_paren :: Atto.Parser a -> Atto.Parser a
atto_paren p = Atto.char '(' *> p <* Atto.char ')'

-- | A polynomial with one variable.
newtype SingPoly coef = SingPoly (V.Vector coef)
  deriving (Functor, Foldable, Traversable, Show)

instance (Ord coef, Num coef, Pretty coef) => Pretty (SingPoly coef) where
  pretty (SingPoly v) =
    case V.findIndex (/= 0) v of
      Nothing -> pretty "0"
      Just j -> go (V.length v - 1)
        where ppc c | c < 0 = parens (pretty c)
                    | otherwise = pretty c

              ppi 1 = pretty "*x"
              ppi i = pretty "*x^" <> pretty i

              go 0 = ppc (v V.! 0)
              go i | seq i False = error "pretty SingPoly"
                   | i == j = ppc (v V.! i) <> ppi i
                   | v V.! i == 0 = go (i-1)
                   | otherwise = ppc (v V.! i) <> ppi i <+> pretty "+" <+> go (i-1)

fromList :: [c] -> SingPoly c
fromList = SingPoly . V.fromList

-- | Create a polyomial from a map from powers to coefficient.
fromMap :: (Eq c, Num c) => Map.Map Int c -> SingPoly c
fromMap m0 = SingPoly (V.generate (n+1) f)
  where m = Map.filter (/= 0) m0
        (n,_) = Map.findMax m
        f i   = Map.findWithDefault 0 i m

-- | Parse a positive monomial
pos_mono :: Integral c => Atto.Parser (c, Int)
pos_mono = (,) <$> Atto.decimal <*> times_x
  where times_x :: Atto.Parser Int
        times_x = (Atto.char '*' *> Atto.char 'x' *> expon) <|> pure 0

        -- Parse explicit exponent or return 1
        expon :: Atto.Parser Int
        expon = (Atto.char '^' *> Atto.decimal) <|> pure 1


-- | Parses a monomial and returns the coefficient and power
mono :: Integral c => Atto.Parser (c, Int)
mono = atto_paren (Atto.char '-' *> (over _1 negate <$> pos_mono))
     <|> pos_mono

parseYicesPoly :: Integral c => Atto.Parser (SingPoly c)
parseYicesPoly = do
     (c,p) <- mono
     go (Map.singleton p c)
  where go m = next m <|> pure (fromMap m)
        next m = seq m $ do
          _ <- Atto.char ' ' *> Atto.char '+' *> Atto.char ' '
          (c,p) <- mono
          go (Map.insertWith (+) p c m)


-- | Evaluate polynomial at a specific value.
--
-- Note that due to rounding, the result may not be exact when using
-- finite precision arithmetic.
eval :: forall c . Num c => SingPoly c -> c -> c
eval (SingPoly v) c = f 0 1 0
  where -- f takes an index, the current power, and the current sum.
        f :: Int -> c -> c -> c
        f i p s
          | seq p $ seq s $ False = error "internal error: Poly.eval"
          | i < V.length v = f (i+1) (p * c) (s + p * (v V.! i))
          | otherwise = s

data Root c = Root { rootPoly :: !(SingPoly c)
                   , rootLbound :: !c
                   , rootUbound :: !c
                   }
  deriving (Show)

-- | Construct a root from a rational constant
rootFromRational :: Num c => c -> Root c
rootFromRational r = Root { rootPoly = fromList [ negate r, 1 ]
                          , rootLbound = r
                          , rootUbound = r
                          }

instance (Ord c, Num c, Pretty c) => Pretty (Root c) where
  pretty (Root p l u) = langle <> pretty p <> comma <+> bounds <> rangle
    where bounds = parens (pretty l <> comma <+> pretty u)

-- | This either returns the root exactly, or it computes the closest double
-- precision approximation of the root.
--
-- Underneath the hood, this uses rational arithmetic to guarantee precision,
-- so this operation is relatively slow.  However, it is guaranteed to provide
-- an exact answer.
--
-- If performance is a concern, there are faster algorithms for computing this.
approximate :: Root Rational -> Rational
approximate r
    | l0 == u0       = l0
    | init_lval == 0 = l0
    | init_uval == 0 = u0
    | init_lval < 0 && init_uval > 0 = bisect (fromRational l0) (fromRational u0)
    | init_lval > 0 && init_uval < 0 = bisect (fromRational u0) (fromRational l0)
    | otherwise = error "Closest root given bad root."
  where p_rat = rootPoly r
        l0 = rootLbound r
        u0 = rootUbound r

        init_lval = eval p_rat l0
        init_uval = eval p_rat u0

        -- bisect takes a value that evaluates to a negative value under the 'p',
        -- and a value that evalautes to a positive value, and runs until it
        -- converges.
        bisect :: Double -> Double -> Rational
        bisect l u   -- Stop if mid point is at bound.
                   | m == l || m == u = toRational $
                      -- Pick whichever bound is cl oser to root.
                      if l_val <= u_val then l else u
                   | m_val == 0 = toRational m -- Stop if mid point is exact root.
                   | m_val <  0 = bisect m u -- Use mid point as new lower bound
                   | otherwise  = bisect l m -- Use mid point as new upper bound.
          where m = (l + u) / 2
                m_val = eval p_rat (toRational m)
                l_val = abs (eval p_rat (toRational l))
                u_val = abs (eval p_rat (toRational u))


atto_pair :: (a -> b -> r) -> Atto.Parser a -> Atto.Parser b -> Atto.Parser r
atto_pair f x y = f <$> x <*> (Atto.char ',' *> Atto.char ' ' *> y)

atto_sdecimal :: Integral c => Atto.Parser c
atto_sdecimal = Atto.char '-' *> (negate <$> Atto.decimal)
              <|> Atto.decimal

atto_rational :: Integral c => Atto.Parser (Ratio c)
atto_rational = (%) <$> atto_sdecimal <*> denom
  where denom = (Atto.char '/' *> Atto.decimal) <|> pure 1

parseYicesRoot :: Atto.Parser (Root Rational)
parseYicesRoot = atto_angle (atto_pair mkRoot (fmap fromInteger <$> parseYicesPoly) parseBounds)
             <|> (rootFromRational <$> atto_rational)
  where mkRoot :: SingPoly c -> (c, c) -> Root c
        mkRoot = uncurry . Root
        parseBounds :: Atto.Parser (Rational, Rational)
        parseBounds = atto_paren (atto_pair (,) atto_rational atto_rational)

-- | Convert text to a root
fromYicesText :: Text -> Maybe (Root Rational)
fromYicesText t = resolve (Atto.parse parseYicesRoot t)
  where resolve (Atto.Fail _rem _ _msg) = Nothing
        resolve (Atto.Partial f) =
          resolve (f Text.empty)
        resolve (Atto.Done i r)
          | Text.null i = Just $! r
          | otherwise = Nothing
