-- |
-- Module      : Math.Diophantine.Parser
-- Copyright   : Joe Jevnik 2013
--
-- License     : GPL-2
-- Maintainer  : joejev@gmail.org
-- Stability   : stable
-- Portability : GHC
--
-- A module for parsing 'Equation's.

module Math.Diophantine.Parser
    ( ReadError(..)     -- instances: Show
    , readEquation      -- :: String -> Either ReadError Equation
    , readMaybeEquation -- :: String -> Maybe Equation
    ) where

import Data.List                 (isInfixOf,find)
import Data.Maybe                (fromMaybe,isNothing)
import Math.Diophantine.Internal
import Text.Read                 (readMaybe)

-- -------------------------------------------------------------------------- --
-- Data.

-- | A type representing a token to be read.
data Token = Token { match :: String
                   , value :: Either ReadError Integer
                   } deriving Show

-- | A type to represent any errors that may occur in reading a the equation.
data ReadError = UnexpectedChar Char deriving Show

-- -------------------------------------------------------------------------- --
-- Internal parsing functions.

-- | Reads one token out of the string, returning the value, and the rest of the
-- string.
getToken :: String -> (Token,String)
getToken str = let (t,rs) = span (`notElem` "xy=") str
                in getToken' (t,rs)
  where
      getToken' (t,'x':'y':rs) = (Token "xy" $ readT t,rs)
      getToken' (t,v:'^':p:rs) = (Token [v,'^',p] $ readT t,rs)
      getToken' (t,[])       = (Token "" $ readT t,[])
      getToken' (t,v:rs)     = (Token [v] $ readT t,rs)
      readT = readWithError . filter (`notElem`" +")

-- | Builds a list of tokens for the equation.
tokenize :: String -> [Token]
tokenize []  = []
tokenize str = let (t,s) = getToken str
               in t:tokenize s

-- | A way to make verify that the 'String's are proper, and also report any
-- 'ReadError's.
readWithError :: String -> Either ReadError Integer
readWithError ""  = Right 1
readWithError str
    | null $ filter (`elem` "0123456789-") str = Right (-1)
    | otherwise = case find (`notElem` "0123456789-") str of
                      Nothing -> Right $ read str
                      Just c  -> Left $ UnexpectedChar c

-- | WARNING: Throws error: extracts the 'Integer' value from the value of a
-- token. Only used after it is assured it is NOT a 'ReadError'.
getVal :: Either ReadError Integer -> Integer
getVal (Left _)  = error "Parsing error when filtering errors."
getVal (Right n) = n

-- | WARNING: Throws error: extracts the 'ReadError' value from the value of a
-- token. Only used after it is assured it is NOT an 'Integer'.
getErr :: Either ReadError Integer -> ReadError
getErr (Right _) = error "Parse error when finding errors."
getErr (Left e)  = e

-- | Gets the coefficient for the given term out of the list of tokens.
getC :: (String -> Bool) -> [Token] -> Either ReadError Integer
getC b ts = value $ fromMaybeTok $ find (\t -> b $ match t) ts

-- | a fromMaybe specialized to 'Token's.
fromMaybeTok :: Maybe Token -> Token
fromMaybeTok (Just (Token s v)) = Token s v
fromMaybeTok Nothing            = Token "" (Right 0)

-- -------------------------------------------------------------------------- --
-- Exported functions.

-- | Reads an 'Equation' as a 'String'. Equations must be set equal to 0.
-- Order of the terms does not matter expect for constant term, and " = 0",
-- which should come last if given. Returns 'Either ReadError Equation'
--
-- >>> readEquation "-5y + 2x - 3xy + 2"
-- -3xy + 2x + -5y + 2 = 0
-- >>> readEquation  "2xy + x + 1 = 0"
-- 2xy + x + 1 = 0
readEquation :: String -> Either ReadError Equation
readEquation str = let ts = tokenize str
                       a  = getC ("x^2" ==) ts
                       b  = getC ("xy"  ==) ts
                       c  = getC ("y^2" ==) ts
                       d  = getC ("x"   ==) ts
                       e  = getC ("y"   ==) ts
                       f  = getC null       ts
                       g  = find (\n -> case n of
                                            Left _ -> True
                                            _      -> False) [a,b,c,d,e,f]
                   in maybe (Right $ GeneralEquation (getVal a) (getVal b)
                             (getVal c) (getVal d) (getVal e) (getVal f))
                          (Left . getErr) g

-- | Like readEquation; however, it does not return any errors.
readMaybeEquation :: String -> Maybe Equation
readMaybeEquation str = let ts = tokenize str
                            a  =  getC ("x^2" ==) ts
                            b  = getC ("xy"  ==) ts
                            c  = getC ("y^2" ==) ts
                            d  = getC ("x"   ==) ts
                            e  = getC ("y"   ==) ts
                            f  = getC null       ts
                            g  = find (\n -> case n of
                                                 Left _ -> True
                                                 _      -> False) [a,b,c,d,e,f]
                        in if isNothing g
                             then Just $ GeneralEquation (getVal a) (getVal b)
                                      (getVal c) (getVal d) (getVal e)
                                                     (getVal f)
                             else Nothing