{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Data types used by `Filediff`
module Filediff.Types
( Filediff(..)
, Diff(..)
, FileChange(..)
, ListDiff(..)
, listDiff
, isDel
, isMod
, isAdd
, Line
, Error
) where

import GHC.Generics

import Data.Default
import Data.Function (on)

import qualified Data.Text as T

import Data.Maybe

import Zora.List (merge, merge_by)
import Data.List (find, intersect, intersectBy, sortBy, (\\))

import Data.Monoid
import Control.Applicative

import Data.MemoCombinators (Memo, wrap)
import Data.MemoCombinators.Class (MemoTable, table, memoize)

-- | Diff between two lists. `dels` represents the indices
--   at which to delete, and `adds` represents the indices and
--   contents to add.
data ListDiff a = ListDiff
    { dels :: [(Int, a)]
    , adds :: [(Int, a)] }
    deriving (Show, Eq, Generic)

instance Default (ListDiff a) where
    def :: ListDiff a
    def = ListDiff [] []

instance (Eq a, Ord a, MemoTable a) => Monoid (ListDiff a) where
    mempty :: ListDiff a
    mempty = ListDiff [] []

    -- may fail
    mappend :: ListDiff a -> ListDiff a -> ListDiff a
    mappend
        (ListDiff abDels abAdds)
        (ListDiff bcDels bcAdds)
        = ListDiff acDels acAdds
        where
            acDels :: [(Int, a)]
            acDels = merge abDels bDelsFromA

            -- indices (in `a`) of elements that survive (a -> b)
            -- , but not (b -> c)
            -- TODO: `intersectBy` almost certainly ain't linear.
            -- Should probably write it here since we know these
            -- are sorted.
            bDelsFromA :: [(Int, a)]
            bDelsFromA
                = catMaybes
                . map f
                $ bcDels
                where
                    f :: (Int, a) -> Maybe (Int, a)
                    f (bi, ch) =
                        case match of
                            Just (ai, _) -> Just (ai, ch)
                            Nothing -> Nothing
                        where
                            match :: Maybe (Int, Int)
                            match = find (\(sai, sbi) -> sbi == bi) survivingAIndicesInB


                    --(\(_, bi) (_, biDeleted) -> bi == biDeleted)
                    --(zip survivingAIndicesInB) --:: [(Int, Int!)]
                    --(bcDels) 

            -- indices (in b) of elements that survive (a -> b)
            -- (in format [(in a, in b)])
            survivingAIndicesInB :: [(Int, Int)]
            survivingAIndicesInB = map (\(b,a) -> (a,b)) $ indicesAfterAdds 0 survivingAIndices (map fst abAdds)

            -- will not be all if the last elem of `a` is
            -- not deleted, but doesn't make a difference
            survivingAIndices :: [Int]
            survivingAIndices = if null abDels
                then []
                else [0..(maximum abDels')] \\ abDels'
                where
                    abDels' :: [Int]
                    abDels' = map fst abDels

            -- TODO: WEIRD. not using `forall a.` but still needs to be `b`?
            -- Given elements and their indices as [(Int, b)] as the only
            -- elements to survive the transformation, and [Int] as the
            -- indices added in the transformation, calculate the eventual
            -- positions of the elements.
            indicesAfterAdds :: Int -> [b] -> [Int] -> [(Int, b)]
            indicesAfterAdds _ [] _ = []
            indicesAfterAdds i elems@(x:xs) [] = (:) (i, x) $ indicesAfterAdds (i + 1) xs []
            indicesAfterAdds i elems@(x:xs) adds@(a:as) =
                if i < a
                    then (:) (i, x) $ indicesAfterAdds (i + 1) xs (a:as)
                    else indicesAfterAdds (i + 1) (x:xs) as

            acAdds :: [(Int, a)]
            acAdds = merge_by (\(i,_) (j,_) -> i `compare` j) bcAdds cAddsFromA

            cAddsFromA :: [(Int, a)]
            cAddsFromA = indicesAfterAdds 0 (map snd survivingABAdds) (map fst bcAdds)

            -- adds in (a -> b) that survive (b -> c)
            survivingABAdds :: [(Int, a)]
            survivingABAdds = survivingABAdds' abAdds bcDels

            survivingABAdds' :: [(Int, a)] -> [(Int, a)] -> [(Int, a)]
            survivingABAdds' [] _ = []
            survivingABAdds' adds [] = adds
            survivingABAdds' (a:adds) (d:dels) =
                case (fst a) `compare` (fst d) of
                    LT -> (:) a $ survivingABAdds' adds (d:dels)
                    EQ -> survivingABAdds' adds dels
                    GT -> survivingABAdds' (a:adds) dels

-- | The basic data type for a difference between two files. The
--   "base" `FilePath` is the file chose state is being compared against,
--   and the "comp" `FilePath` is the file being compared (the "later"
--   of the two).
data Filediff = Filediff {
    base :: FilePath,
    comp :: FilePath,
    change :: FileChange
} deriving (Eq, Show, Generic)

-- | The types and sets of changes possible between two files.
data FileChange
    = Del (ListDiff Line)
    | Mod (ListDiff Line)
    | Add (ListDiff Line) deriving (Eq, Show, Generic)

-- | Gets the 'ListDiff' stored in a 'FileChange'.
listDiff :: FileChange -> ListDiff Line
listDiff (Del diff) = diff
listDiff (Mod diff) = diff
listDiff (Add diff) = diff

-- | Whether a 'FileChange' is a deletion or not.
isDel :: FileChange -> Bool
isDel (Del _) = True
isDel (Mod _) = False
isDel (Add _) = False
-- | Whether a 'FileChange' is a modification or not.
isMod :: FileChange -> Bool
isMod (Del _) = False
isMod (Mod _) = True
isMod (Add _) = False
-- | Whether a 'FileChange' is a addition or not.
isAdd :: FileChange -> Bool
isAdd (Del _) = False
isAdd (Mod _) = False
isAdd (Add _) = True

instance Monoid FileChange where
    mempty :: FileChange
    mempty = Mod mempty -- no changes (no add / del); identity diff

    mappend :: FileChange -> FileChange -> FileChange
    mappend (Del _    ) (Del _    ) = error "del ++ del"
    mappend (Del _    ) (Mod _    ) = error "del ++ mod"
    mappend (Del diff1) (Add diff2) = Mod $ diff1 `mappend` diff2
    mappend (Mod _    ) (Del diff2) = Del diff2
    mappend (Mod diff1) (Mod diff2) = Mod $ diff1 `mappend` diff2
    mappend (Mod _    ) (Add _    ) = error "mod ++ add"
    mappend (Add _    ) (Del _    ) = mempty -- will be filtered out during directory composition. Yes; this isn't ideal, but it's at least clean.
    mappend (Add diff1) (Mod diff2) = Add $ diff1 `mappend` diff2
    mappend (Add _    ) (Add _    ) = error "add ++ add"

-- TODO: is this mathematically correct?
instance Monoid Filediff where
    mempty :: Filediff
    mempty = Filediff "" "" (Mod mempty)

    mappend :: Filediff -> Filediff -> Filediff
    mappend fd1 fd2 =
        if comp fd1 /= base fd2
            then error $ "`comp` of filediff 1 is not `base` of filediff 2: " ++ (show fd1) ++ " vs. " ++ (show fd2) 
            else Filediff {
                base = base fd1,
                comp = comp fd2,
                change = change fd1 `mappend` change fd2 }

-- | A data type for differences between directories. `filediffs`
--   stores 'Filediffs` whose filepaths are relative to directories being
--   diffed.
data Diff = Diff {
    filediffs :: [Filediff]
} deriving (Show, Generic)

instance Eq Diff where
    (==) :: Diff -> Diff -> Bool
    (==) a b
        = sortBy cmp (filediffs a) == sortBy cmp (filediffs b)
        where
            cmp :: Filediff -> Filediff -> Ordering
            cmp a b = if base a /= base b
                then base a `compare` base b
                else comp a `compare` comp b

instance Default Diff where
    def :: Diff
    def = Diff []

instance MemoTable T.Text where
    table :: Memo T.Text
    table = wrap T.pack T.unpack table

instance Monoid Diff where
    mempty :: Diff
    mempty = Diff []

    mappend :: Diff -> Diff -> Diff
    mappend (Diff aFilediffs) (Diff bFilediffs)
        = Diff $ filediffs
        where
            filediffs :: [Filediff]
            filediffs
                = filter ((/=) mempty . change)
                $ exclusion ++ (map (uncurry mappend) intersection)

            exclusion :: [Filediff]
            exclusion
                =  (excludeBy dirsEqual aFilediffs bFilediffs)
                ++ (excludeBy dirsEqual bFilediffs aFilediffs)

            intersection :: [(Filediff, Filediff)]
            intersection = intersectBy dirsEqual aFilediffs bFilediffs

            dirsEqual :: Filediff -> Filediff -> Bool
            dirsEqual
                (Filediff aBase aComp _)
                (Filediff bBase bComp _)
                = (aBase == bBase) && (aComp == bComp)

            excludeBy  :: (a -> a -> Bool) -> [a] -> [a] -> [a]
            excludeBy _ [] _ = []
            excludeBy f (x:xs) ys =
                if any (f x) ys
                    then excludeBy f xs ys
                    else x : excludeBy f xs ys

            intersectBy :: (a -> a -> Bool) -> [a] -> [a] -> [(a, a)]
            intersectBy f a b
                = filter (uncurry f)
                $ (\x y -> (x,y)) <$> a <*> b

-- | Data type for a line.
type Line = T.Text

-- | Basic error type.
type Error = String