{-|
Module    : TI
Description : The Template Instantiator
-}
{-# LANGUAGE LambdaCase, BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE TemplateHaskell #-}
module TI
    ( module Core.Examples
    , hdbgProg
    ) where
----------------------------------------------------------------------------------
import Data.Maybe               (fromJust, fromMaybe)
import Data.List                (mapAccumL, intersperse)
import Control.Monad            (guard)
import Data.Foldable            (traverse_, find)
import Data.Function            ((&))
import System.IO                (Handle, hPutStr)
import Text.Printf              (printf, hPrintf)
import Data.Proxy               (Proxy(..))
import Data.Monoid              (Endo(..))
import Lens.Micro
import Lens.Micro.TH
import Data.Pretty
import Data.Heap
import Core.Examples
import Core
----------------------------------------------------------------------------------

data TiState = TiState Stack Dump TiHeap Env Stats
    deriving Show

type Stack = [Addr]

type Env = [(Name, Addr)]

type TiHeap = Heap Node

data Node = NAp Addr Addr
          | NSupercomb Name [Name] Expr
          | NPrim Name Prim
          | NNum Int
          | NInd Addr
          | NData Int [Addr] -- NData Tag [Component]
          | NMarked Node
          deriving Show

type Dump = [Stack]

data Prim = ConP Int Int -- ConP Tag Arity
          | IfP
          | IntP Int
          | IntAddP
          | IntSubP
          | IntMulP
          | IntDivP
          | IntNegP
          | IntEqP
          | CasePairP
          | CaseListP
          | AbortP
          deriving (Show, Eq)

instance Pretty Prim where
    prettyPrec (IntP n) = withPrec maxBound $ IStr $ show n ++ "#"
    prettyPrec IntAddP  = withPrec maxBound $ "+#"

data Stats = Stats
    { _stsReductions   :: Int
    , _stsAllocations  :: Int
    , _stsDereferences :: Int
    , _stsGCCycles     :: Int
    }
    deriving (Show)

makeLenses ''Stats

tiStats :: Lens' TiState Stats
tiStats = lens
    (\ (TiState _ _ _ _ sts) -> sts)
    (\ (TiState s d h g _) sts' -> TiState s d h g sts')

tiHeap :: Lens' TiState TiHeap
tiHeap = lens
    (\ (TiState _ _ h _ _) -> h)
    (\ (TiState s d _ g sts) h' -> TiState s d h' g sts)

----------------------------------------------------------------------------------

compile :: Program -> Maybe TiState
compile prog = Just $ TiState s d h g stats
    where
        s = [mainAddr]
        d = []
        (h,g) = buildInitialHeap defs
        defs = insertModule corePrelude prog
        stats = Stats 0 0 0 0

        mainAddr = fromJust $ lookup "main" g

buildInitialHeap :: Program -> (TiHeap, [(Name, Addr)])
buildInitialHeap (Program scDefs) = (h'', scAddrs ++ primAddrs)
    where
        h = mempty

        (h', scAddrs) = mapAccumL allocateSc h scDefs
        (h'', primAddrs) = mapAccumL allocatePrim h' primitives

        allocateSc :: TiHeap -> ScDef -> (TiHeap, (Name, Addr))
        allocateSc h (ScDef n a b) = (h', (n, addr))
            where
                (h', addr) = alloc h (NSupercomb n a b)

        allocatePrim :: TiHeap -> (Name, Prim) -> (TiHeap, (Name, Addr))
        allocatePrim h (n, p) = (h', (n, addr))
            where (h', addr) = alloc h (NPrim n p)

primitives :: [(Name, Prim)]
primitives =
    [ ("negate#",   IntNegP)
    , ("+#",        IntAddP)
    , ("-#",        IntSubP)
    , ("*#",        IntMulP)
    , ("/#",        IntDivP)
    , ("==#",       IntEqP)
    , ("if#",       IfP)
    , ("casePair#", CasePairP)
    , ("caseList#", CaseListP)
    , ("abort#",    AbortP)
    ]

instantiate :: Expr -> TiHeap -> [(Name, Addr)] -> (TiHeap, Addr)
instantiate (App f x)    h g = alloc h'' (NAp f' x')
    where
        (h', f') = instantiate f h g
        (h'', x') = instantiate x h' g

instantiate (Var k)      h g =
    (h, fromMaybe (error $ "variable `" <> k <> "' not in scope") v)
    where v = lookup k g

instantiate (Con t a) h _ = alloc h $ NPrim "Pack" (ConP t a)

instantiate (Case _ _)   _ _ = error "cannot instantiate case expressions"

instantiate (Let NonRec bs e) h g = instantiate e h' (g' ++ g)
    where
        -- :t mapAccumL @[] @TiHeap @(Name, Expr) @(Name,Addr)
        --    :: (TiHeap -> (Name, Expr) -> (TiHeap, (Name, Addr)))
        --    -> TiHeap -> [(Name, Expr)] -> (TiHeap, [(Name, Addr)])
        (h', g') = mapAccumL instBinder h bs
        instBinder :: TiHeap -> Binding -> (TiHeap, (Name, Addr))
        instBinder h (k := v) =
            let (h',a) = instantiate v h g
            in (h',(k,a))

instantiate (Let Rec bs e) h g = instantiate e h' env
    where
        env = g' ++ g
        (h', g') = mapAccumL instBinder h bs
        instBinder :: TiHeap -> Binding -> (TiHeap, (Name, Addr))
        instBinder h (k := v) =
            let (h',a) = instantiate v h env
            in (h',(k,a))

instantiate (IntE n) h _ = alloc h (NNum n)

instantiate _ _ _ = error "unimplemented"

-- instantiate and update
instantiateU :: Expr -> Addr -> TiHeap -> [(Name, Addr)] -> TiHeap
instantiateU (App f x) root h g = update root (NAp f' x') h''
    where
        (h',f') = instantiate f h g
        (h'',x') = instantiate x h' g

instantiateU (Case _ _) _ _ _ = error "cannot instantiate case expressions"

instantiateU (Con t a) root h g = update root c h
    where
        c = NPrim "Pack" (ConP t a)

instantiateU (Var k) root h g = update root (NInd a) h'
    where (h',a) = instantiate (Var k) h g

-- i don't really know if this is correct tbh i'm gonna cry
instantiateU (Let NonRec bs e) root h g = h''
    where
        h'' = instantiateU e root h' (g' ++ g)
        (h', g') = mapAccumL instBinder h bs

        instBinder :: TiHeap -> Binding -> (TiHeap, (Name, Addr))
        instBinder h (k := v) =
            let (h',a) = instantiate v h g
            in (h',(k,a))

instantiateU (IntE n) root h _ = update root (NNum n) h

----------------------------------------------------------------------------------

eval :: TiState -> [TiState]
eval st = st : sts
    where
        sts | isFinal st = []
            | otherwise  = eval next
        next = doAdmin (step st)

step :: TiState -> TiState
step st =
    let TiState (top:_) _ h _ _ = st
    in case hLookupUnsafe top h of
        NNum n            -> numStep  n      st
        NAp f x           -> apStep   f x    st
        NSupercomb n as b -> scStep   n as b st
        NInd a            -> indStep  a      st
        NPrim n p         -> primStep n p    st
        NData t as        -> dataStep t as   st

    where
        numStep :: Int -> TiState -> TiState

        -- rule 2.7
        numStep _ (TiState [a] (s:d) h g sts) =
            case hLookupUnsafe a h of
                NNum n -> TiState s d h g sts

        numStep _ _ = error "number applied as function..."

        apStep :: Addr -> Addr -> TiState -> TiState

        apStep f _ (TiState (ap:s) d h g sts) =
            case hLookupUnsafe ap h of
                -- this is bad rewrite later :3
                -- rule 2.8
                NAp f (hViewUnsafe h -> NInd a) ->
                    TiState (ap:s) d h' g sts'
                    where
                        h' = update ap (NAp f a) h
                        sts' = sts & stsDereferences %~ succ

                _ ->
                    TiState (f:ap:s) d h g sts

        -- >> [ref/scStep]
        scStep :: Name -> [Name] -> Expr -> TiState -> TiState
        scStep n as e (TiState s d h g sts) =
            TiState s' d h' g sts
            where
                s' = rootAddr : drop (length as + 1) s  -- 3., 4.
                h' = instantiateU e rootAddr h env      -- 2.
                rootAddr = s !! length as

                env = argBinds ++ g                     -- 1.
                argBinds = as `zip` argAddrs
                argAddrs = getArgs h s
        -- << [ref/scStep]

        -- dereference indirections
        indStep :: Addr -> TiState -> TiState
        indStep a (TiState (_:s) d h g sts) =
            TiState (a:s) d h g sts'
            where sts' = sts & stsDereferences %~ succ

        primStep :: Name -> Prim -> TiState -> TiState
        primStep _ IntNegP (TiState s d h g sts) =
            case isDataNode arg of
                True  -> TiState s'' d h' g sts
                    where
                        h' = update rootAddr (NNum $ negate n) h
                        s'' = rootAddr : s'
                        (_:rootAddr:s') = s
                        NNum n = arg

                False -> TiState s'' d' h g sts
                    where
                        s'' = b : s'
                        NAp _ b = hLookupUnsafe a1 h
                        -- a1 is an NAp
                        (_:a1:s') = s
                        d' = [a1] : d
            where
                [argAddr] = getArgs h s
                arg = hLookupUnsafe argAddr h

        primStep _ IntAddP st = primArith (+) st
        primStep _ IntSubP st = primArith (-) st
        primStep _ IntMulP st = primArith (*) st
        primStep _ IntDivP st = primArith (div) st
        primStep _ IntEqP  st = primComp  (==) st

        primStep _ IfP (TiState s d h g sts) =
            case needsEval cn of
                True  -> TiState s' d' h g sts
                    where
                        s' = [c]
                        d' = drop 1 s : d
                False -> TiState s' d h' g sts
                    where
                        s' = drop 3 s
                        h' = update rootAddr res h
                        res = NInd $ if isTrue then t else f
                        rootAddr = head s'
                        isTrue = case cn of
                            -- see Core.Examples.corePrelude; True and False are
                            -- defined as Con 1 0 and Con 0 0, respectively
                            NData 0 [] -> False
                            NData 1 [] -> True
            where
                cn = hLookupUnsafe c h
                (c:t:f:_) = getArgs h s

        primStep _ CasePairP (TiState s d h g sts) =
            case needsEval pn of
                True  -> TiState s' d' h g sts
                    where
                        s' = [p]
                        d' = drop 1 s : d
                False -> TiState s' d h' g sts
                    where
                        -- TODO: maybe should be a drop 2 perhaps
                        s' = drop 1 s
                        h' = h & update a1 (NAp f x)
                               & update a2 (NAp a1 y)
                        rootAddr = head s'
                        a1 = s' !! 0
                        a2 = s' !! 1
                        NData 0 [x,y] = pn
            where
                (p:f:_) = getArgs h s
                pn = hLookupUnsafe p h

        primStep _ CaseListP (TiState s d h g sts) =
            case needsEval ln of
                True  -> TiState s' d' h g sts
                    where
                        s' = [l]
                        d' = drop 1 s : d
                False -> TiState s' d h' g sts
                    where
                        s' = drop 3 s
                        a2 = s !! 2
                        a3 = s !! 3
                        h' = case ln of
                            NData 1 [] ->
                                h & update a3 (NInd cn)
                            NData 2 [x,xs] ->
                                h & update a2 (NAp cc x)
                                  & update a3 (NAp a2 xs)
                            _ -> error "bad list"
            where
                (l:cn:cc:_) = getArgs h s
                ln = hLookupUnsafe l h

        primStep n (ConP t a) (TiState s d h g sts) =
            TiState s' d h' g sts
            where
                s' = drop a s
                h' = update rootAddr (NData t argAddrs) h
                rootAddr = s !! a
                argAddrs = getArgs h s

        primStep _ AbortP (TiState s d h g sts) =
            error "rl' called abort#!"

        dataStep :: Int -> [Addr] -> TiState -> TiState
        dataStep _ _ (TiState [a] (s:d) h g sts) = TiState s d h g sts

        dataStep _ _ _ = error "data applied as function..."

----------------------------------------------------------------------------------

-- EVERY ARGUMENT WILL BE EVALUATED!!!!
primArbitrary :: forall a. (PrimArbitraryType a) => a -> TiState -> TiState
primArbitrary f (TiState s d h g sts) = 
    TiState s' d' h' g sts
    where
        s' = case unevaled of
            Just (_,a) -> [a]
            Nothing    -> drop ar s
        d' = case unevaled of
            Just (i,_) -> drop i s : d
            Nothing    -> d
        h' = case unevaled of
            Just _     -> h
            Nothing    -> update rootAddr x h
                where x = onList f (fmap (\a -> hLookupUnsafe a h) argAddrs)

        unevaled = find (\ (_,a) -> needsEval $ hLookupUnsafe a h) ans
        ans = [1..] `zip` argAddrs
        argAddrs = getArgs h s
        rootAddr = s !! ar
        ar = arity (Proxy @a)

class PrimArbitraryType a where
    -- primArbitrary' :: a -> TiState -> TiState
    arity :: Proxy a -> Int
    -- runArb :: Node -> a
    onList :: a -> [Node] -> Node

instance PrimArbitraryType Node where
    arity _ = 0
    onList n [] = n
    onList _ _ = error "arity and list length do not match!"

instance (PrimArbitraryType a) => PrimArbitraryType (Node -> a) where
    arity _ = 1 + arity (Proxy @a)
    onList nf (a:as) = onList (nf a) as

primBinary :: (Node -> Node -> Node) -> TiState -> TiState
primBinary f (TiState s d h g sts) = 
    TiState s' d' h' g sts
    where
        s' | needsEval xarg    = [xAddr]
           | needsEval yarg    = [yAddr]
           | otherwise         = drop 2 s -- # of arguments

        h' | needsEval xarg    = h
           | needsEval yarg    = h
           | otherwise         = update rootAddr (xarg `f` yarg) h

        d' | needsEval xarg    = drop 1 s : d
           | needsEval yarg    = drop 2 s : d
           | otherwise         = d

        rootAddr = head s'

        needsEval = not . isDataNode

        [xAddr,yAddr] = getArgs h s
        xarg = hLookupUnsafe xAddr h
        yarg = hLookupUnsafe yAddr h

primComp :: (Int -> Int -> Bool) -> TiState -> TiState
primComp f = primBinary f'
    where
        f' (NNum a) (NNum b)
            | a `f` b   = NData 1 []
            | otherwise = NData 0 []

        f' _ _ = error "primComp expected number"

primArith :: (Int -> Int -> Int) -> TiState -> TiState
primArith f = primBinary f'
    where
        f' (NNum a) (NNum b) = NNum (a `f` b)
        f' _        _        = error "primArith expected number"

----------------------------------------------------------------------------------

getArgs :: TiHeap -> [Addr] -> [Addr]
getArgs h (_:s) = fmap f s
    where
        f addr = case hLookupUnsafe addr h of
            NAp _ arg -> arg
            _         -> error $ "major uh-oh: " ++ show addr

isFinal :: TiState -> Bool
isFinal (TiState [addr] [] h _ _) =
    case hLookup addr h of
        Just a -> isDataNode a
        _      -> error "isFinal: segfault!"
isFinal (TiState [] _ _ _ _) = error "empty stack..."
isFinal _ = False

isDataNode :: Node -> Bool
isDataNode (NNum _)    = True
isDataNode (NData _ _) = True
isDataNode _           = False

needsEval :: Node -> Bool
needsEval = not . isDataNode

-- TODO: count allocations
doAdmin :: TiState -> TiState
doAdmin = doStats
        . doGC
    where
        doStats = tiStats . stsReductions %~ succ
        doGC st
            | isHeapLarge st = gc st & tiStats . stsGCCycles %~ succ
            | otherwise      = st

-- | is the heap larger than some arbitrary number i declared "bloated"?
isHeapLarge :: TiState -> Bool
isHeapLarge st = length (st ^. tiHeap) >= triggerSize
    where triggerSize = 50

----------------------------------------------------------------------------------

dbgProg :: Program -> IO (Node, Stats)
dbgProg p = do
    prettyPrint `traverse` p'
    pure (res, sts)
    where
        p' = eval (fromJust $ compile p)
        TiState [resAddr] _ h _ sts = last p'
        res = hLookupUnsafe resAddr h

hdbgProg :: Program -> Handle -> IO (Node, Stats)
hdbgProg p hio = do
    -- lazy IO causes a funny quirk here! before we had the `hPrintf` call
    -- printing the final statistics, crashes would leave emit logs allowing us
    -- to see the state of the machine before an error. the statistics however,
    -- needs to be fully evaluated and before the logs are printed. a bit goofy.
    hPrintf hio "==== Stats ====\n\
                \result       : %s\n\
                \allocations  : %4d\n\
                \reductions   : %4d\n\
                \dereferences : %4d\n\
                \gc cycles    : %4d\n\n"
                (show res)
                (sts ^. stsAllocations)
                (sts ^. stsReductions)
                (sts ^. stsDereferences)
                (sts ^. stsGCCycles)
    (hPutStr hio . prettyShow) `traverse_` p'
    pure (res, sts)
    where
        p' = eval (fromJust $ compile p)
        TiState [resAddr] _ h _ sts = last p'
        res = hLookupUnsafe resAddr h

----------------------------------------------------------------------------------

instance Pretty TiState where
    prettyPrec (TiState s d h g sts) _ =
        (IStr $ printf "==== TiState Stack %d ====" no) <> IBreak
        <> mconcat (fmap ((<>IBreak) . showAddr) s)
        <> (IStr $ printf "==== TiState Heap %d ====" no) <> IBreak
        <> sheap <> IBreak
        where
            no :: Int
            no = sts ^. stsReductions

            showAddr a = IStr (show a) <> ": " <> pnode (hLookupUnsafe a h) 0
            -- showAddr a = IStr (show a) <> ": " <> IStr (show (hLookupUnsafe a h))
            sheap = mconcat $ ((<>IBreak) . showAddr) <$> addresses h

            pnode :: Node -> Int -> ISeq
            pnode (NAp f x) p = bracketPrec 0 p $
                f' <> " " <> pnode (hLookupUnsafe x h) (succ p)
                where
                    f' = case hLookupUnsafe f h of
                        x@(NAp _ _) -> pnode x       0
                        x           -> pnode x (succ p) 

            pnode (NInd a) p = bracketPrec 0 p $
                "NInd (" <> IStr (show a) <> ") -> " <> pnode (hLookupUnsafe a h) 0

            pnode (NNum n) _ =
                IStr (show n) <> IStr "#"

            pnode (NSupercomb n _ _) _ = IStr n

            pnode (NPrim n (ConP t a)) _ = IStr $ printf "%s{%d,%d}" n t a

            pnode (NPrim n _) _ = IStr n

            pnode (NData t cs) p = "NData{" <> IStr (show t) <> "} " <> m
                where
                    m = cs
                      & fmap (\a -> pnode (hLookupUnsafe a h) (succ p))
                      & intersperse " "
                      & mconcat

            pnode (NMarked n) p = bracketPrec 0 p $ "NMarked (" <> pnode n 0 <> ")"

----------------------------------------------------------------------------------

findRoots :: TiState -> [Addr]
findRoots (TiState s d _ g _) = stackRoots s <> dumpRoots d <> globalsRoots g
    where
        stackRoots :: Stack -> [Addr]
        stackRoots = id

        dumpRoots :: Dump -> [Addr]
        dumpRoots = foldMap stackRoots

        globalsRoots :: Env -> [Addr]
        globalsRoots = fmap snd

markFrom :: Addr -> TiHeap -> TiHeap
markFrom a h = case hLookup a h of
    Just (NMarked _) -> h
    Just n@(NAp l r) -> h & update a (NMarked n)
                          & markFrom l
                          & markFrom r
    -- note that we don't mark the indirection node itself.
    Just n@(NInd p) -> h & update a (NMarked n)
                         & markFrom p
    Just n@(NData _ as) -> h & update a (NMarked n)
                             & appEndo (foldMap Endo $ markFrom <$> as)
    Just n -> h & update a (NMarked n)

    -- we silently ignore dangling pointers without a ruckus as findRoots may
    -- scout the same address multiple times
    Nothing -> h

scanHeap :: TiHeap -> TiHeap
scanHeap h = appEndo (foldMap f $ addresses h) h
    where
        f a = case hLookupUnsafe a h of
            NMarked n -> Endo $ update a n
            _         -> Endo $ free a

gc :: TiState -> TiState
gc st@(TiState s d h g sts) = TiState s d h' g sts
    where
        as = findRoots st
        marked = h & appEndo (foldMap Endo $ markFrom <$> as)
        h' = scanHeap marked

