{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveTraversable #-}
{-# LANGUAGE StandaloneDeriving, DeriveGeneric #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module Development.Rattle.Types(
    Trace(..), Touch(..), fsaTrace, normalizeTouch,
    TouchSet, tsRead, tsWrite, newTouchSet, addTouchSet,
    Cmd(..), mkCmd,
    RunIndex, runIndex0, nextRunIndex,
    ) where

import Data.Hashable
import Data.List.Extra
import System.Directory
import System.Info.Extra
import Control.Monad
import General.Binary
import Data.Word
import Development.Shake.Command
import Data.Semigroup
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString as BS
import qualified Data.HashSet as Set
import GHC.Generics
import Prelude
import System.Time.Extra
import General.FileName

-- record the hash as the first field
data Cmd = Cmd Int [CmdOption] [String]
    deriving Eq

instance Show Cmd where
    show (Cmd _ a b) = "Cmd " ++ show a ++ " " ++ show b

instance Hashable Cmd where
    hashWithSalt _ = hash
    hash (Cmd x _ _) = x

mkCmd :: [CmdOption] -> [String] -> Cmd
mkCmd a b = Cmd (hash (a,b)) a b

instance BinaryEx Cmd where
    getEx x = mkCmd (getEx a) (getEx b)
        where (a,b) = getExPair x
    putEx (Cmd _ a b) = putExPair (putEx a) (putEx b)

-- The common values for CmdOption are [], [Shell] and a few others - optimise those
instance BinaryEx [CmdOption] where
    getEx x
        | BS.null x = []
        | BS.length x == 1 = case getEx x :: Word8 of
            0 -> [Shell]
            1 -> [EchoStderr False]
            2 -> [Shell,EchoStderr False]
        | otherwise = map read $ getEx x

    putEx [] = mempty
    putEx [Shell] = putEx (0 :: Word8)
    putEx [EchoStderr False] = putEx (1 :: Word8)
    putEx [Shell,EchoStderr False] = putEx (2 :: Word8)
    putEx xs = putEx $ map show xs

deriving instance Generic CmdOption
deriving instance Read CmdOption
instance Hashable CmdOption

data Trace a = Trace
    {tRun :: {-# UNPACK #-} !RunIndex
    ,tStart :: {-# UNPACK #-} !Seconds
    ,tStop :: {-# UNPACK #-} !Seconds
    ,tTouch :: Touch a
    } deriving (Show, Functor, Foldable, Traversable, Eq)

instance BinaryEx a => BinaryEx (Trace a) where
    getEx x = Trace a b c $ getEx d
        where (a,b,c,d) = binarySplit3 x
    putEx (Trace a b c d) = putExStorable a <> putExStorable b <> putExStorable c <> putEx d

data Touch a = Touch
    {tRead :: [a]
    ,tWrite :: [a]
    } deriving (Show, Functor, Foldable, Traversable, Eq)

instance BinaryEx a => BinaryEx (Touch a) where
    getEx x = Touch (map getEx $ getExList a) (map getEx $ getExList b)
        where [a,b] = getExList x
    putEx (Touch a b) = putExList [putExList $ map putEx a, putExList $ map putEx b]

instance Semigroup (Touch a) where
    Touch r1 w1 <> Touch r2 w2 = Touch (r1++r2) (w1++w2)

instance Monoid (Touch a) where
    mempty = Touch [] []
    mappend = (<>)
    mconcat xs = Touch (concatMap tRead xs) (concatMap tWrite xs)

instance Hashable a => Hashable (Trace a) where
    hashWithSalt s (Trace a b c d) = hashWithSalt s (a,b,c,d)

instance Hashable a => Hashable (Touch a) where
    hashWithSalt s (Touch r w) = hashWithSalt s (r,w)

fsaTrace :: [FSATrace BS.ByteString] -> IO (Touch FileName)
-- We want to get normalized traces. On Linux, things come out normalized, and we just want to dedupe them
-- On Windows things come out as C:\windows\system32\KERNELBASE.dll instead of C:\Windows\System32\KernelBase.dll
-- so important to call (expensive) normalizeTouch
fsaTrace fs
    | isWindows =
        -- normalize twice because normalisation is cheap, but canonicalisation might be expensive
        fmap (normalizeTouch . fmap (byteStringToFileName . UTF8.fromString)) $
        canonicalizeTouch $
        fmap UTF8.toString $ normalizeTouch $ mconcatMap f fs
    | otherwise =
        -- We know the file names are already normalized from Shake so avoid a redundant conversion
        pure $ normalizeTouch $ byteStringToFileName <$> mconcatMap f fs
    where
        f (FSAWrite x) = Touch [] [x]
        f (FSARead x) = Touch [x] []
        f (FSADelete x) = Touch [] [x]
        f (FSAMove x y) = Touch [] [x,y]
        f (FSAQuery x) = Touch [x] []
        f (FSATouch x) = Touch [] [x]

normalizeTouch :: (Ord a, Hashable a) => Touch a -> Touch a
-- added 'sort' because HashSet uses the ordering of the hashes, which is confusing
-- and since we are sorting, try and avoid doing too much hash manipulation of the reads
normalizeTouch (Touch a b) = Touch (f $ sort a) (sort $ Set.toList b2)
    where
        b2 = Set.fromList b
        f (x1:x2:xs) | x1 == x2 = f (x1:xs)
        f (x:xs) | x `Set.member` b2 = f xs
                 | otherwise = x : f xs
        f [] = []

canonicalizeTouch :: Touch FilePath -> IO (Touch FilePath)
canonicalizeTouch (Touch a b) = Touch <$> mapM canonicalizePath a <*> mapM canonicalizePath b


-- For sets, Set.fromList is fastest if there are no dupes
-- Otherwise a Set.member/Set.insert is fastest
data TouchSet = TouchSet {tsRead :: Set.HashSet FileName, tsWrite :: Set.HashSet FileName}

newTouchSet :: [Touch FileName] -> TouchSet
newTouchSet [] = TouchSet Set.empty Set.empty
newTouchSet (Touch{..}:xs) = foldl' addTouchSet (TouchSet (Set.fromList tRead) (Set.fromList tWrite)) xs

addTouchSet :: TouchSet -> Touch FileName -> TouchSet
addTouchSet TouchSet{..} Touch{..} = TouchSet (f tsRead tRead) (f tsWrite tWrite)
    where f = foldl' (\mp k -> if Set.member k mp then mp else Set.insert k mp)


-- | Which run we are in, monotonically increasing
newtype RunIndex = RunIndex Int
    deriving (Eq,Ord,Show,Storable,BinaryEx,Hashable)

runIndex0 :: RunIndex
runIndex0 = RunIndex 0

nextRunIndex :: RunIndex -> RunIndex
nextRunIndex (RunIndex i) = RunIndex $ i + 1