{-|
Module      : Unify 
Description : Unification by permutation
Copyright   : (c) Murdoch J. Gabbay, 2020
License     : GPL-3
Maintainer  : murdoch.gabbay@gmail.com
Stability   : experimental
Portability : POSIX

Unification by permutation
-}

{-# LANGUAGE ConstraintKinds            
           , DataKinds                  
           , DefaultSignatures          
           , DeriveGeneric              
           , DerivingStrategies         
           , DerivingVia                
           , EmptyCase                  
           , FlexibleContexts           
           , FlexibleInstances          
           , GADTs                      
           , GeneralizedNewtypeDeriving   
           , InstanceSigs               
           , MultiParamTypeClasses      
           , PartialTypeSignatures        
           , ScopedTypeVariables        
           , StandaloneDeriving         
           , TupleSections              
           , TypeOperators              
#-}

module Language.Nominal.Unify
    ( -- $setup
      -- * Introduction
      -- -- $intro
      -- * @'Ren'@
      KRen (..)
    , Ren
    , idRen
    , nothingRen
    , isJustRen
    , isNothingRen
      -- Manipulating renamings
    , renNub
    , renExtend
    , renToList
    , renFromList
    , renRemoveBlock
      -- * Unification up to a @'KRen'@
    , KUnifyPerm (..)
    , UnifyPerm
    , unifyPerm
    , kunifiablePerm
    , unifiablePerm
      -- * Special case: unifying prefixes of lists
    , kevPrefixRen
    , evPrefixRen
    , kevIsPrefix
    , evIsPrefix
      -- * Tests
      -- $tests
    ) where

import           Data.Function            (on)
import qualified Data.Map.Strict          as DM -- for unifyPerm 
import           Data.Maybe               -- (isJust)
import           Data.List.Extra          (takeEnd)
import           Data.List.NonEmpty       (NonEmpty)
import           Data.Proxy               (Proxy (..))
import qualified Data.Set                 as S -- (fromList) 
import           Data.Type.Equality
import           GHC.Generics
import           Type.Reflection

import           Language.Nominal.Name
import           Language.Nominal.NameSet
import           Language.Nominal.Nom
import           Language.Nominal.Binder
import           Language.Nominal.Abs

-- $setup
-- >>> import Data.Set as S

{- $intro

-}


-- * @'Ren'@

-- | An element of `Ren` is either
-- 
-- 1. `Just` an injective finite map from `Atom` to `Atom` (for this file, call this a __renaming__), or
--
-- 2. A "nomap" value (`Nothing`).
-- 
-- A `Ren` represents a solution to the problem 
--
-- /does there exist a permutation that makes @x@ equal to @y@?/
--
-- A type in the @'KUnifyPerm'@ typeclass is structurally guaranteed to give /at most one/ solution to such a unification problem, up to renaming irrelevant atoms. 
-- So in a nutshell: names, lists, and tuples are good here, and functions (not an equality type) and sets (may have multiple solutions) are not good.
--
--  A 'Just' value represents a solution to this unification problem; a 'Nothing' value represents that no such solution can be found.  @'unifyPerm'@ calculates these solutions. 
newtype KRen s = Ren {unRen :: Maybe (DM.Map (KAtom s) (KAtom s))}
   deriving (Show, Generic)

instance Typeable s => Swappable (KRen s) where

-- | Renaming on a @'Tom'@ instance
type Ren = KRen Tom

-- | The identity renaming.  
idRen :: KRen s
idRen = Ren . Just $ DM.empty

-- | The Nothing renaming.  If we think of @'KRen' s@ as type of solutions to permutation unification problems, then @'nothingRen'@ indicates no solution could be found. 
nothingRen :: KRen s
nothingRen = Ren Nothing


-- | Is it Just a renaming?  (My unification problem can be solved, and here's the solution.) 
isJustRen :: KRen s -> Bool
isJustRen (Ren x) = isJust x

-- | Is it Nothing?  (Nope; no solution exists.) 
isNothingRen :: KRen s -> Bool
isNothingRen = not . isJustRen

-- | Elements of @'KRen'@ are compared for equality on their __nub__ @'renNub'@, meaning just that identity mappings are ignored.
--
-- >>> a = exit $ freshAtom
-- >>> idRen == renExtend a a idRen
-- True 
instance Eq (KRen s) where
    (==) = (==) `on` (unRen . renNub)

-- | Elements of @'KRen'@ are compared on their __nub__ @'renNub'@, meaning just that identity mappings are ignored.
instance Ord (KRen s) where
    compare = compare `on` unRen . renNub

-- | Support of a renaming is the set of nontrivially mapped atoms 
--
-- >>> (supp (idRen :: Ren)) == S.empty
-- True
-- >>> (supp (nothingRen :: Ren)) == S.empty
-- True
instance Typeable s => KSupport s (KRen s) where
   ksupp _ (Ren m') =
      fromMaybe S.empty $
         DM.foldrWithKey (\a b s -> if a == b then s else s `S.union` (S.fromList [a,b])) S.empty <$> m'


-- | The parts of the map that are not identity
renNub :: KRen s -> KRen s
renNub (Ren m') = Ren $ DM.filterWithKey (/=) <$> m'


-- | Extend a renaming m with a new pair a -> b, maintaining the property of being a partial injection.
-- If adding a->b would destroy partial injectivity, then return @Nothing@. 
renExtend :: KAtom s -> KAtom s -> KRen s -> KRen s
renExtend a b (Ren m') = Ren $ do -- Maybe monad 
   m <- m'
   case DM.lookup a m of
      Nothing | not $ elem b $ DM.elems m -> DM.insert a b <$> m' -- if a doesn't map and b isn't already mapped to, then add (a,b)
      Just b' | b' == b                   -> m' -- if a maps to b already, then do nothing 
      _                                   -> Nothing -- otherwise, complain

-- | Extract the graph of a 'Ren', Maybe.
renToList :: KRen s -> Maybe [(KAtom s, KAtom s)]
renToList (Ren Nothing)  = Nothing
renToList (Ren (Just m)) = Just $ DM.toList m

-- | Convert a graph to a 'Ren'. 
renFromList :: [(KAtom s, KAtom s)] -> KRen s
renFromList = foldr (\(a,b) r -> renExtend a b r) idRen

-- | Given a block of atoms, remove them from the map /provided/ that the atoms in that block only map to other atoms within it; if an atom gets mapped across the block boundary, return @Nothing@. 
--
-- Needed for equality instance of 'Language.Nominal.Examples.IdealisedEUTxO.Chunk'.
renRemoveBlock :: [KAtom s] -> KRen s -> KRen s
renRemoveBlock _    (Ren Nothing)  = Ren Nothing
renRemoveBlock atms (Ren (Just m)) =
   DM.foldrWithKey (\a b r -> case (a `elem` atms, b `elem` atms) of
                                 (True,  True)  -> r                 -- both elements in block; discard
                                 (False, False) -> renExtend a b r   -- both elements out of block; append (some inefficiency here, no doubt)
                                 _              -> nothingRen        -- otherwise stop
                   ) idRen m


instance Semigroup (KRen s) where
    (Ren (Just m)) <> r = DM.foldrWithKey renExtend r m
    (Ren Nothing)  <> _ = Ren Nothing

instance Monoid (KRen s) where
   mempty = Ren $ Just DM.empty

-- | There are two reasonable notions of restriction; filter, or default to Nothing.  We choose the option that discards minimal information, i.e. the filter.
instance Typeable s => KRestrict s (KRen s) where
   restrict :: [KAtom s] -> KRen s -> KRen s
   restrict as (Ren m') = Ren $ do -- Maybe monad
      m <- m'
      return $ DM.filterWithKey (\a b -> not (elem a as) && not (elem b as)) m

-- * Unification up to a @'KRen'@

-- | Equal-up-to-permutation.  The algorithm works very simply by traversing the element looking for atoms and when it finds them, trying to match them up.  If it can do so injectively then it succeeds with @'Just'@ a renaming; if it fails it returns @'Nothing'@. 
--
-- /Question: Granted that 'KUnifyPerm' is a subset of 'KSupport', why are they not equal?/
-- Because for simplicity we only consider types for which at most one unifier can be found, by an efficient structural traversal.
-- This avoids backtracking, and makes a 'kunifyPerm' a function to 'KRen'.
-- So, a key difference is that 'KSupport' can easily calculate the support of a set (unordered list without multiplicities) whereas 'KUnifyPerm' does not even attempt to calculate unifiers of sets; not because this would be impossible, but because it would require a significant leap in complexity that we do not seem to need (so far).
class KSupport s a => KUnifyPerm s a where

    -- | This calculates a solution to a unification problem
    kunifyPerm :: proxy s -> a -> a -> KRen s
    default kunifyPerm :: (Generic a, GUnifyPerm s (Rep a)) => proxy s -> a -> a -> KRen s
    kunifyPerm _ x y = gunifyPerm (from x) (from y)

    -- | This applies a solution to a unification problem (represented as a renaming) to an element.  
    --
    -- __Note:__ @'ren'@ is not just swapping.  It traverses the structure of its argument performing an atom-for-atom renaming.  In the case that its argument solves a unification problem, its /effect/ is the same as if a permuatation were applied.
    --
    -- This is checked in 'Language.Nominal.Properties.UnifySpec.iprop_fresh_ren' and 'Language.Nominal.Properties.UnifySpec.prop_unify_ren'.
    ren :: KRen s -> a -> a
    default ren :: (Generic a, GUnifyPerm s (Rep a)) => KRen s -> a -> a
    ren r = to . gren r . from

-- | 'Tom'-instance of typeclass 'KUnifyPerm'.
type UnifyPerm = KUnifyPerm Tom

-- | Unify on 'Atom's, thus 'Tom'-instance of 'kunifyPerm'.
unifyPerm :: UnifyPerm a => a -> a -> Ren
unifyPerm = kunifyPerm atTom

-- | Does an s-unifier exist?
--
-- We write @proxy@ instead of 'Proxy' for the user's convenience, so the user can take advantage of any type that mentions @s@.
kunifiablePerm :: KUnifyPerm s a => proxy s -> a -> a -> Bool
kunifiablePerm p a a' = isJustRen $ kunifyPerm p a a'

-- | Does a 'Tom'-unifier exist?
unifiablePerm :: UnifyPerm a => a -> a -> Bool
unifiablePerm = kunifiablePerm atTom


----------------------------------------------
-- KUnifyPerm instances
-- instance order: nameless, tuple, list, nonempty list, maybe, sum, atom, name, nom, abs 

instance (Typeable s, Eq a) => KUnifyPerm s (Nameless a) where
    kunifyPerm _ a b
        | a == b    = mempty
        | otherwise = Ren Nothing
    ren _ = id

deriving via Nameless ()   instance Typeable s => KUnifyPerm s ()
deriving via Nameless Bool instance Typeable s => KUnifyPerm s Bool
deriving via Nameless Char instance Typeable s => KUnifyPerm s Char
deriving via Nameless Int  instance Typeable s => KUnifyPerm s Int
deriving via Nameless Integer instance Typeable s => KUnifyPerm s Integer

instance (KUnifyPerm s a, KUnifyPerm s b) => KUnifyPerm s (a, b)
instance (KUnifyPerm s a, KUnifyPerm s b, KUnifyPerm s c) => KUnifyPerm s (a, b, c)
instance (KUnifyPerm s a, KUnifyPerm s b, KUnifyPerm s c, KUnifyPerm s d) => KUnifyPerm s (a, b, c, d)
instance (KUnifyPerm s a, KUnifyPerm s b, KUnifyPerm s c, KUnifyPerm s d, KUnifyPerm s e) => KUnifyPerm s (a, b, c, d, e)
instance KUnifyPerm s a => KUnifyPerm s [a]
instance KUnifyPerm s a => KUnifyPerm s (NonEmpty a)
instance KUnifyPerm s a => KUnifyPerm s (Maybe a)
instance (KUnifyPerm s a, KUnifyPerm s b) => KUnifyPerm s (Either a b)

-- | Unify atoms
instance (Typeable s, Typeable t) => KUnifyPerm s (KAtom t) where

    kunifyPerm :: proxy s -> KAtom t -> KAtom t -> KRen s
    kunifyPerm _ a b = case testEquality (typeRep :: TypeRep s) (typeRep :: TypeRep t) of
        Nothing
            | a == b    -> mempty
            | otherwise -> Ren Nothing
        Just Refl       -> renExtend a b mempty

    ren :: KRen s -> KAtom t -> KAtom t
    ren (Ren (Just m)) a =
        case testEquality (typeRep :: TypeRep s) (typeRep :: TypeRep t) of
            Nothing   -> a
            Just Refl -> DM.findWithDefault a a m
    ren (Ren Nothing)  a = a

-- | Unify names: they behave just an atom-label tuple  
instance (Typeable s, Typeable u, KUnifyPerm s t) => KUnifyPerm s (KName u t)

-- | Unify 'Nom' abstractions.
-- Unpack, unify, clean out fresh names
instance (Typeable s, KUnifyPerm s a) => KUnifyPerm s (KNom s a) where
   -- kunifyPerm p noma nomb = resAppC (\a -> resAppC (kunifyPerm p a) nomb) noma  
   kunifyPerm p noma nomb = resAppC' noma $ \a -> resAppC' nomb $ \b -> kunifyPerm p a b
   ren r m = ren r <$> m


-- | Unify 'Abs' name-abstraction
instance (Typeable s, KUnifyPerm s t, KUnifyPerm s a) => KUnifyPerm s (KAbs (KName s t) a) where
   kunifyPerm p absa absb =
      (fuse (absa, absb)) @@. \na (a,b) -> kunifyPerm p (na, a) (na, b) -- use of '@@.' here cleans out the (na, na) binding. 
   ren r m = ren r <$> m


-- * Special case: unifying prefixes of lists

-- | Unify a list with a prefix of the other 
kevPrefixRen :: KUnifyPerm s a => proxy s -> [a] -> [a] -> KRen s
kevPrefixRen p l1 l2 = kunifyPerm p l1 (takeEnd (length l1) l2)

-- | Unify a list with a prefix of the other (at @'Tom'@ version). 
evPrefixRen :: UnifyPerm a => [a] -> [a] -> Ren
evPrefixRen = kevPrefixRen atTom


-- | One list unifies with a prefix of the other 
kevIsPrefix :: forall s proxy a. KUnifyPerm s a => proxy s -> [a] -> [a] -> Bool
kevIsPrefix p l1 l2 = isJustRen $ kevPrefixRen p l1 l2

-- | One list unifies with a prefix of the other (at @'Tom'@ version). 
evIsPrefix :: UnifyPerm a => [a] -> [a] -> Bool
evIsPrefix = kevIsPrefix atTom


-- * Generics support for @'KUnifyPerm'@

class GUnifyPerm s f where
    gunifyPerm :: f x -> f x -> KRen s
    gren       :: KRen s -> f x -> f x

-- Unifiers accumulate on datatype-formers
instance (GUnifyPerm s f, GUnifyPerm s g) => GUnifyPerm s (f :*: g) where
    gunifyPerm (x1 :*: y1) (x2 :*: y2) = gunifyPerm x1 x2 <> gunifyPerm y1 y2
    gren r (x :*: y) = gren r x :*: gren r y

-- Unifier on a leaf is trivial
instance GUnifyPerm s U1 where
    gunifyPerm U1 U1 = mempty
    gren _ U1 = U1

instance GUnifyPerm s V1 where
    gunifyPerm x = case x of
    gren _ x = case x of

instance (GUnifyPerm s f, GUnifyPerm s g) => GUnifyPerm s (f :+: g) where

    gunifyPerm (L1 x) (L1 y) = gunifyPerm x y
    gunifyPerm (R1 x) (R1 y) = gunifyPerm x y
    gunifyPerm _      _      = Ren Nothing

    gren r (L1 x) = L1 $ gren r x
    gren r (R1 x) = R1 $ gren r x

instance KUnifyPerm s c => GUnifyPerm s (K1 i c) where
    gunifyPerm (K1 x) (K1 y) = kunifyPerm Proxy x y
    gren r (K1 x) = K1 $ ren r x

instance GUnifyPerm s f => GUnifyPerm s (M1 i t f) where
    gunifyPerm (M1 x) (M1 y) = gunifyPerm x y
    gren r (M1 x) = M1 $ gren r x

{- $tests Property-based tests are in "Language.Nominal.Properties.UnifySpec". -}