{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Boolean algebra on encodings
--
-- @since 0.2.1.0
-- 
-- == Grammar
-- 
-- Simple grammar requires boolean terms to be included in parentheses
--
-- @
-- bool[BinaryOp]:(leftTerm)(rightTerm)
-- bool[UnaryOp]:(term)
-- @
--
-- Expected behavior is described next to corresponding combinator.
--
-- Typeclass encoding is not used to avoid instance overlapping.
-- 
-- Use 'Data.TypedEncoding.Combinators.Restriction.Common.recWithEncR' 
-- to create manual recovery step that can be combined with 'recreateFPart'.

-- This is very much in beta state.
--
module Data.TypedEncoding.Combinators.Restriction.Bool where


import           GHC.TypeLits
import           Data.Proxy
import           Data.Symbol.Ascii

import           Data.TypedEncoding
import           Data.TypedEncoding.Instances.Support
import           Data.TypedEncoding.Internal.Util.TypeLits
import           Data.TypedEncoding.Combinators.Restriction.Common

-- import qualified Data.Text as T
-- import           Data.TypedEncoding.Instances.Restriction.Common()


-- $setup
-- >>> :set -XOverloadedStrings -XMultiParamTypeClasses -XDataKinds -XTypeApplications
-- >>> import qualified Data.Text as T
-- >>> import           Data.TypedEncoding.Instances.Restriction.Common()


-- |
-- See examples in 'encBoolOrRight''
encBoolOrLeft :: forall f s t xs c str . (
    BoolOpIs s "or" ~ 'True
    -- IsBoolOr s ~ 'True
    , Functor f
    , LeftTerm s ~ t
    ) => (Enc xs c str -> f (Enc (t ': xs) c str)) -> Enc xs c str -> f (Enc (s ': xs) c str)
encBoolOrLeft = implChangeAnn

-- |
-- See examples in 'encBoolOrRight''
encBoolOrLeft' :: forall f s t xs c str . (
    BoolOpIs s "or" ~ 'True
    -- IsBoolOr s ~ 'True
    , Functor f
    , LeftTerm s ~ t
    , EncodeF f (Enc xs c str) (Enc (t ': xs) c str)
    ) => Enc xs c str -> f (Enc (s ': xs) c str)
encBoolOrLeft' = encBoolOrLeft (encodeF @f @(Enc xs c str) @(Enc (t ': xs) c str))

-- |
-- 
encBoolOrRight :: forall f s t xs c str . (
    BoolOpIs s "or" ~ 'True
    -- IsBoolOr s ~ 'True
    , Functor f
    , RightTerm s ~ t
    ) => (Enc xs c str -> f (Enc (t ': xs) c str)) -> Enc xs c str -> f (Enc (s ': xs) c str)
encBoolOrRight = implChangeAnn

-- |
-- >>> :{ 
-- let tst1, tst2, tst3 :: Either EncodeEx (Enc '["boolOr:(r-Word8-decimal)(r-Int-decimal)"] () T.Text)
--     tst1 = encBoolOrLeft' . toEncoding () $ "212" 
--     tst2 = encBoolOrRight' . toEncoding () $ "1000000" 
--     tst3 = encBoolOrLeft' . toEncoding () $ "1000000"
-- :}
-- 
-- >>> tst1 
-- Right (MkEnc Proxy () "212")
--
-- >>> tst2
-- Right (MkEnc Proxy () "1000000")
--
-- >>> tst3
-- Left (EncodeEx "r-Word8-decimal" ("Payload does not satisfy format Word8-decimal: 1000000"))
encBoolOrRight' :: forall f s t xs c str . (
    BoolOpIs s "or" ~ 'True
    -- IsBoolOr s ~ 'True
    , Functor f
    , RightTerm s ~ t
    , EncodeF f (Enc xs c str) (Enc (t ': xs) c str)
    ) => Enc xs c str -> f (Enc (s ': xs) c str)
encBoolOrRight' = encBoolOrRight (encodeF @f @(Enc xs c str) @(Enc (t ': xs) c str))

encBoolAnd :: forall f s t1 t2 xs c str . (
    BoolOpIs s "and" ~ 'True
    , KnownSymbol s
    -- IsBoolAnd s ~ 'True
    , f ~ Either EncodeEx
    , Eq str
    , LeftTerm s ~ t1
    , RightTerm s ~ t2
    ) =>
    (Enc xs c str -> f (Enc (t1 ': xs) c str))
    -> (Enc xs c str -> f (Enc (t2 : xs) c str))
    -> Enc xs c str -> f (Enc (s ': xs) c str)
encBoolAnd fnl fnr en0 =
       let
           eent1 = fnl en0
           eent2 = fnr en0
           p = (Proxy :: Proxy s)
       in
           case (eent1, eent2) of
               (Right ent1, Right ent2) ->
                   if getPayload ent1 == getPayload ent2
                   then Right . withUnsafeCoerce id $ ent1
                   else Left $ EncodeEx p "Left - right encoding do not match"
               (_, _) -> mergeErrs (emptyEncErr p) (mergeEncodeEx p) eent1 eent2

-- |
-- @"boolOr:(enc1)(enc2)"@ contains strings that encode the same way under both encodings.
-- for example  @"boolOr:(r-UPPER)(r-lower)"@ valid elements would include @"123-34"@ but not @"abc"@
--
-- >>> :{
-- let tst1, tst2 :: Either EncodeEx (Enc '["boolAnd:(r-Word8-decimal)(r-Int-decimal)"] () T.Text)
--     tst1 = encBoolAnd' . toEncoding () $ "234"
--     tst2 = encBoolAnd' . toEncoding () $ "100000"
-- :}
-- 
-- >>> tst1
-- Right (MkEnc Proxy () "234")
-- >>> tst2
-- Left (EncodeEx "r-Word8-decimal" ("Payload does not satisfy format Word8-decimal: 100000"))
encBoolAnd' :: forall f s t1 t2 xs c str . (
    BoolOpIs s "and" ~ 'True
    , KnownSymbol s
    -- IsBoolAnd s ~ 'True
    , f ~ Either EncodeEx
    , Eq str
    , LeftTerm s ~ t1
    , RightTerm s ~ t2
    , EncodeF f (Enc xs c str) (Enc (t1 ': xs) c str)
    , EncodeF f (Enc xs c str) (Enc (t2 ': xs) c str)
    ) => Enc xs c str -> f (Enc (s ': xs) c str)
encBoolAnd'  = encBoolAnd (encodeF @f @(Enc xs c str) @(Enc (t1 ': xs) c str)) (encodeF @f @(Enc xs c str) @(Enc (t2 ': xs) c str))


-- tst1, tst2 :: Either EncodeEx (Enc '["boolNot:(r-Word8-decimal)"] () T.Text)
-- tst1 = encBoolNot' . toEncoding () $ "334"
-- tst2 = encBoolNot' . toEncoding () $ "127"

encBoolNot :: forall f s t xs c str . (
    BoolOpIs s "not" ~ 'True
    , KnownSymbol s
    , f ~ Either EncodeEx
    , FirstTerm s ~ t
    , KnownSymbol t
    , IsR t ~ 'True
    ) => (Enc xs c str -> f (Enc (t ': xs) c str)) -> Enc xs c str -> f (Enc (s ': xs) c str)
encBoolNot fn en0 =
        let
           een = fn en0
           p = (Proxy :: Proxy s)
           pt = (Proxy :: Proxy t)
        in
           case een of
               Left _ -> Right . withUnsafeCoerce id $ en0
               Right _ -> Left $ EncodeEx p $ "Encoding " ++ symbolVal pt ++ " succeeded"


-- |
-- >>> :{
-- let tst1, tst2 :: Either EncodeEx (Enc '["boolNot:(r-Word8-decimal)"] () T.Text)
--     tst1 = encBoolNot' . toEncoding () $ "334"
--     tst2 = encBoolNot' . toEncoding () $ "127"
-- :}
--
-- >>> tst1
-- Right (MkEnc Proxy () "334")
-- >>> tst2
-- Left (EncodeEx "boolNot:(r-Word8-decimal)" ("Encoding r-Word8-decimal succeeded"))
encBoolNot' :: forall f s t xs c str . (
    BoolOpIs s "not" ~ 'True
    , KnownSymbol s
    , f ~ Either EncodeEx
    , FirstTerm s ~ t
    , KnownSymbol t
    , IsR t ~ 'True
    , EncodeF f (Enc xs c str) (Enc (t ': xs) c str)
    ) => Enc xs c str -> f (Enc (s ': xs) c str)
encBoolNot' = encBoolNot (encodeF :: Enc xs c str -> f (Enc (t ': xs) c str))

-- | 
-- Decodes boolean expression if all leaves are @"r-"@
decBoolR :: forall f xs t s c str . (
    NestedR s  ~ 'True
    , Applicative f
    ) => Enc (s ': xs) c str -> f (Enc xs c str)

decBoolR = implTranP id

recWithEncBoolR :: forall (s :: Symbol) xs c str . (NestedR s ~ 'True)
                       => (Enc xs c str -> Either EncodeEx (Enc (s ': xs) c str))
                       -> Enc xs c str -> Either RecreateEx (Enc (s ': xs) c str)
recWithEncBoolR = unsafeRecWithEncR

-- * Type family based parser 

-- |
-- >>> :kind! BoolOpIs "boolAnd:(someenc)(otherenc)" "and"
-- ...
-- = 'True
type family BoolOpIs (s :: Symbol) (op :: Symbol) :: Bool where
    BoolOpIs s op = AcceptEq ('Text "Invalid bool encoding " ':<>: ShowType s ) (CmpSymbol (BoolOp s) op)

-- | 
-- This works fast with @!kind@ but is much slower in declaration 
-- :kind! BoolOp "boolOr:()()"
type family BoolOp (s :: Symbol) :: Symbol where
    BoolOp s = Fst (BoolOpHelper (Dupl s))
        -- ToLower (TakeUntil (Drop 4 s) ":")

type family BoolOpHelper (x :: (Symbol, Symbol)) :: (Symbol, Bool) where
    BoolOpHelper ('(,) s1 s2) = '(,) (ToLower (TakeUntil (Drop 4 s1) ":")) ( AcceptEq ('Text "Invalid bool encoding " ':<>: ShowType s2 ) (CmpSymbol "bool" (Take 4 s2)))


type family IsBool (s :: Symbol) :: Bool where
    IsBool s = AcceptEq ('Text "Not boolean encoding " ':<>: ShowType s ) (CmpSymbol "bool" (Take 4 s))

-- |
-- 
-- >>> :kind! NestedR "boolOr:(r-abc)(r-cd)"
-- ...
-- = 'True
--
-- >>> :kind! NestedR "boolOr:(boolAnd:(r-ab)(r-ac))(boolNot:(r-cd))"
-- ...
-- = 'True
--
-- :kind! NestedR "boolOr:(boolAnd:(r-ab)(ac))(boolNot:(r-cd))"
-- ...
-- ... (TypeError ...)
-- ...
type family NestedR (s :: Symbol) :: Bool where
    NestedR "" = 'True -- RightTerm, LeftTerm return "" on "r-"
    NestedR s = Or (IsROrEmpty s)
                   (And (IsBool s) (And (NestedR (LeftTerm s)) (NestedR (RightTerm s))))

-- Value level check
--
-- nestedR :: String -> Bool
-- nestedR s = isROrEmpty s || isBool s && (nestedR (leftTerm s) && nestedR (rightTerm s))

-- isBool ('b' :'o' : 'o' : _) = True
-- isBool _ = False

-- isROrEmpty "" = True
-- isROrEmpty ('r' : '-' : _) = True
-- isROrEmpty _ = False


type family FirstTerm (s :: Symbol) :: Symbol where
    FirstTerm s = LeftTerm s

-- | 
-- returns "" for unary operator
type family SecondTerm (s :: Symbol) :: Symbol where
    SecondTerm s = RightTerm s


-- |
-- >>> :kind! LeftTerm "boolSomeOp:(agag)(222)"
-- ...
-- = "agag"
--
-- >>> :kind! LeftTerm "r-Int-decimal"
-- ...
-- = ""
type family LeftTerm (s :: Symbol) :: Symbol where
    LeftTerm s = Concat (LDropLast( LTakeFstParen (LParenCnt (ToList s))))

-- |
-- >>> :kind! RightTerm "boolSomeOp:(agag)(222)"
-- ...
-- = "222"
--
-- >>> :kind! RightTerm "r-Int-decimal"
-- ...
-- = ""
type family RightTerm (s :: Symbol) :: Symbol where
    RightTerm s = Concat (LDropLast (LTakeSndParen 0 (LParenCnt (ToList s))))


type family LDropLast (s :: [Symbol]) :: [Symbol] where
    LDropLast '[] = '[]
    LDropLast '[x] = '[]
    LDropLast (x ': xs) = x ': LDropLast xs


type family LParenCnt (s :: [Symbol]) :: [(Symbol, Nat)] where
    LParenCnt '[] = '[]
    LParenCnt ("(" ': xs) = LParenCntHelper ('(,) "(" 'Decr) (LParenCnt xs) -- '(,) "(" (LParenCntFstCnt (LParenCnt xs) - 1) ': LParenCnt xs
    LParenCnt (")" ': xs) = LParenCntHelper ('(,) ")" 'Incr) (LParenCnt xs)
    LParenCnt (x ': xs) = LParenCntHelper ('(,) x 'NoChng) (LParenCnt xs)

data Adjust = Incr | Decr | NoChng

type family AdjHelper (a :: Adjust) (n :: Nat) :: Nat where
   AdjHelper 'Incr n = n + 1
   AdjHelper 'Decr 0 = 0
   AdjHelper 'Decr n = n - 1
   AdjHelper 'NoChng n = n

type family LParenCntHelper (s :: (Symbol, Adjust)) (sx :: [(Symbol, Nat)]) :: [(Symbol, Nat)] where
    LParenCntHelper ('(,) x k) '[] = '(,) x (AdjHelper k 0) ': '[]
    LParenCntHelper ('(,) x k) ('(,) c i : xs) = '(,) x (AdjHelper k i) ': '(,) c i ': xs


type family LTakeFstParen (si :: [(Symbol, Nat)]) :: [Symbol] where
    LTakeFstParen '[] = '[]
    LTakeFstParen ('(,) _ 0 ': xs) = LTakeFstParen xs
    LTakeFstParen ('(,) ")" 1 ': _) = '[")"]
    LTakeFstParen ('(,) a p ': xs) = a ': LTakeFstParen xs


type family LTakeSndParen (n :: Nat)  (si :: [(Symbol, Nat)]) :: [Symbol] where
    LTakeSndParen _ '[] = '[]
    LTakeSndParen 0 ('(,) ")" 1 ': xs) = LTakeSndParen 1 xs
    LTakeSndParen 1 ('(,) _ 0 : xs) = LTakeSndParen 1 xs
    LTakeSndParen 0 ('(,) _ _ ': xs) = LTakeSndParen 0 xs
    LTakeSndParen 1 ('(,) a _ ': xs) = a : LTakeSndParen 1 xs
    LTakeSndParen n _ = '[]

-- -- * (Example) Value level equivalend of type family parsers


-- -- map snd . parenCnt $ "((AGA)(bcd))(123) "
-- -- dropLast . takeFstParen . parenCnt $ "((AGA)(bcd))(123)" 
-- -- dropLast . takeSndParen 0 . parenCnt $ "((AGA)(bcd))(123)" 

-- leftTerm :: String -> String
-- leftTerm = dropLast . takeFstParen . parenCnt

-- rightTerm :: String -> String
-- rightTerm = dropLast . takeSndParen 0 . parenCnt

-- parenCnt :: String -> [(Char, Int)]
-- parenCnt [] = []
-- parenCnt ('(' : xs) = parenCntHelper ('(', -1) (parenCnt xs) --('(', parenCntFstCnt (parenCnt xs) - 1) : parenCnt xs
-- parenCnt (')' : xs) = parenCntHelper (')', 1) (parenCnt xs)  -- (')', parenCntFstCnt (parenCnt xs) + 1) : parenCnt xs
-- parenCnt (x : xs) = parenCntHelper (x, 0) (parenCnt xs) -- (x, parenCntFstCnt (parenCnt xs) ) : parenCnt xs


-- parenCntHelper :: (Char, Int) -> [(Char, Int)]  -> [(Char, Int)]
-- parenCntHelper (x, k) [] = [(x,k)]
-- parenCntHelper (x, k) ((c,i) : xs) = (x, i + k): (c,i) : xs


-- takeFstParen :: [(Char, Int)] -> [Char]
-- takeFstParen [] = []
-- takeFstParen ((_, 0) : xs) = takeFstParen xs
-- takeFstParen ((')', 1) : _) = [')']
-- takeFstParen ((a, p) : xs) = a : takeFstParen xs

-- takeSndParen :: Int -> [(Char, Int)] -> [Char]
-- takeSndParen _ [] = []
-- takeSndParen 0 ((')', 1) : xs) = takeSndParen 1 xs
-- takeSndParen 1 ((_, 0) : xs) = takeSndParen 1 xs
-- takeSndParen 0 ((_, _) : xs) = takeSndParen 0 xs
-- takeSndParen 1 ((a, _) : xs) = a : takeSndParen 1 xs
-- takeSndParen _ _ = []

-- dropLast :: [Char] -> [Char]
-- dropLast [] = []
-- dropLast [x] = []
-- dropLast (x:xs) = x : dropLast xs