module Debug.Hoed.Render
(CompStmt(..)
,StmtDetails(..)
,stmtRes
,renderCompStmts
,CDS
,eventsToCDS
,noNewlines
,sortOn
) where
import           Control.DeepSeq
import           Control.Exception        (assert)
import           Control.Monad.Primitive
import           Control.Monad.ST
import           Control.Monad.Trans.Reader
import           Control.Monad.Trans.State.Strict
import           Data.Array               as Array
import           Data.Char                (isAlpha)
import           Data.Coerce
import           Data.Hashable
import           Data.List                (nub, sort, unfoldr)
import           Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import           Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import           Data.Primitive.MutVar
import           Data.Text (Text, pack, unpack)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Generic as VG
import           Data.Word
import           Debug.Hoed.Compat
import           Debug.Hoed.Observe
import           GHC.Exts(IsList(..))
import           GHC.Generics
import           Text.PrettyPrint.FPretty hiding (sep, (<$>), text)
import qualified Text.PrettyPrint.FPretty as FPretty
import           Text.Read ()
data CompStmt = CompStmt { stmtLabel      :: !Text
                         , stmtIdentifier :: !UID
                         , stmtDetails    :: !StmtDetails
                         }
                deriving (Generic)
instance NFData CompStmt
instance Eq CompStmt where c1 == c2 = stmtIdentifier c1 == stmtIdentifier c2
instance Ord CompStmt where
  compare c1 c2 = compare (stmtIdentifier c1) (stmtIdentifier c2)
instance Hashable CompStmt where
  hashWithSalt s cs = hashWithSalt s (stmtIdentifier cs)
data StmtDetails
  = StmtCon { stmtCon :: Hashed Text
           ,  stmtPretty :: Hashed Text}
  | StmtLam { stmtLamArgs :: [Hashed Text]
           ,  stmtLamRes :: Hashed Text
           ,  stmtPretty :: Hashed Text}
  deriving (Generic)
instance NFData StmtDetails
stmtRes :: CompStmt -> Text
stmtRes = unhashed . stmtPretty . stmtDetails
instance Show CompStmt where
  show = unpack . stmtRes
  showList eqs eq = unlines (map show eqs) ++ eq
noNewlines :: String -> String
noNewlines = noNewlines' False
noNewlines' :: Bool -> String -> String
noNewlines' _ [] = []
noNewlines' w (s:ss)
 | w       && (s == ' ' || s == '\n') =       noNewlines' True ss
 | not w && (s == ' ' || s == '\n') = ' ' : noNewlines' True ss
 | otherwise                          = s   : noNewlines' False ss
renderCompStmts :: (?statementWidth::Int) => CDSSet -> [CompStmt]
renderCompStmts cdss = runMemoM $ concat <$> mapM renderCompStmt cdss
renderCompStmt :: (?statementWidth::Int) => CDS -> MemoM [CompStmt]
renderCompStmt (CDSNamed name uid set) = do
        let output = cdssToOutput set
        concat <$> mapM (renderNamedTop name uid) output
renderCompStmt other = error $ show other
prettySet cds = prettySet_noid(coerce cds)
prettySet_noid :: (?statementWidth::Int) => [CDSsansUID] -> MemoM(Hashed Text)
prettySet_noid = MemoM . memo (prettyW . renderSet . coerce)
renderNamedTop :: (?statementWidth::Int) => Text -> UID -> Output -> MemoM [CompStmt]
renderNamedTop name observeUid (OutData cds) = mapM f pairs
  where
    f (args, res, Just i) =
      CompStmt name i <$>
      (StmtLam <$> mapM prettySet args <*>
       prettySet res <*>
       pure (prettyW $ renderNamedFn name (args, res)))
    f (_, cons, Nothing) =
      CompStmt name observeUid <$>
      (StmtCon <$> prettySet cons <*>
       pure (prettyW $ renderNamedCons name cons))
    pairs = (nubSorted . sortOn argAndRes) pairs'
    pairs' = findFn [cds]
    argAndRes (arg, res, _) = (arg, res)
renderNamedTop name _ other = error $ show other
nubSorted :: Eq a => [a] -> [a]
nubSorted []        = []
nubSorted (a:a':as) | a == a' = nubSorted (a' : as)
nubSorted (a:as)    = a : nubSorted as
data CDS = CDSNamed      !Text !UID    !CDSSet
         | CDSCons       !UID  !Text   ![CDSSet]
         | CDSFun        !UID  !CDSSet !CDSSet
         | CDSEntered    !UID
         | CDSTerminated !UID
         | CDSChar       !Char   
         | CDSString     !String 
        deriving (Show,Eq,Ord,Generic)
instance NFData CDS
normalizeCDS :: CDS -> CDS
normalizeCDS (CDSString s) = CDSCons 0 (pack $ show s) []
normalizeCDS (CDSChar   s) = CDSCons 0 (pack $ show s) []
normalizeCDS other = other
type CDSSet = [CDS]
data ParentList = ParentCons !Int !Word8 ParentList | ParentNil
instance IsList ParentList where
  type Item ParentList = Parent
  toList = unfoldr (\case ParentNil -> Nothing ; ParentCons pp pc t -> Just (Parent pp pc,t))
  fromList = foldr (\(Parent pp pc) t -> ParentCons pp pc t) ParentNil
eventsToCDS :: Trace -> CDSSet
eventsToCDS pairs = getChild (1) 0
   where
     
     res i = getNode'' i (change (pairs VG.! i))
     mid_arr :: V.Vector ParentList
     mid_arr = VG.unsafeAccumulate
                  (\i (Parent pp pc) -> ParentCons pp pc i)
                  (V.replicate (VG.length pairs) ParentNil)
                  ( VG.map (\(node, Event (Parent pnode pport) _) ->
                              (pnode+1, Parent node pport))
                  $ VG.filter (\(_,e) -> change e /= Enter)
                  $ VG.convert
                  $ VG.indexed pairs)
     getNode'' ::  Int -> Change -> CDS
     getNode'' node change =
       case change of
        Observe str         -> let chd = normalizeCDS <$> getChild node 0
                               in CDSNamed str (getId chd node) chd
        Enter               -> CDSEntered node
        Fun                 -> CDSFun node (normalizeCDS <$> getChild node 0)
                                           (normalizeCDS <$> getChild node 1)
        ConsChar char       -> CDSChar char
        Cons portc cons
                            -> simplifyCons node cons
                                 [ getChild node (fromIntegral n)
                                 | n <- [0::Int .. fromIntegral portc  1]]
     getId []                 i  = i
     getId (CDSFun i _ _:_) _    = i
     getId (_:cs)             i  = getId cs i
     getChild :: Int -> Word8 -> CDSSet
     getChild pnode pport =
       [ res content
       | Parent content pport' <- toList $ mid_arr VG.! succ pnode
       , pport == pport'
       ]
simplifyCons :: UID -> Text -> [CDSSet] -> CDS
simplifyCons _ "throw" [[CDSCons _ "ErrorCall" set]]
  = CDSCons 0 "error" set
simplifyCons _ ":" [[CDSChar !ch], [CDSCons _ "[]" []]]
  = CDSString [ch]
simplifyCons _ ":" [[CDSChar !ch], [CDSString s]]
  = CDSString (ch:s)
simplifyCons uid con xx = CDSCons uid con (map (map normalizeCDS) xx)
render :: Int -> Bool -> CDS -> Doc
render prec par (CDSCons _ ":" [cds1,cds2]) =
        if par && not needParen
        then doc 
        else paren needParen doc
   where
        doc = grp (renderSet' 5 False cds1 <> text " : ") <>
              renderSet' 4 True cds2
        needParen = prec > 4
render prec par (CDSCons _ "," cdss) | length cdss > 0 =
        nest 2 (text "(" <> foldl1 (\ a b -> a <> text ", " <> b)
                            (map renderSet cdss) <>
                text ")")
render prec _par (CDSCons _ name cdss)
  | not (T.null name)
  , (not . isAlpha . T.head) name && length cdss > 1 = 
        paren (prec /= 0)
                  (grp
                    (renderSet' 10 False (head cdss)
                     <> sep <> text name
                     <> nest 2 (foldr (<>) nil
                                 [ if null cds then nil else sep <> renderSet' 10 False cds
                                 | cds <- tail cdss
                                 ]
                              )
                    )
                  )
  | otherwise = 
        paren (not (null cdss) && prec /= 0)
                 ( grp
                   (text name <> nest 2 (foldr (<>) nil
                                          [ sep <> renderSet' 10 False cds
                                          | cds <- cdss
                                          ]
                                       )
                   )
                 )
renderSet :: CDSSet -> Doc
renderSet = renderSet' 0 False
renderSet' :: Int -> Bool -> CDSSet -> Doc
renderSet' _ _      [] = text "_"
renderSet' prec par [cons@(CDSCons {})]    = render prec par cons
renderSet' prec par cdss                   =
        nest 0 (text "{ " <> foldl1 (\ a b -> a <> line <>
                                    text ", " <> b)
                                    (map renderFn pairs) <>
                line <> text "}")
   where
        findFn_noUIDs :: CDSSet -> [([CDSSet],CDSSet)]
        findFn_noUIDs c = map (\(a,r,_) -> (a,r)) (findFn c)
        pairs = nub (sort (findFn_noUIDs cdss))
        
        nub []        = []
        nub (a:a':as) | a == a' = nub (a' : as)
        nub (a:as)    = a : nub as
renderFn :: ([CDSSet],CDSSet) -> Doc
renderFn (args, res)
        = grp  (nest 3
                (text "\\ " <>
                 foldr (\ a b -> nest 0 (renderSet' 10 False a) <> sp <> b)
                       nil
                       args <> softline <>
                 text "-> " <> renderSet' 0 False res
                )
               )
renderNamedCons :: Text -> CDSSet -> Doc
renderNamedCons name cons
  = text name <> nest 2
     ( sep <> grp (text "= " <> renderSet cons)
     )
renderNamedFn :: Text -> ([CDSSet],CDSSet) -> Doc
renderNamedFn name (args,res)
  = text name <> nest 2
     ( sep <> foldr (\ a b -> grp (renderSet' 10 False a) <> sep <> b) nil args
       <> sep <> grp ("= " <> align(renderSet res))
     )
findFn :: CDSSet -> [([CDSSet],CDSSet, Maybe UID)]
findFn = foldr findFn' []
findFn' :: CDS -> [([CDSSet], CDSSet, Maybe UID)] -> [([CDSSet], CDSSet, Maybe UID)]
findFn' (CDSFun i arg res) rest =
    case findFn res of
       [(args',res',_)] -> (arg : args', res', Just i) : rest
       _                -> ([arg], res, Just i) : rest
findFn' other rest = ([],[other], Nothing) : rest
paren :: Bool -> Doc -> Doc
paren False doc = grp (nest 0 doc)
paren True  doc = grp (text "(" <> doc <> text ")")
data Output = OutLabel Text CDSSet [Output]
            | OutData  CDS
              deriving (Eq,Ord,Show)
cdssToOutput :: CDSSet -> [Output]
cdssToOutput =  map cdsToOutput
cdsToOutput :: CDS -> Output
cdsToOutput (CDSNamed name _ cdsset)
            = OutLabel name res1 res2
  where
      res1 = [ cdss | (OutData cdss) <- res ]
      res2 = [ out  | out@OutLabel {} <- res ]
      res  = cdssToOutput cdsset
cdsToOutput cons@CDSCons {} = OutData cons
cdsToOutput    fn@CDSFun {} = OutData fn
nil :: Doc
nil = Text.PrettyPrint.FPretty.empty
grp :: Doc -> Doc
grp = Text.PrettyPrint.FPretty.group
sep :: Doc
sep = softline  
sp :: Doc
sp = text " "   
text = FPretty.text . unpack
prettyW :: (?statementWidth::Int) => Doc -> (Hashed Text)
prettyW doc = hashed $ pack $ pretty ?statementWidth doc
newtype CDSsansUID = CDSsansUID CDS
instance Eq CDSsansUID where
  CDSsansUID(CDSNamed t _ xx) == CDSsansUID(CDSNamed t' _ yy) =
    t == t' && coerce xx == (coerce yy :: [CDSsansUID])
  CDSsansUID (CDSCons _ t xx) == CDSsansUID(CDSCons _ t' yy)  =
    t == t'  && coerce xx == (coerce yy :: [[CDSsansUID]])
  CDSsansUID (CDSFun _ res args) == CDSsansUID (CDSFun _ res' args') =
    (coerce res :: [CDSsansUID]) == coerce res' && coerce args == (coerce args' :: [CDSsansUID])
  CDSsansUID x == CDSsansUID y = x == y
instance Ord CDSsansUID where
  CDSsansUID (CDSNamed t _ xx) `compare` CDSsansUID (CDSNamed t' _ yy) =
    (t, coerce xx :: [CDSsansUID]) `compare` (t', coerce yy)
  CDSsansUID (CDSCons _ t xx) `compare` CDSsansUID (CDSCons _ t' yy) =
    (t, coerce xx :: [[CDSsansUID]]) `compare` (t', coerce yy)
  CDSsansUID (CDSFun _ args res) `compare` CDSsansUID (CDSFun _ args' res') =
    (coerce args :: [CDSsansUID], coerce res :: [CDSsansUID]) `compare` (coerce args', coerce res')
  CDSsansUID x `compare` CDSsansUID y = x `compare` y
instance Hashable CDSsansUID where
  s `hashWithSalt` CDSsansUID (CDSNamed t _ xx) = s `hashWithSalt` t `hashWithSalt` (coerce xx :: [CDSsansUID])
  s `hashWithSalt` CDSsansUID (CDSCons _  t xx) = s `hashWithSalt` t `hashWithSalt` (coerce xx :: [[CDSsansUID]])
  s `hashWithSalt` CDSsansUID (CDSFun _ args res) = s `hashWithSalt` (coerce args :: [CDSsansUID]) `hashWithSalt` (coerce res :: [CDSsansUID])
newtype MemoM a = MemoM (State (Map [CDSsansUID] (Hashed Text)) a) deriving (Applicative, Functor, Monad)
runMemoM :: MemoM a -> a
runMemoM (MemoM comp) = evalState comp mempty
memo :: Ord a => (a->b) -> a -> State (Map a b) b
memo f a = do
  table <- get
  case Map.lookup a table of
    Just b -> return b
    Nothing -> do
      let b = f a
      modify (Map.insert a b)
      return b