{-# LANGUAGE ScopedTypeVariables #-}
-- | A list diff.
module Data.TreeDiff.List (diffBy, Edit (..)) where

import Data.List.Compat (sortOn)
import qualified Data.MemoTrie as M
import qualified Data.Vector as V

-- | List edit operations
--
-- The 'Swp' constructor is redundant, but it let us spot
-- a recursion point when performing tree diffs.
data Edit a
    = Ins a    -- ^ insert
    | Del a    -- ^ delete
    | Cpy a    -- ^ copy unchanged
    | Swp a a  -- ^ swap, i.e. delete + insert
  deriving Show

-- | List difference.
--
-- >>> diffBy (==) "hello" "world"
-- [Swp 'h' 'w',Swp 'e' 'o',Swp 'l' 'r',Cpy 'l',Swp 'o' 'd']
--
-- >>> diffBy (==) "kitten" "sitting"
-- [Swp 'k' 's',Cpy 'i',Cpy 't',Cpy 't',Swp 'e' 'i',Cpy 'n',Ins 'g']
--
-- prop> \xs ys -> length (diffBy (==) xs ys) >= max (length xs) (length (ys :: String))
-- prop> \xs ys -> length (diffBy (==) xs ys) <= length xs + length (ys :: String)
--
-- /Note:/ currently this has O(n*m) memory requirements, for the sake
-- of more obviously correct implementation.
--
diffBy :: forall a. (a -> a -> Bool) -> [a] -> [a] -> [Edit a]
diffBy eq xs' ys' = reverse (snd (lcs (V.length xs) (V.length ys)))
  where
    xs = V.fromList xs'
    ys = V.fromList ys'

    lcs = M.memo2 impl

    impl :: Int -> Int -> (Int, [Edit a])
    impl 0 0 = (0, [])
    impl 0 m = case lcs 0 (m-1) of
        (w, edit) -> (w + 1, Ins (ys V.! (m - 1)) : edit)
    impl n 0 = case lcs (n -1) 0 of
        (w, edit) -> (w + 1, Del (xs V.! (n - 1)) : edit)

    impl n m = head $ sortOn fst
        [ edit
        , bimap (+1) (Ins y :) (lcs n (m - 1))
        , bimap (+1) (Del x :) (lcs (n - 1) m)
        ]
      where
        x = xs V.! (n - 1)
        y = ys V.! (m - 1)

        edit
            | eq x y    = bimap id   (Cpy x :)   (lcs (n - 1) (m - 1))
            | otherwise = bimap (+1) (Swp x y :) (lcs (n -1 ) (m - 1))

bimap :: (a -> c) -> (b -> d) -> (a, b) -> (c, d)
bimap f g (x, y) = (f x, g y)