{-# LANGUAGE CPP                 #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators       #-}

module Data.InvertibleGrammar.Combinators
  ( iso
  , osi
  , partialIso
  , partialOsi
  , push
  , pair
  , swap
  , cons
  , nil
  , insert
  , insertMay
  , toDefault
  , coproduct
  , onHead
  , onTail
  , traversed
  , flipped
  , sealed
  , coerced
  , annotated
  ) where

import Control.Category ((>>>))
import Data.Coerce
import Data.Maybe
import Data.Void
import Data.Text (Text)
import Data.InvertibleGrammar.Base

#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup
#endif

-- | Isomorphism on the stack head.
iso :: (a -> b) -> (b -> a) -> Grammar p (a :- t) (b :- t)
iso f' g' = Iso f g
  where
    f (a :- t) = f' a :- t
    g (b :- t) = g' b :- t


-- | Flipped isomorphism on the stack head.
osi :: (b -> a) -> (a -> b) -> Grammar p (a :- t) (b :- t)
osi f' g' = Iso g f
  where
    f (a :- t) = f' a :- t
    g (b :- t) = g' b :- t

-- | Partial isomorphism (for backward run) on the stack head.
partialIso :: (a -> b) -> (b -> Either Mismatch a) -> Grammar p (a :- t) (b :- t)
partialIso f' g' = PartialIso f g
  where
    f (a :- t) = f' a :- t
    g (b :- t) = (:- t) <$> g' b


-- | Partial isomorphism (for forward run) on the stack head.
partialOsi :: (a -> Either Mismatch b) -> (b -> a) -> Grammar p (a :- t) (b :- t)
partialOsi g' f' = Flip $ PartialIso f g
  where
    f (a :- t) = f' a :- t
    g (b :- t) = (:- t) <$> g' b


-- | Push an element to the stack on forward run, check if the element
-- satisfies predicate, otherwise report a mismatch.
push :: a -> (a -> Bool) -> (a -> Mismatch) -> Grammar p t (a :- t)
push a p e = PartialIso f g
  where
    f t = a :- t
    g (a' :- t)
      | p a' = Right t
      | otherwise = Left $ e a'


-- | 2-tuple grammar. Construct on forward run, deconstruct on
-- backward run.
pair :: Grammar p (b :- a :- t) ((a, b) :- t)
pair = Iso
  (\(b :- a :- t) -> (a, b) :- t)
  (\((a, b) :- t) -> b :- a :- t)


-- | List cons-cell grammar. Construct on forward run, deconstruct on
-- backward run.
cons :: Grammar p ([a] :- a :- t) ([a] :- t)
cons = PartialIso
  (\(lst :- el :- t) -> (el:lst) :- t)
  (\(lst :- t) ->
      case lst of
        [] -> Left (expected "list element")
        (el:rest) -> Right (rest :- el :- t))


-- | Empty list grammar. Construct empty list on forward run, check if
-- list is empty on backward run.
nil :: Grammar p t ([a] :- t)
nil = PartialIso
  (\t -> [] :- t)
  (\(lst :- t) ->
      case lst of
        [] -> Right t
        (_el:_rest) -> Left (expected "end of list"))


-- | Swap two topmost stack elements.
swap :: Grammar p (a :- b :- t) (b :- a :- t)
swap = Iso
  (\(a :- b :- t) -> (b :- a :- t))
  (\(b :- a :- t) -> (a :- b :- t))


-- | Assoc-list element grammar. Inserts an element (with static key)
-- on forward run, look up an element on backward run.
insert :: (Eq k) => k -> Mismatch -> Grammar p (v :- [(k, v)] :- t) ([(k, v)] :- t)
insert k m = PartialIso
  (\(v :- alist :- t) -> ((k, v) : alist) :- t)
  (\(alist :- t) ->
     case popKey k alist of
       Nothing -> Left m
       Just (v, alist') -> Right (v :- alist' :- t))


-- | Optional assoc-list element grammar. Like 'insert', but does not
-- report a mismatch on backward run. Instead takes and produces a
-- Maybe-value.
insertMay :: (Eq k) => k -> Grammar p (Maybe v :- [(k, v)] :- t) ([(k, v)] :- t)
insertMay k = PartialIso
  (\(mv :- alist :- t) ->
      case mv of
        Just v -> ((k, v) : alist) :- t
        Nothing -> alist :- t)
  (\(alist :- t) ->
     case popKey k alist of
       Nothing -> Right (Nothing :- alist :- t)
       Just (v, alist') -> Right (Just v :- alist' :- t))


popKey :: forall k v. Eq k => k -> [(k, v)] -> Maybe (v, [(k, v)])
popKey k' = go []
  where
    go :: [(k, v)] -> [(k, v)] -> Maybe (v, [(k, v)])
    go acc (x@(k, v) : xs)
      | k == k' = Just (v, reverse acc ++ xs)
      | otherwise = go (x:acc) xs
    go _ [] = Nothing


-- | Default value grammar. Replaces 'Nothing' with a default value on
-- forward run, an replaces a default value with 'Nothing' on backward
-- run.
toDefault :: (Eq a) => a -> Grammar p (Maybe a :- t) (a :- t)
toDefault def = iso
  (fromMaybe def)
  (\val -> if val == def then Nothing else Just val)


-- | Run a grammar operating on the stack head in a context where
-- there is no stack.
sealed :: Grammar p (a :- Void) (b :- Void) -> Grammar p a b
sealed g =
  Iso (:- error "void") (\(a :- _) -> a) >>>
  g >>>
  Iso (\(a :- _) -> a) (:- error "void")


-- | Focus a given grammar to the stack head.
onHead :: Grammar p a b -> Grammar p (a :- t) (b :- t)
onHead = OnHead


-- | Focus a given grammar to the stack tail.
onTail :: Grammar p ta tb -> Grammar p (h :- ta) (h :- tb)
onTail = OnTail


-- | Traverse a structure with a given grammar.
traversed :: (Traversable f) => Grammar p a b -> Grammar p (f a) (f b)
traversed = Traverse


-- | Run a grammar with inputs and outputs flipped.
flipped :: Grammar p a b -> Grammar p b a
flipped = Flip


-- | Run a grammar with an annotation.
annotated :: Text -> Grammar p a b -> Grammar p a b
annotated = Annotate


-- | Run a grammar with the stack heads coerced to other ('Coercible')
-- types.
coerced
  :: (Coercible a c, Coercible b d) =>
     Grammar p (a :- t) (b :- t')
  -> Grammar p (c :- t) (d :- t')
coerced g = iso coerce coerce >>> g >>> iso coerce coerce


-- | Join alternative grammars in parallel.
coproduct :: [Grammar p a b] -> Grammar p a b
coproduct = foldl1 (<>)