{-# LANGUAGE TypeOperators #-}

module Data.Iso.Core (

  -- * Partial isomorphisms
  Iso(..), convert, inverse, many,
  
  -- * Stack-based isomorphisms
  (:-)(..), stack, unstack, swap, duck,
  lit, inverseLit, matchWithDefault, ignoreWithDefault
  
  ) where


import Prelude hiding (id, (.), head)

import Data.Monoid
import Data.Semigroup

import Control.Applicative hiding (many)
import Control.Monad
import Control.Category


-- Partial isomorphisms

-- | Bidirectional partial isomorphism.
data Iso a b = Iso (a -> Maybe b) (b -> Maybe a)

instance Category Iso where
  id                          = Iso Just Just
  ~(Iso f1 g1) . ~(Iso f2 g2) = Iso (f1 <=< f2) (g1 >=> g2)

instance Monoid (Iso a b) where
  mempty = Iso (const Nothing) (const Nothing)
  ~(Iso f1 g1) `mappend` ~(Iso f2 g2) =
    Iso
      ((<|>) <$> f1 <*> f2)
      ((<|>) <$> g1 <*> g2)

instance Semigroup (Iso a b) where
  (<>) = mappend

-- | Apply an isomorphism in one direction.
convert :: Iso a b -> a -> Maybe b
convert (Iso f _) = f

-- | Inverse of an isomorphism.
inverse :: Iso a b -> Iso b a
inverse (Iso f g) = Iso g f

-- | Apply an isomorphism as many times as possible, greedily.
many :: Iso a a -> Iso a a
many (Iso f g) = Iso manyF manyG
  where
    manyF = ((<|>) <$> (f >=> manyF) <*> Just)
    manyG = ((<|>) <$> (g >=> manyG) <*> Just)


-- Stack-based isomorphisms

-- | Heterogenous stack with a head and a tail.
data h :- t = h :- t
  deriving (Eq, Show)
infixr 5 :-

head :: (h :- t) -> h
head (h :- _) = h

-- | Convert to a stack isomorphism.
stack :: Iso a b -> Iso (a :- t) (b :- t)
stack (Iso f g) = Iso (lift f) (lift g)
  where
    lift k (x :- t) = (:- t) <$> k x

-- | Convert from a stack isomorphism.
unstack :: Iso (a :- ()) (b :- ()) -> Iso a b
unstack (Iso f g) = Iso (lift f) (lift g)
  where
    lift k = fmap head . k . (:- ())

-- | Swap the top two arguments.
swap :: Iso (a :- b :- t) (b :- a :- t)
swap = Iso f f
  where
    f (x :- y :- t) = Just (y :- x :- t)

-- | Introduce a head value that is passed unmodified.
duck :: Iso t1 t2 -> Iso (h :- t1) (h :- t2)
duck (Iso f g) = Iso (lift f) (lift g)
  where
    lift k (h :- t) = (h :-) <$> k t

-- | Push or pop a specific value.
lit :: Eq a => a -> Iso t (a :- t)
lit x = Iso f g
  where
    f t = Just (x :- t)
    g (x' :- t) = do
      guard (x' == x)
      Just t

-- | Inverse of 'lit'.
inverseLit :: Eq a => a -> Iso (a :- t) t
inverseLit = inverse . lit

-- | When converting from left to right, push the default value on top of the
-- stack. When converting from right to left, pop the value, make sure it
-- matches the predicate and then discard it.
matchWithDefault :: (a -> Bool) -> a -> Iso t (a :- t)
matchWithDefault p x = Iso f g
  where
    f t = Just (x :- t)
    g (x' :- t) = do
      guard (p x')
      return t

-- | When converting from left to right, push the default value on top of the stack. When converting from right to left, pop the value and discard it.
ignoreWithDefault :: a -> Iso t (a :- t)
ignoreWithDefault = matchWithDefault (const True)
