{-# OPTIONS_GHC -fno-warn-orphans -fno-warn-dodgy-imports #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE FlexibleContexts #-}

module Render
    ( blockBodyToDoc
    ) where

import Data.List.Compat hiding (group)
import Data.Maybe
import Data.Monoid.Compat
import Data.Ord
import Distribution.Pretty
import Distribution.Types.Dependency
import Distribution.Types.IncludeRenaming
import Distribution.Types.LegacyExeDependency
import Distribution.Types.Mixin
import Distribution.Types.ModuleReexport
import Distribution.Types.ModuleRenaming
import Distribution.Types.PackageName
import Distribution.Types.PkgconfigDependency
import Distribution.Types.PkgconfigName
import Distribution.Version
import Documentation.Haddock.Types
import Prelude.Compat hiding ((<$>))
import qualified Prelude.Compat as P
import Text.PrettyPrint.ANSI.Leijen hiding ((<>))

import Render.Lib
import Render.Options
import Types.Block
import Types.Field

deriving instance Ord ModuleReexport

fieldValueToDoc k (Field _ f) =
    case f of
        Dependencies ds ->
            buildDepsToDoc k $ map (\(Dependency pn v) -> (P $ unPackageName pn, v)) ds
        ToolDepends ts -> buildDepsToDoc k $ map exeDependencyAsDependency ts
        OldToolDepends ds ->
            buildDepsToDoc k $ map (\(LegacyExeDependency pn v) -> (P pn, v)) ds
        PcDepends ds ->
            buildDepsToDoc k $
            map (\(PkgconfigDependency pn v) -> (P $ unPkgconfigName pn, v)) ds
        Mixins ms -> mixinsToDoc k $ map (\(Mixin pn r) -> (P $ unPackageName pn, r)) ms
        RexpModules rms ->
            buildDepsToDoc k $
            map (\rexp -> (P $ show $ rexpModuleDoc rexp, anyVersion)) rms
        n -> val' n <&> \v -> colon <> indent (k + 1) (align v)
  where
    val' (Str x) = pure $ string x
    val' (File x) = pure $ filepath x
    val' (Version v) = pure $ string $ prettyShow v
    val' (CabalVersion (Left v)) = pure $ string $ prettyShow v
    val' (CabalVersion (Right vr))
        | vr == anyVersion = showVersionRange (orLaterVersion (mkVersion [1, 10]))
        | otherwise = showVersionRange vr
    val' (License l) = pure $ string $ prettyShow l
    val' (SPDXLicense l) = pure $ string $ prettyShow l
    val' (TestedWith ts) = renderTestedWith ts
    val' (LongList fs) = pure $ vcat $ map filepath fs
    val' (Commas fs) = pure $ fillSep $ punctuate comma $ map filepath fs
    val' (Spaces ls) = pure $ fillSep $ map filepath ls
    val' (Modules ms) = pure $ vcat $ map moduleDoc $ sort ms
    val' (Module m) = pure $ moduleDoc m
    val' (Extensions es) = val' (LongList $ map prettyShow $ sort es)
    val' (FlibType ty) = pure $ string $ prettyShow ty
    val' (FlibOptions fs) = val' $ Spaces $ map prettyShow fs
    val' x = error $ show x
fieldValueToDoc k (Description s) = descriptionToDoc k s

descriptionToDoc k paras = do
    n <- asks indentSize
    return $
        (<>) colon $
        nest n $
        case paras of
            DocParagraph {} -> indent (k + 1) ds
            _ -> linebreak <> ds
  where
    ds = renderDescription paras

mixinsToDoc k bs
    | k == 0 = pure $ deps ": "
    | otherwise = pure $ colon <> indent (k - 1) (deps "  ")
  where
    deps lsep =
        enclose (string lsep) empty $
        hcat $
        intersperse (hardline <> string ", ") $ map showField $ sortOn fst bs
    longest = maximum $ map (length . unP . fst) bs
    hasRequires = any (\(_, c) -> not (isDefaultRenaming $ includeRequiresRn c)) bs
    showField (P fName, i@IncludeRenaming {..})
        | isDefaultIncludeRenaming i = string fName
        | otherwise =
            width (string fName) $ \fn ->
                let delt n =
                        indent
                            (n + 1)
                            (if isDefaultRenaming includeRequiresRn
                                 then providesDoc includeProvidesRn
                                 else group $
                                      align'
                                          9
                                          (providesDoc includeProvidesRn <$>
                                           string "requires" <+>
                                           providesDoc includeRequiresRn))
                    pad doc =
                        if hasRequires
                            then string (replicate (8 - longest) ' ') <> doc
                            else doc
                 in flatAlt (pad $ delt (longest - fn)) (delt 0)
    parenthesize =
        group .
        encloseSep
            (flatAlt (string " (") lparen) -- `mixin-name ( Module` doesn't parse
            (flatAlt (line <> rparen) rparen)
            (string ", ")
    providesDoc (ModuleRenaming ms) = parenthesize $ map renaming ms
    providesDoc (HidingRenaming hs) = string "hiding" <+> parenthesize (map moduleDoc hs)
    providesDoc DefaultRenaming = empty
    renaming (m1, m2) = moduleDoc m1 <+> string "as" <+> moduleDoc m2
    align' n doc = column (\ko -> nesting (\i -> nest (ko - i - n) doc))

buildDepsToDoc :: Int -> [(P, VersionRange)] -> Render Doc
buildDepsToDoc k bs
    | k == 0 = deps ": "
    | otherwise = fmap (\r -> colon <> indent (k - 1) r) (deps "  ")
  where
    deps lsep = do
        fs <- mapM showField $ sortOn fst bs
        return $ encloseSep (string lsep) empty (string ", ") fs
    longest = maximum $ map (length . unP . fst) bs
    showField (P fName, fieldVal)
        | fieldVal == anyVersion = pure $ string fName
        | otherwise =
            widthR (string fName) $ \fn -> do
                shown <- showVersionRange fieldVal
                let delt n = indent (n + 1) shown
                 in pure $ flatAlt (delt (longest - fn)) (delt 0)

fieldsToDoc :: [Field] -> Render Doc
fieldsToDoc fs =
    vcat P.<$>
    mapM
        (\field ->
             widthR (dullblue $ string (fieldName field)) $ \fn ->
                 fieldValueToDoc (longestField - fn) field)
        fs
  where
    longestField = maximum $ map (length . fieldName) fs

renderBlock :: Block -> Render Doc
renderBlock (Block t fs blocks) = do
    blkhead <- renderBlockHead t
    body <- indentM . align =<< blockBodyToDoc fs blocks
    return $
        (if isElse t
             then id
             else (<>) line)
            blkhead <$$>
        body

blockBodyToDoc :: [Maybe Field] -> [Block] -> Render Doc
blockBodyToDoc fs blocks = do
    fields <-
        fieldsToDoc
            (if null fs'
                 then buildable'
                 else fs')
    subblocks <- mapM renderBlock blocks
    return $ fields <> vcat (empty : subblocks)
  where
    fs' = catMaybes fs
    buildable' = [fromJust $ stringField "buildable" "True"]