{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DerivingStrategies #-}
module Development.IDE.Types.Exports
(
    IdentInfo(..),
    ExportsMap(..),
    rendered,
    moduleNameText,
    occNameText,
    renderOcc,
    mkTypeOcc,
    mkVarOrDataOcc,
    isDatacon,
    createExportsMap,
    createExportsMapMg,
    buildModuleExportMapFrom,
    createExportsMapHieDb,
    size,
    exportsMapSize,
    updateExportsMapMg
    ) where

import           Control.DeepSeq             (NFData (..), force, ($!!))
import           Control.Monad
import           Data.Char                   (isUpper)
import           Data.Hashable               (Hashable)
import           Data.HashSet                (HashSet)
import qualified Data.HashSet                as Set
import           Data.List                   (isSuffixOf)
import           Data.Text                   (Text, uncons)
import           Data.Text.Encoding          (decodeUtf8, encodeUtf8)
import           Development.IDE.GHC.Compat
import           Development.IDE.GHC.Orphans ()
import           GHC.Generics                (Generic)
import           HieDb                       hiding (withHieDb)
import           Prelude                     hiding (mod)


data ExportsMap = ExportsMap
    { getExportsMap       :: !(OccEnv (HashSet IdentInfo))
    , getModuleExportsMap :: !(ModuleNameEnv (HashSet IdentInfo))
    }

instance NFData ExportsMap where
  rnf (ExportsMap a b) = nonDetFoldOccEnv (\c d -> rnf c `seq` d) (seqEltsUFM rnf b) a

instance Show ExportsMap where
  show (ExportsMap occs mods) =
    unwords [ "ExportsMap { getExportsMap ="
            , printWithoutUniques $ mapOccEnv (textDoc . show) occs
            , "getModuleExportsMap ="
            , printWithoutUniques $ mapUFM (textDoc . show) mods
            , "}"
            ]

-- | `updateExportsMap old new` results in an export map containing
-- the union of old and new, but with all the module entries new overriding
-- those in old.
updateExportsMap :: ExportsMap -> ExportsMap -> ExportsMap
updateExportsMap old new = ExportsMap
  { getExportsMap = delListFromOccEnv (getExportsMap old) old_occs `plusOccEnv` getExportsMap new -- plusOccEnv is right biased
  , getModuleExportsMap = getModuleExportsMap old `plusUFM` getModuleExportsMap new -- plusUFM is right biased
  }
  where old_occs = concat [map name $ Set.toList (lookupWithDefaultUFM_Directly (getModuleExportsMap old) mempty m_uniq)
                          | m_uniq <- nonDetKeysUFM (getModuleExportsMap new)]

size :: ExportsMap -> Int
size = sum . map Set.size . nonDetOccEnvElts . getExportsMap

mkVarOrDataOcc :: Text -> OccName
mkVarOrDataOcc t = mkOcc $ mkFastStringByteString $ encodeUtf8 t
  where
    mkOcc
      | Just (c,_) <- uncons t
      , c == ':' || isUpper c = mkDataOccFS
      | otherwise = mkVarOccFS

mkTypeOcc :: Text -> OccName
mkTypeOcc t = mkTcOccFS $ mkFastStringByteString $ encodeUtf8 t

exportsMapSize :: ExportsMap -> Int
exportsMapSize = nonDetFoldOccEnv (\_ x -> x+1) 0 . getExportsMap

instance Semigroup ExportsMap where
  ExportsMap a b <> ExportsMap c d = ExportsMap (plusOccEnv_C (<>) a c) (plusUFM_C (<>) b d)

instance Monoid ExportsMap where
  mempty = ExportsMap emptyOccEnv emptyUFM

rendered :: IdentInfo -> Text
rendered = occNameText . name

-- | Render an identifier as imported or exported style.
-- TODO: pattern synonymoccNameText :: OccName -> Text
occNameText :: OccName -> Text
occNameText name
  | isSymOcc name = "(" <> renderedOcc <> ")"
  | isTcOcc name && isSymOcc name = "type (" <> renderedOcc <> ")"
  | otherwise = renderedOcc
  where
    renderedOcc = renderOcc name

renderOcc :: OccName -> Text
renderOcc = decodeUtf8 . bytesFS . occNameFS

moduleNameText :: IdentInfo -> Text
moduleNameText = moduleNameText' . identModuleName

moduleNameText' :: ModuleName -> Text
moduleNameText' = decodeUtf8 . bytesFS . moduleNameFS

data IdentInfo = IdentInfo
    { name            :: !OccName
    , parent          :: !(Maybe OccName)
    , identModuleName :: !ModuleName
    }
    deriving (Generic, Show)
    deriving anyclass Hashable

isDatacon :: IdentInfo -> Bool
isDatacon = isDataOcc . name

instance Eq IdentInfo where
    a == b = name a == name b
          && parent a == parent b
          && identModuleName a == identModuleName b

instance NFData IdentInfo where
    rnf IdentInfo{..} =
        -- deliberately skip the rendered field
        rnf name `seq` rnf parent `seq` rnf identModuleName

mkIdentInfos :: ModuleName -> AvailInfo -> [IdentInfo]
mkIdentInfos mod (AvailName n) =
    [IdentInfo (nameOccName n) Nothing mod]
mkIdentInfos mod (AvailFL fl) =
    [IdentInfo (nameOccName n) Nothing mod]
    where
      n = flSelector fl
mkIdentInfos mod (AvailTC parent (n:nn) flds)
    -- Following the GHC convention that parent == n if parent is exported
    | n == parent
    = [ IdentInfo (nameOccName name) (Just $! nameOccName parent) mod
        | name <- nn ++ map flSelector flds
      ] ++
      [ IdentInfo (nameOccName n) Nothing mod]

mkIdentInfos mod (AvailTC _ nn flds)
    = [ IdentInfo (nameOccName n) Nothing mod
        | n <- nn ++ map flSelector flds
      ]

createExportsMap :: [ModIface] -> ExportsMap
createExportsMap modIface = do
  let exportList = concatMap doOne modIface
  let exportsMap = mkOccEnv_C (<>) $ map (\(a,_,c) -> (a, c)) exportList
  force $ ExportsMap exportsMap $ buildModuleExportMap $ map (\(_,b,c) -> (b, c)) exportList -- UFM is lazy, so need to seq
  where
    doOne modIFace = do
      let getModDetails = unpackAvail $ moduleName $ mi_module modIFace
      concatMap getModDetails (mi_exports modIFace)

createExportsMapMg :: [ModGuts] -> ExportsMap
createExportsMapMg modGuts = do
  let exportList = concatMap doOne modGuts
  let exportsMap = mkOccEnv_C (<>) $ map (\(a,_,c) -> (a, c)) exportList
  force $ ExportsMap exportsMap $ buildModuleExportMap $ map (\(_,b,c) -> (b, c)) exportList -- UFM is lazy, so need to seq
  where
    doOne mi = do
      let getModuleName = moduleName $ mg_module mi
      concatMap (unpackAvail getModuleName) (mg_exports mi)

updateExportsMapMg :: [ModGuts] -> ExportsMap -> ExportsMap
updateExportsMapMg modGuts old = updateExportsMap old new
    where
        new = createExportsMapMg modGuts

nonInternalModules :: ModuleName -> Bool
nonInternalModules = not . (".Internal" `isSuffixOf`) . moduleNameString

type WithHieDb = forall a. (HieDb -> IO a) -> IO a

createExportsMapHieDb :: WithHieDb -> IO ExportsMap
createExportsMapHieDb withHieDb = do
    mods <- withHieDb getAllIndexedMods
    idents' <- forM (filter (nonInternalModules . modInfoName . hieModInfo) mods) $ \m -> do
        let mn = modInfoName $ hieModInfo m
        fmap (unwrap mn) <$> withHieDb (\hieDb -> getExportsForModule hieDb mn)
    let idents = concat idents'
    let exportsMap = mkOccEnv_C (<>) (keyWith name idents)
    return $!! ExportsMap exportsMap $ buildModuleExportMap (keyWith identModuleName idents) -- UFM is lazy so need to seq
  where
    unwrap m ExportRow{..} = IdentInfo exportName exportParent m
    keyWith f xs = [(f x, Set.singleton x) | x <- xs]

unpackAvail :: ModuleName -> IfaceExport -> [(OccName, ModuleName, HashSet IdentInfo)]
unpackAvail mn
  | nonInternalModules mn = map f . mkIdentInfos mn
  | otherwise = const []
  where
    f identInfo@IdentInfo {..} = (name, mn, Set.singleton identInfo)


identInfoToKeyVal :: IdentInfo -> (ModuleName, IdentInfo)
identInfoToKeyVal identInfo =
  (identModuleName identInfo, identInfo)

buildModuleExportMap:: [(ModuleName, HashSet IdentInfo)] -> ModuleNameEnv (HashSet IdentInfo)
buildModuleExportMap exportsMap = do
  let lst = concatMap (Set.toList . snd) exportsMap
  let lstThree = map identInfoToKeyVal lst
  sortAndGroup lstThree

buildModuleExportMapFrom:: [ModIface] -> ModuleNameEnv (HashSet IdentInfo)
buildModuleExportMapFrom modIfaces = do
  let exports = map extractModuleExports modIfaces
  listToUFM_C (<>) exports

extractModuleExports :: ModIface -> (ModuleName, HashSet IdentInfo)
extractModuleExports modIFace = do
  let modName = moduleName $ mi_module modIFace
  let functionSet = Set.fromList $ concatMap (mkIdentInfos modName) $ mi_exports modIFace
  (modName, functionSet)

sortAndGroup :: [(ModuleName, IdentInfo)] -> ModuleNameEnv (HashSet IdentInfo)
sortAndGroup assocs = listToUFM_C (<>) [(k, Set.singleton v) | (k, v) <- assocs]
