{-# LANGUAGE OverloadedStrings #-}
module Futhark.Doc.Generator (renderFiles) where

import Control.Arrow ((***))
import Control.Monad
import Control.Monad.Reader
import Control.Monad.Writer
import Data.List (sort, sortOn, intersperse, inits, tails, isPrefixOf, find, groupBy, partition)
import Data.Char (isSpace, isAlpha, toUpper)
import Data.Loc
import Data.Maybe
import Data.Ord
import qualified Data.Map as M
import qualified Data.Set as S
import System.FilePath (splitPath, (</>), (-<.>), (<.>), makeRelative)
import Text.Blaze.Html5 (AttributeValue, Html, (!), toHtml)
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import Data.String (fromString)
import Data.Version
import qualified Data.Text.Lazy as LT
import Text.Markdown

import Prelude hiding (abs)

import Language.Futhark.Semantic
import Language.Futhark.TypeChecker.Monad hiding (warn)
import Language.Futhark
import Futhark.Doc.Html
import Futhark.Version

-- | A set of names that we should not generate links to, because they
-- are uninteresting.  These are for example type parameters.
type NoLink = S.Set VName

data Context = Context { ctxCurrent :: String
                       , ctxFileMod :: FileModule
                       , ctxImports :: Imports
                       , ctxNoLink :: NoLink
                       , ctxFileMap :: FileMap
                       , ctxVisibleMTys :: S.Set VName
                         -- ^ Local module types that show up in the
                         -- interface.  These should be documented,
                         -- but clearly marked local.
                       }
type FileMap = M.Map VName (String, Namespace)
type DocM = ReaderT Context (WriterT Documented (Writer Warnings))

data IndexWhat = IndexValue | IndexFunction | IndexModule | IndexModuleType | IndexType

-- | We keep a mapping of the names we have actually documented, so we
-- can generate an index.
type Documented = M.Map VName IndexWhat

warn :: SrcLoc -> String -> DocM ()
warn loc s = lift $ lift $ tell $ singleWarning loc s

document :: VName -> IndexWhat -> DocM ()
document v what = tell $ M.singleton v what

noLink :: [VName] -> DocM a -> DocM a
noLink names = local $ \ctx ->
  ctx { ctxNoLink = S.fromList names <> ctxNoLink ctx }

selfLink :: AttributeValue -> Html -> Html
selfLink s = H.a ! A.id s ! A.href ("#" <> s) ! A.class_ "self_link"

fullRow :: Html -> Html
fullRow = H.tr . (H.td ! A.colspan "3")

emptyRow :: Html
emptyRow = H.tr $ H.td mempty <> H.td mempty <> H.td mempty

specRow :: Html -> Html -> Html -> Html
specRow a b c = H.tr $ (H.td ! A.class_ "spec_lhs") a <>
                       (H.td ! A.class_ "spec_eql") b <>
                       (H.td ! A.class_ "spec_rhs") c

vnameToFileMap :: Imports -> FileMap
vnameToFileMap = mconcat . map forFile
  where forFile (file, FileModule abs file_env _prog) =
          mconcat (map (vname Type) (M.keys abs)) <>
          forEnv file_env
          where vname ns v = M.singleton (qualLeaf v) (file, ns)
                vname' ((ns, _), v) = vname ns v

                forEnv env =
                  mconcat (map vname' $ M.toList $ envNameMap env) <>
                  mconcat (map forMty $ M.elems $ envSigTable env)
                forMod (ModEnv env) = forEnv env
                forMod ModFun{} = mempty
                forMty = forMod . mtyMod

renderFiles :: [FilePath] -> Imports -> ([(FilePath, Html)], Warnings)
renderFiles important_imports imports = runWriter $ do
  (import_pages, documented) <- runWriterT $ forM imports $ \(current, fm) ->
    let ctx = Context current fm imports mempty file_map
              (progModuleTypes $ fileProg fm) in
    flip runReaderT ctx $ do

    (first_paragraph, maybe_abstract, maybe_sections) <- headerDoc $ fileProg fm

    synopsis <- (H.div ! A.id "module") <$> synopsisDecs (progDecs $ fileProg fm)

    description <- describeDecs $ progDecs $ fileProg fm

    return (current,
            (H.docTypeHtml ! A.lang "en" $
             addBoilerplateWithNav important_imports imports ("doc" </> current) current $
             H.main $ maybe_abstract <>
             selfLink "synopsis" (H.h2 "Synopsis") <> (H.div ! A.id "overview") synopsis <>
             selfLink "description" (H.h2 "Description") <> description <>
             maybe_sections,
             first_paragraph))

  return $
    [("index.html", contentsPage important_imports $ map (fmap snd) import_pages),
     ("doc-index.html", indexPage important_imports imports documented file_map)]
    ++ map (importHtml *** fst) import_pages
  where file_map = vnameToFileMap imports
        importHtml import_name = "doc" </> import_name <.> "html"

-- | The header documentation (which need not be present) can contain
-- an abstract and further sections.
headerDoc :: Prog -> DocM (Html, Html, Html)
headerDoc prog =
  case progDoc prog of
    Just (DocComment doc loc) -> do
      let (abstract, more_sections) = splitHeaderDoc doc
      first_paragraph <- docHtml $ Just $ DocComment (firstParagraph abstract) loc
      abstract' <- docHtml $ Just $ DocComment abstract loc
      more_sections' <- docHtml $ Just $ DocComment more_sections loc
      return (first_paragraph,
              selfLink "abstract" (H.h2 "Abstract") <> abstract',
              more_sections')
    _ -> return mempty
  where splitHeaderDoc s = fromMaybe (s, mempty) $
                           find (("\n##" `isPrefixOf`) . snd) $
                           zip (inits s) (tails s)
        firstParagraph = unlines . takeWhile (not . paragraphSeparator) . lines
        paragraphSeparator = all isSpace


contentsPage :: [FilePath] -> [(String, Html)] -> Html
contentsPage important_imports pages =
  H.docTypeHtml $ addBoilerplate "index.html" "Futhark Library Documentation" $
  H.main $ H.h2 "Main libraries" <>
  fileList important_pages <>
  if null unimportant_pages then mempty else
    H.h2 "Supporting libraries" <>
    fileList unimportant_pages
  where (important_pages, unimportant_pages) =
          partition ((`elem` important_imports) . fst) pages

        fileList pages' =
          H.dl ! A.class_ "file_list" $
          mconcat $ map linkTo $ sortOn fst pages'

        linkTo (name, maybe_abstract) =
          H.div ! A.class_ "file_desc" $
          (H.dt ! A.class_ "desc_header") (importLink "index.html" name) <>
          (H.dd ! A.class_ "desc_doc") maybe_abstract

importLink :: FilePath -> String -> Html
importLink current name =
  let file = relativise (makeRelative "/" $ "doc" </> name -<.> "html") current
  in (H.a ! A.href (fromString file) $ fromString name)

indexPage :: [FilePath] -> Imports -> Documented -> FileMap -> Html
indexPage important_imports imports documented fm =
  H.docTypeHtml $ addBoilerplateWithNav important_imports imports "doc-index.html" "Index" $
  H.main $
  (H.ul ! A.id "doc_index_list" $
   mconcat $ map initialListEntry $
   letter_group_links ++ [symbol_group_link]) <>
  (H.table ! A.id "doc_index" $
   H.thead (H.tr $ H.td "Who" <> H.td "What" <> H.td "Where") <>
   mconcat (letter_groups ++ [symbol_group]))
  where (letter_names, sym_names) =
          partition (isLetterName . baseString . fst) $
          sortOn (map toUpper . baseString . fst) $
          mapMaybe isDocumented $ M.toList fm

        isDocumented (k, (file, _)) = do
          what <- M.lookup k documented
          Just (k, (file, what))

        (letter_groups, letter_group_links) =
          unzip $ map tbodyForNames $ groupBy sameInitial letter_names
        (symbol_group, symbol_group_link) =
          tbodyForInitial "Symbols" sym_names

        isLetterName [] = False
        isLetterName (c:_) = isAlpha c

        sameInitial (x, _) (y, _) =
          case (baseString x, baseString y) of
            (x':_, y':_) -> toUpper x' == toUpper y'
            _            -> False

        tbodyForNames names@((s,_):_) =
          tbodyForInitial (map toUpper $ take 1 $ baseString s) names
        tbodyForNames _ = mempty

        tbodyForInitial initial names =
          (H.tbody $ mconcat $ initial' : map linkTo names,
           initial)
          where initial' =
                  H.tr $ H.td ! A.colspan "2" ! A.class_ "doc_index_initial" $
                  H.a ! A.id (fromString initial)
                      ! A.href (fromString $ '#' : initial)
                      $ fromString initial

        initialListEntry initial =
          H.li $ H.a ! A.href (fromString $ '#' : initial) $ fromString initial

        linkTo (name, (file, what)) =
          let link = (H.a ! A.href (fromString (makeRelative "/" $ "doc" </> vnameLink' name "" file))) $
                     fromString $ baseString name
              what' = case what of IndexValue -> "value"
                                   IndexFunction -> "function"
                                   IndexType -> "type"
                                   IndexModuleType -> "module type"
                                   IndexModule -> "module"
              html_file = makeRelative "/" $ "doc" </> file -<.> "html"
          in H.tr $
             (H.td ! A.class_ "doc_index_name" $ link) <>
             (H.td ! A.class_ "doc_index_namespace" $ what') <>
             (H.td ! A.class_ "doc_index_file" $
              (H.a ! A.href (fromString html_file) $ fromString file))

addBoilerplate :: String -> String -> Html -> Html
addBoilerplate current titleText content =
  let headHtml = H.head $
                 H.meta ! A.charset "utf-8" <>
                 H.title (fromString titleText) <>
                 H.link ! A.href (fromString $ relativise "style.css" current)
                        ! A.rel "stylesheet"
                        ! A.type_ "text/css"

      navigation = H.ul ! A.id "navigation" $
                   H.li (H.a ! A.href (fromString $ relativise "index.html" current) $ "Contents") <>
                   H.li (H.a ! A.href (fromString $ relativise "doc-index.html" current) $ "Index")

      madeByHtml =
        "Generated by " <> (H.a ! A.href futhark_doc_url) "futhark-doc"
        <> " " <> fromString (showVersion version)
  in headHtml <>
     H.body ((H.div ! A.id "header") (H.h1 (toHtml titleText) <> navigation) <>
             (H.div ! A.id "content") content <>
             (H.div ! A.id "footer") madeByHtml)
  where futhark_doc_url =
          "https://futhark.readthedocs.io/en/latest/man/futhark-doc.html"

addBoilerplateWithNav :: [FilePath] -> Imports -> String -> String -> Html -> Html
addBoilerplateWithNav important_imports imports current titleText content =
  addBoilerplate current titleText $
  (H.nav ! A.id "filenav" $ files) <> content
  where files = H.ul $ mconcat $ map pp $ sort $ filter visible important_imports
        pp name = H.li $ importLink current name
        visible = (`elem` map fst imports)

synopsisDecs :: [Dec] -> DocM Html
synopsisDecs decs = do
  visible <- asks ctxVisibleMTys
  fm <- asks ctxFileMod
  -- We add an empty row to avoid generating invalid HTML in cases
  -- where all rows are otherwise colspan=2.
  (H.table ! A.class_ "specs") . (emptyRow<>) . mconcat <$>
    sequence (mapMaybe (synopsisDec visible fm) decs)

synopsisDec :: S.Set VName -> FileModule -> Dec -> Maybe (DocM Html)
synopsisDec visible fm dec = case dec of
  SigDec s -> synopsisModType mempty s
  ModDec m -> synopsisMod fm m
  ValDec v -> synopsisValBind v
  TypeDec t -> synopsisType t
  OpenDec x _
    | Just opened <- synopsisOpened x -> Just $ do
        opened' <- opened
        return $ fullRow $ keyword "open " <> opened'
    | otherwise ->
        Just $ return $ fullRow $
        keyword "open" <> fromString (" <" <> pretty x <> ">")
  LocalDec (SigDec s) _
    | sigName s `S.member` visible ->
        synopsisModType (keyword "local" <> " ") s
  LocalDec{} -> Nothing
  ImportDec{} -> Nothing

synopsisOpened :: ModExp -> Maybe (DocM Html)
synopsisOpened (ModVar qn _) = Just $ qualNameHtml qn
synopsisOpened (ModParens me _) = do me' <- synopsisOpened me
                                     Just $ parens <$> me'
synopsisOpened (ModImport _ (Info file) _) = Just $ do
  current <- asks ctxCurrent
  let dest = fromString $ relativise file current <> ".html"
  return $ keyword "import " <> (H.a ! A.href dest) (fromString $ show file)
synopsisOpened (ModAscript _ se _ _) = Just $ do
  se' <- synopsisSigExp se
  return $ "... : " <> se'
synopsisOpened _ = Nothing

synopsisValBind :: ValBind -> Maybe (DocM Html)
synopsisValBind vb = Just $ do
  let name' = vnameSynopsisDef $ valBindName vb
  (lhs, mhs, rhs) <- valBindHtml name' vb
  return $ specRow lhs (mhs <> " : ") rhs

valBindHtml :: Html -> ValBind -> DocM (Html, Html, Html)
valBindHtml name (ValBind _ _ retdecl (Info rettype) tparams params _ _ _) = do
  let tparams' = mconcat $ map ((" "<>) . typeParamHtml) tparams
      noLink' = noLink $ map typeParamName tparams ++
                map identName (S.toList $ mconcat $ map patIdentSet params)
  rettype' <- noLink' $ maybe (typeHtml rettype) typeExpHtml retdecl
  params' <- noLink' $ mapM patternHtml params
  return (keyword "val " <> (H.span ! A.class_ "decl_name") name,
          tparams',
          mconcat (intersperse " -> " $ params' ++ [rettype']))

synopsisModType :: Html -> SigBind -> Maybe (DocM Html)
synopsisModType prefix sb = Just $ do
  let name' = vnameSynopsisDef $ sigName sb
  fullRow <$> do
    se' <- synopsisSigExp $ sigExp sb
    return $ prefix <> keyword "module type " <> name' <> " = " <> se'

synopsisMod :: FileModule -> ModBind -> Maybe (DocM Html)
synopsisMod fm (ModBind name ps sig _ _ _) =
  case sig of Nothing    -> (proceed <=< envSig) <$> M.lookup name modtable
              Just (s,_) -> Just $ proceed =<< synopsisSigExp s
  where proceed sig' = do
          let name' = vnameSynopsisDef name
          ps' <- modParamHtml ps
          return $ specRow (keyword "module " <> name') ": " (ps' <> sig')

        FileModule _abs Env { envModTable = modtable} _ = fm
        envSig (ModEnv e) = renderEnv e
        envSig (ModFun (FunSig _ _ (MTy _ m))) = envSig m

synopsisType :: TypeBind -> Maybe (DocM Html)
synopsisType tb = Just $ do
  let name' = vnameSynopsisDef $ typeAlias tb
  fullRow <$> typeBindHtml name' tb

typeBindHtml :: Html -> TypeBind -> DocM Html
typeBindHtml name' (TypeBind _ tparams t _ _) = do
  t' <- noLink (map typeParamName tparams) $ typeDeclHtml t
  return $ typeAbbrevHtml Unlifted name' tparams <> " = " <> t'

renderEnv :: Env -> DocM Html
renderEnv (Env vtable ttable sigtable modtable _) = do
  typeBinds <- mapM renderTypeBind (M.toList ttable)
  valBinds <- mapM renderValBind (M.toList vtable)
  sigBinds <- mapM renderModType (M.toList sigtable)
  modBinds <- mapM renderMod (M.toList modtable)
  return $ braces $ mconcat $ typeBinds ++ valBinds ++ sigBinds ++ modBinds

renderModType :: (VName, MTy) -> DocM Html
renderModType (name, _sig) =
  (keyword "module type " <>) <$> qualNameHtml (qualName name)

renderMod :: (VName, Mod) -> DocM Html
renderMod (name, _mod) =
  (keyword "module " <>) <$> qualNameHtml (qualName name)

renderValBind :: (VName, BoundV) -> DocM Html
renderValBind = fmap H.div . synopsisValBindBind

renderTypeBind :: (VName, TypeBinding) -> DocM Html
renderTypeBind (name, TypeAbbr l tps tp) = do
  tp' <- typeHtml tp
  return $ H.div $ typeAbbrevHtml l (vnameHtml name) tps <> " = " <> tp'

synopsisValBindBind :: (VName, BoundV) -> DocM Html
synopsisValBindBind (name, BoundV tps t) = do
  let tps' = map typeParamHtml tps
  t' <- typeHtml t
  return $
    keyword "val " <> vnameHtml name <>
    mconcat (map (" "<>) tps') <> ": " <> t'

prettyEnum :: [Name] -> Html
prettyEnum cs = pipes $ map (("#"<>) . renderName) cs

typeHtml :: StructType -> DocM Html
typeHtml t = case t of
  Prim et -> return $ primTypeHtml et
  Record fs
    | Just ts <- areTupleFields fs ->
        parens . commas <$> mapM typeHtml ts
    | otherwise ->
        braces . commas <$> mapM ppField (M.toList fs)
    where ppField (name, tp) = do
            tp' <- typeHtml tp
            return $ toHtml (nameToString name) <> ": " <> tp'
  TypeVar _ u et targs -> do
    targs' <- mapM typeArgHtml targs
    et' <- typeNameHtml et
    return $ prettyU u <> et' <> joinBy " " targs'
  Array _ u et shape -> do
    shape' <- prettyShapeDecl shape
    et' <- prettyElem et
    return $ prettyU u <> shape' <> et'
  Arrow _ pname t1 t2 -> do
    t1' <- typeHtml t1
    t2' <- typeHtml t2
    return $ case pname of
      Just v ->
        parens (vnameHtml v <> ": " <> t1') <> " -> " <> t2'
      Nothing ->
        t1' <> " -> " <> t2'
  Enum cs -> return $ prettyEnum cs

prettyElem :: ArrayElemTypeBase (DimDecl VName) -> DocM Html
prettyElem (ArrayPrimElem et) = return $ primTypeHtml et
prettyElem (ArrayPolyElem et targs) = do
  targs' <- mapM typeArgHtml targs
  return $ prettyTypeName et <> joinBy " " targs'
prettyElem (ArrayRecordElem fs)
  | Just ts <- areTupleFields fs =
      parens . commas <$> mapM prettyRecordElem ts
  | otherwise =
      braces . commas <$> mapM ppField (M.toList fs)
  where ppField (name, tp) = do
          tp' <- prettyRecordElem tp
          return $ toHtml (nameToString name) <> ": " <> tp'
prettyElem (ArrayEnumElem cs) = return $ braces $ prettyEnum cs

prettyRecordElem :: RecordArrayElemTypeBase (DimDecl VName) -> DocM Html
prettyRecordElem (RecordArrayElem et) = prettyElem et
prettyRecordElem (RecordArrayArrayElem et shape) =
  typeHtml $ Array () Nonunique et shape

prettyShapeDecl :: ShapeDecl (DimDecl VName) -> DocM Html
prettyShapeDecl (ShapeDecl ds) =
  mconcat <$> mapM (fmap brackets . dimDeclHtml) ds

typeArgHtml :: TypeArg (DimDecl VName) -> DocM Html
typeArgHtml (TypeArgDim d _) = brackets <$> dimDeclHtml d
typeArgHtml (TypeArgType t _) = typeHtml t

modParamHtml :: [ModParamBase Info VName] -> DocM Html
modParamHtml [] = return mempty
modParamHtml (ModParam pname psig _ _ : mps) =
  liftM2 f (synopsisSigExp psig) (modParamHtml mps)
  where f se params = "(" <> vnameHtml pname <>
                      ": " <> se <> ") -> " <> params

synopsisSigExp :: SigExpBase Info VName -> DocM Html
synopsisSigExp e = case e of
  SigVar v _ -> qualNameHtml v
  SigParens e' _ -> parens <$> synopsisSigExp e'
  SigSpecs ss _ -> braces . (H.table ! A.class_ "specs") . mconcat <$> mapM synopsisSpec ss
  SigWith s (TypeRef v ps t _) _ -> do
    s' <- synopsisSigExp s
    t' <- typeDeclHtml t
    v' <- qualNameHtml v
    let ps' = mconcat $ map ((" "<>) . typeParamHtml) ps
    return $ s' <> keyword " with " <> v' <> ps' <> " = " <> t'
  SigArrow Nothing e1 e2 _ ->
    liftM2 f (synopsisSigExp e1) (synopsisSigExp e2)
    where f e1' e2' = e1' <> " -> " <> e2'
  SigArrow (Just v) e1 e2 _ ->
    do let name = vnameHtml v
       e1' <- synopsisSigExp e1
       e2' <- noLink [v] $ synopsisSigExp e2
       return $ "(" <> name <> ": " <> e1' <> ") -> " <> e2'

keyword :: String -> Html
keyword = (H.span ! A.class_ "keyword") . fromString

vnameHtml :: VName -> Html
vnameHtml (VName name tag) =
  H.span ! A.id (fromString (show tag)) $ renderName name

vnameDescDef :: VName -> IndexWhat -> DocM Html
vnameDescDef v what = do
  document v what
  return $ H.a ! A.id (fromString (show (baseTag v))) $ renderName (baseName v)

vnameSynopsisDef :: VName -> Html
vnameSynopsisDef (VName name tag) =
  H.span ! A.id (fromString (show tag ++ "s")) $
  H.a ! A.href (fromString ("#" ++ show tag)) $ renderName name

vnameSynopsisRef :: VName -> Html
vnameSynopsisRef v = H.a ! A.class_ "synopsis_link"
                         ! A.href (fromString ("#" ++ show (baseTag v) ++ "s")) $
                     "↑"

synopsisSpec :: SpecBase Info VName -> DocM Html
synopsisSpec spec = case spec of
  TypeAbbrSpec tpsig ->
    fullRow <$> typeBindHtml (vnameSynopsisDef $ typeAlias tpsig) tpsig
  TypeSpec l name ps _ _ ->
    return $ fullRow $ keyword l' <> vnameSynopsisDef name <> mconcat (map ((" "<>) . typeParamHtml) ps)
    where l' = case l of Unlifted -> "type "
                         Lifted   -> "type^ "
  ValSpec name tparams rettype _ _ -> do
    let tparams' = map typeParamHtml tparams
    rettype' <- noLink (map typeParamName tparams) $
                typeDeclHtml rettype
    return $ specRow
      (keyword "val " <> vnameSynopsisDef name)
      (mconcat (map (" "<>) tparams') <> ": ") rettype'
  ModSpec name sig _ _ ->
    specRow (keyword "module " <> vnameSynopsisDef name) ": " <$> synopsisSigExp sig
  IncludeSpec e _ -> fullRow . (keyword "include " <>) <$> synopsisSigExp e

typeDeclHtml :: TypeDeclBase f VName -> DocM Html
typeDeclHtml = typeExpHtml . declaredType

typeExpHtml :: TypeExp VName -> DocM Html
typeExpHtml e = case e of
  TEUnique t _  -> ("*"<>) <$> typeExpHtml t
  TEArray at d _ -> do
    at' <- typeExpHtml at
    d' <- dimDeclHtml d
    return $ brackets d' <> at'
  TETuple ts _ -> parens . commas <$> mapM typeExpHtml ts
  TERecord fs _ -> braces . commas <$> mapM ppField fs
    where ppField (name, t) = do
            t' <- typeExpHtml t
            return $ toHtml (nameToString name) <> ": " <> t'
  TEVar name  _ -> qualNameHtml name
  TEApply t arg _ -> do
    t' <- typeExpHtml t
    arg' <- typeArgExpHtml arg
    return $ t' <> " " <> arg'
  TEArrow pname t1 t2 _ -> do
    t1' <- case t1 of TEArrow{} -> parens <$> typeExpHtml t1
                      _         -> typeExpHtml t1
    t2' <- typeExpHtml t2
    return $ case pname of
      Just v ->
        parens (vnameHtml v <> ": " <> t1') <> " -> " <> t2'
      Nothing ->
        t1' <> " -> " <> t2'
  TEEnum cs _ -> return $ prettyEnum cs

qualNameHtml :: QualName VName -> DocM Html
qualNameHtml (QualName names vname@(VName name tag)) =
  if tag <= maxIntrinsicTag
      then return $ renderName name
      else f <$> ref
  where prefix :: Html
        prefix = mapM_ ((<> ".") . renderName . baseName) names
        f (Just s) = H.a ! A.href (fromString s) $ prefix <> renderName name
        f Nothing = prefix <> renderName name

        ref = do boring <- asks $ S.member vname . ctxNoLink
                 if boring
                   then return Nothing
                   else Just <$> vnameLink vname

vnameLink' :: VName -> String -> String -> String
vnameLink :: VName -> DocM String
vnameLink vname = do
  current <- asks ctxCurrent
  file <- maybe current fst <$> asks (M.lookup vname . ctxFileMap)
  return $ vnameLink' vname current file

vnameLink' (VName _ tag) current file =
  if file == current
    then "#" ++ show tag
    else relativise file current ++ ".html#" ++ show tag

typeNameHtml :: TypeName -> DocM Html
typeNameHtml = qualNameHtml . qualNameFromTypeName

patternHtml :: Pattern -> DocM Html
patternHtml pat = do
  let (pat_param, t) = patternParam pat
  t' <- typeHtml t
  return $ case pat_param of
             Just v  -> parens (vnameHtml v <> ": " <> t')
             Nothing -> t'

relativise :: FilePath -> FilePath -> FilePath
relativise dest src =
  concat (replicate (length (splitPath src) - 1) "../") ++ dest

dimDeclHtml :: DimDecl VName -> DocM Html
dimDeclHtml AnyDim = return mempty
dimDeclHtml (NamedDim v) = qualNameHtml v
dimDeclHtml (ConstDim n) = return $ toHtml (show n)

typeArgExpHtml :: TypeArgExp VName -> DocM Html
typeArgExpHtml (TypeArgExpDim d _) = dimDeclHtml d
typeArgExpHtml (TypeArgExpType d) = typeExpHtml d

typeParamHtml :: TypeParam -> Html
typeParamHtml (TypeParamDim name _) = brackets $ vnameHtml name
typeParamHtml (TypeParamType Unlifted name _) = "'" <> vnameHtml name
typeParamHtml (TypeParamType Lifted name _) = "'^" <> vnameHtml name

typeAbbrevHtml :: Liftedness -> Html -> [TypeParam] -> Html
typeAbbrevHtml l name params =
  what <> name <> mconcat (map ((" "<>) . typeParamHtml) params)
  where what = case l of Lifted -> keyword "type " <> "^"
                         Unlifted -> keyword "type "

docHtml :: Maybe DocComment -> DocM Html
docHtml (Just (DocComment doc loc)) =
  markdown def { msAddHeadingId = True } . LT.pack <$> identifierLinks loc doc
docHtml Nothing = return mempty

identifierLinks :: SrcLoc -> String -> DocM String
identifierLinks _ [] = return []
identifierLinks loc s
  | Just ((name, namespace, file), s') <- identifierReference s = do
      let proceed x = (x<>) <$> identifierLinks loc s'
          unknown = proceed $ "`" <> name <> "`"
      case knownNamespace namespace of
        Just namespace' -> do
          maybe_v <- lookupName (namespace', name, file)
          case maybe_v of
            Nothing -> do
              warn loc $
                "Identifier '" <> name <> "' not found in namespace '" <>
                namespace <> "'" <> maybe "" (" in file "<>) file <> "."
              unknown
            Just v' -> do
              link <- vnameLink v'
              proceed $ "[`" <> name <> "`](" <> link <> ")"
        _ -> do
          warn loc $ "Unknown namespace '" <> namespace <> "'."
          unknown
  where knownNamespace "term" = Just Term
        knownNamespace "mtype" = Just Signature
        knownNamespace "type" = Just Type
        knownNamespace _ = Nothing
identifierLinks loc (c:s') = (c:) <$> identifierLinks loc s'

lookupName :: (Namespace, String, Maybe FilePath) -> DocM (Maybe VName)
lookupName (namespace, name, file) = do
  current <- asks ctxCurrent
  let file' = includeToString . flip (mkImportFrom (mkInitialImport current)) noLoc <$> file
  env <- lookupEnvForFile file'
  case M.lookup (namespace, nameFromString name) . envNameMap =<< env of
    Nothing -> return Nothing
    Just qn -> return $ Just $ qualLeaf qn

lookupEnvForFile :: Maybe FilePath -> DocM (Maybe Env)
lookupEnvForFile Nothing     = asks $ Just . fileEnv . ctxFileMod
lookupEnvForFile (Just file) = asks $ fmap fileEnv . lookup file . ctxImports

describeGeneric :: VName
                -> IndexWhat
                -> Maybe DocComment
                -> (Html -> DocM Html)
                -> DocM Html
describeGeneric name what doc f = do
  name' <- H.span ! A.class_ "decl_name" <$> vnameDescDef name what
  decl_type <- f name'
  doc' <- docHtml doc
  let decl_doc = H.dd ! A.class_ "desc_doc" $ doc'
      decl_header = (H.dt ! A.class_ "desc_header") $
                    vnameSynopsisRef name <> decl_type
  return $ decl_header <> decl_doc

describeGenericMod :: VName
                   -> IndexWhat
                   -> SigExp
                   -> Maybe DocComment
                   -> (Html -> DocM Html)
                   -> DocM Html
describeGenericMod name what se doc f = do
  name' <- H.span ! A.class_ "decl_name" <$> vnameDescDef name what

  decl_type <- f name'

  doc' <- case se of
            SigSpecs specs _ -> (<>) <$> docHtml doc <*> describeSpecs specs
            _                -> docHtml doc

  let decl_doc = H.dd ! A.class_ "desc_doc" $ doc'
      decl_header = (H.dt ! A.class_ "desc_header") $
                    vnameSynopsisRef name <> decl_type
  return $ decl_header <> decl_doc

describeDecs :: [Dec] -> DocM Html
describeDecs decs = do
  visible <- asks ctxVisibleMTys
  H.dl . mconcat <$>
    mapM (fmap $ H.div ! A.class_ "decl_description")
    (mapMaybe (describeDec visible) decs)

describeDec :: S.Set VName -> Dec -> Maybe (DocM Html)
describeDec _ (ValDec vb) = Just $
  describeGeneric (valBindName vb) (valBindWhat vb) (valBindDoc vb) $ \name -> do
  (lhs, mhs, rhs) <- valBindHtml name vb
  return $ lhs <> mhs <> ": " <> rhs

describeDec _ (TypeDec vb) = Just $
  describeGeneric (typeAlias vb) IndexType (typeDoc vb) (`typeBindHtml` vb)

describeDec _ (SigDec (SigBind name se doc _)) = Just $
  describeGenericMod name IndexModuleType se doc $ \name' ->
  return $ keyword "module type " <> name'

describeDec _ (ModDec mb) = Just $
  describeGeneric (modName mb) IndexModule (modDoc mb) $ \name' ->
  return $ keyword "module " <> name'

describeDec _ OpenDec{} = Nothing

describeDec visible (LocalDec (SigDec (SigBind name se doc _)) _)
  | name `S.member` visible = Just $
  describeGenericMod name IndexModuleType se doc $ \name' ->
  return $ keyword "local module type " <> name'

describeDec _ LocalDec{} = Nothing
describeDec _ ImportDec{} = Nothing

valBindWhat :: ValBind -> IndexWhat
valBindWhat vb =
  if null (valBindParams vb) && orderZero (unInfo (valBindRetType vb))
  then IndexValue
  else IndexFunction

describeSpecs :: [Spec] -> DocM Html
describeSpecs specs =
  H.dl . mconcat <$> mapM describeSpec specs

describeSpec :: Spec -> DocM Html
describeSpec (ValSpec name tparams t doc _) =
  describeGeneric name what doc $ \name' -> do
    let tparams' = mconcat $ map ((" "<>) . typeParamHtml) tparams
    t' <- noLink (map typeParamName tparams) $
          typeExpHtml $ declaredType t
    return $ keyword "val " <>  name' <> tparams' <> ": " <> t'
  where what = if orderZero (unInfo $ expandedType t)
               then IndexValue else IndexFunction
describeSpec (TypeAbbrSpec vb) =
  describeGeneric (typeAlias vb) IndexType (typeDoc vb) (`typeBindHtml` vb)
describeSpec (TypeSpec l name tparams doc _) =
  describeGeneric name IndexType doc $
  return . (\name' -> typeAbbrevHtml l name' tparams)
describeSpec (ModSpec name se doc _) =
  describeGenericMod name IndexModule se doc $ \name' ->
  case se of
    SigSpecs{} -> return $ keyword "module " <> name'
    _ -> do se' <- synopsisSigExp se
            return $ keyword "module " <> name' <> ": " <> se'
describeSpec (IncludeSpec sig _) = do
  sig' <- synopsisSigExp sig
  doc' <- docHtml Nothing
  let decl_header = (H.dt ! A.class_ "desc_header") $
                    (H.span ! A.class_ "synopsis_link") mempty <>
                    keyword "include " <>
                    sig'
      decl_doc = H.dd ! A.class_ "desc_doc" $ doc'
  return $ decl_header <> decl_doc