module EVM.Flatten (flatten) where
import EVM.Dapp (DappInfo, dappSources)
import EVM.Solidity (sourceAsts)
import Control.Lens (preview, view, universe)
import Data.Aeson (Value (String))
import Data.Aeson.Lens (key, _String, _Array)
import qualified Data.Graph.Inductive.Graph as Fgl
import qualified Data.Graph.Inductive.PatriciaTree as Fgl
import qualified Data.Graph.Inductive.Query.BFS as Fgl
import qualified Data.Graph.Inductive.Query.DFS as Fgl
import Control.Monad (forM)
import Data.ByteString (ByteString)
import Data.Foldable (foldl', toList)
import Data.List (sort)
import Data.Map (Map, (!))
import Data.Maybe (mapMaybe, isJust, fromMaybe)
import Data.Monoid ((<>))
import Data.Text (Text, unpack, pack, intercalate)
import Data.Text.Encoding (encodeUtf8)
import Text.Read (readMaybe)
import qualified Data.Map as Map
import qualified Data.Text as Text
import qualified Data.ByteString as BS
type FileGraph = Fgl.Gr Text ()
importsFrom :: Value -> [Text]
importsFrom ast =
  let
    
    
    allNodes :: [Value]
    allNodes = universe ast
    
    
    resolveImport :: Value -> Maybe Text
    resolveImport node =
      case preview (key "name") node of
        Just (String "ImportDirective") ->
          preview (key "attributes" . key "absolutePath" . _String) node
        _ ->
          Nothing
  
  in mapMaybe resolveImport allNodes
flatten :: DappInfo -> Text -> IO ()
flatten dapp target = do
  let
    
    graph :: FileGraph
    graph = Fgl.mkGraph nodes edges
    
    nodes :: [(Int, Text)]
    nodes = zip [1..] (Map.keys asts)
    
    edges =
      [ (indices ! s, indices ! t, ()) 
      | (s, v) <- Map.toList asts      
      , t      <- importsFrom v ]      
    
    indices :: Map Text Int
    indices = Map.fromList [(v, k) | (k, v) <- nodes]
    
    asts :: Map Text Value
    asts = view (dappSources . sourceAsts) dapp
  
  
  case Map.lookup target indices of
    Nothing ->
      error "didn't find contract AST"
    Just root -> do
      let
        
        
        subgraph :: Fgl.Gr Text ()
        subgraph = Fgl.subgraph (Fgl.bfs root graph) graph
        
        
        ordered :: [Text]
        ordered = reverse (Fgl.topsort' subgraph)
        
        pragma :: Text
        pragma = maximalPragma (Map.elems asts)
      
      
      sources <-
        forM ordered $ \path -> do
          src <- BS.readFile (unpack path)
          pure $ mconcat
            [ "////// ", encodeUtf8 path, "\n"
            , stripImportsAndPragmas src (asts ! path), "\n"
            ]
      
      putStrLn $ "// hevm: flattened sources of " <> unpack target
      putStrLn (unpack pragma)
      BS.putStr (mconcat sources)
maximalPragma :: [Value] -> Text
maximalPragma asts =
  case mapMaybe versions asts of
    [] -> error "no Solidity version pragmas in any source files"
    xs ->
      "pragma solidity ^"
        <> intercalate "." (map (pack . show) (maximum xs))
        <> ";\n"
  where
    
    
    versions :: Value -> Maybe [Int]
    versions ast = fmap grok components
      where
        pragma :: Maybe Value
        pragma =
          case filter (nodeIs "PragmaDirective") (universe ast) of
            [x] -> Just x
            []  -> Nothing
            _   -> error "multiple version pragmas"
        components :: Maybe [Value]
        components = fmap toList
          (pragma >>= preview (key "attributes" . key "literals" . _Array))
        grok :: [Value] -> [Int]
        grok = \case
          [String "solidity", String _prefix, String a, String b] ->
            map
              (fromMaybe
                 (error . Text.unpack $ "bad Solidity version: " <> a <> b)
                 . readAs)
              (Text.splitOn "." (a <> b))
          x ->
            error ("unrecognized pragma: " ++ show x)
nodeIs :: Text -> Value -> Bool
nodeIs t x = isSourceNode && hasRightName
  where
    isSourceNode =
      isJust (preview (key "src") x)
    hasRightName =
      Just t == preview (key "name" . _String) x
stripImportsAndPragmas :: ByteString -> Value -> ByteString
stripImportsAndPragmas bs ast = stripAstNodes bs ast p
  where
    p x = nodeIs "ImportDirective" x || nodeIs "PragmaDirective" x
stripAstNodes :: ByteString -> Value -> (Value -> Bool) -> ByteString
stripAstNodes bs ast p =
  cutRanges [sourceRange node | node <- universe ast, p node]
  where
    
    sourceRange :: Value -> (Int, Int)
    sourceRange v =
      case preview (key "src" . _String) v of
        Just (Text.splitOn ":" -> [readAs -> Just i, readAs -> Just n, _]) ->
          (i, i + n)
        _ ->
          error "internal error: no source position for AST node"
    
    
    cutRanges :: [(Int, Int)] -> ByteString
    cutRanges (sort -> rs) = fst (foldl' f (bs, 0) rs)
      where
        f (bs', n) (i, j) =
          ( cut bs' (i + n) (j + n)
          , n + length ("/*  */" :: String))
    
    cut :: ByteString -> Int -> Int -> ByteString
    cut x i j =
      let (a, b) = BS.splitAt i x
      in a <> "/* " <> BS.take (j - i) b <> " */" <> BS.drop (j - i) b
readAs :: Read a => Text -> Maybe a
readAs = readMaybe . Text.unpack