{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Language.Haskell.Brittany.Internal.Layouters.Expr where

import qualified Data.Data
import qualified Data.Semigroup as Semigroup
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import GHC (AnnKeywordId(..), GenLocated(L), RdrName(..), SrcSpan)
import qualified GHC.Data.FastString as FastString
import GHC.Hs
import qualified GHC.OldList as List
import GHC.Types.Basic
import GHC.Types.Name
import Language.Haskell.Brittany.Internal.Config.Types
import Language.Haskell.Brittany.Internal.LayouterBasics
import Language.Haskell.Brittany.Internal.Layouters.Decl
import Language.Haskell.Brittany.Internal.Layouters.Pattern
import Language.Haskell.Brittany.Internal.Layouters.Stmt
import Language.Haskell.Brittany.Internal.Layouters.Type
import Language.Haskell.Brittany.Internal.Prelude
import Language.Haskell.Brittany.Internal.PreludeUtils
import Language.Haskell.Brittany.Internal.Types
import Language.Haskell.Brittany.Internal.Utils



layoutExpr :: ToBriDoc HsExpr
layoutExpr lexpr@(L _ expr) = do
  indentPolicy <- mAsk <&> _conf_layout .> _lconfig_indentPolicy .> confUnpack
  let allowFreeIndent = indentPolicy == IndentPolicyFree
  docWrapNode lexpr $ case expr of
    HsVar _ vname -> do
      docLit =<< lrdrNameToTextAnn vname
    HsUnboundVar _ oname -> docLit $ Text.pack $ occNameString oname
    HsRecFld{} -> do
      -- TODO
      briDocByExactInlineOnly "HsRecFld" lexpr
    HsOverLabel _ext _reboundFromLabel name ->
      let label = FastString.unpackFS name in docLit . Text.pack $ '#' : label
    HsIPVar _ext (HsIPName name) ->
      let label = FastString.unpackFS name in docLit . Text.pack $ '?' : label
    HsOverLit _ olit -> do
      allocateNode $ overLitValBriDoc $ ol_val olit
    HsLit _ lit -> do
      allocateNode $ litBriDoc lit
    HsLam _ (MG _ (L _ [lmatch@(L _ match)]) _)
      | pats <- m_pats match
      , GRHSs _ [lgrhs] llocals <- m_grhss match
      , L _ EmptyLocalBinds{} <- llocals
      , L _ (GRHS _ [] body) <- lgrhs
      -> do
        patDocs <- zip (True : repeat False) pats `forM` \(isFirst, p) ->
          fmap return $ do
            -- this code could be as simple as `colsWrapPat =<< layoutPat p`
            -- if it was not for the following two cases:
            -- \ !x -> x
            -- \ ~x -> x
            -- These make it necessary to special-case an additional separator.
            -- (TODO: we create a BDCols here, but then make it ineffective
            -- by wrapping it in docSeq below. We _could_ add alignments for
            -- stuff like lists-of-lambdas. Nothing terribly important..)
            let
              shouldPrefixSeparator = case p of
                L _ LazyPat{} -> isFirst
                L _ BangPat{} -> isFirst
                _ -> False
            patDocSeq <- layoutPat p
            fixed <- case Seq.viewl patDocSeq of
              p1 Seq.:< pr | shouldPrefixSeparator -> do
                p1' <- docSeq [docSeparator, pure p1]
                pure (p1' Seq.<| pr)
              _ -> pure patDocSeq
            colsWrapPat fixed
        bodyDoc <-
          docAddBaseY BrIndentRegular <$> docSharedWrapper layoutExpr body
        let
          funcPatternPartLine = docCols
            ColCasePattern
            (patDocs <&> (\p -> docSeq [docForceSingleline p, docSeparator]))
        docAlt
          [ -- single line
            docSeq
            [ docLit $ Text.pack "\\"
            , docWrapNode lmatch $ docForceSingleline funcPatternPartLine
            , appSep $ docLit $ Text.pack "->"
            , docWrapNode lgrhs $ docForceSingleline bodyDoc
            ]
            -- double line
          , docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
            (docSeq
              [ docLit $ Text.pack "\\"
              , docWrapNode lmatch $ appSep $ docForceSingleline
                funcPatternPartLine
              , docLit $ Text.pack "->"
              ]
            )
            (docWrapNode lgrhs $ docForceSingleline bodyDoc)
            -- wrapped par spacing
          , docSetParSpacing $ docSeq
            [ docLit $ Text.pack "\\"
            , docWrapNode lmatch $ docForceSingleline funcPatternPartLine
            , appSep $ docLit $ Text.pack "->"
            , docWrapNode lgrhs $ docForceParSpacing bodyDoc
            ]
            -- conservative
          , docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
            (docSeq
              [ docLit $ Text.pack "\\"
              , docWrapNode lmatch $ appSep $ docForceSingleline
                funcPatternPartLine
              , docLit $ Text.pack "->"
              ]
            )
            (docWrapNode lgrhs $ docNonBottomSpacing bodyDoc)
          ]
    HsLam{} -> unknownNodeError "HsLam too complex" lexpr
    HsLamCase _ (MG _ (L _ []) _) -> do
      docSetParSpacing
        $ docAddBaseY BrIndentRegular
        $ (docLit $ Text.pack "\\case {}")
    HsLamCase _ (MG _ lmatches@(L _ matches) _) -> do
      binderDoc <- docLit $ Text.pack "->"
      funcPatDocs <-
        docWrapNode lmatches
        $ layoutPatternBind Nothing binderDoc
        `mapM` matches
      docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
        (docLit $ Text.pack "\\case")
        (docSetBaseAndIndent
        $ docNonBottomSpacing
        $ docLines
        $ return
        <$> funcPatDocs
        )
    HsApp _ exp1@(L _ HsApp{}) exp2 -> do
      let
        gather
          :: [LHsExpr GhcPs]
          -> LHsExpr GhcPs
          -> (LHsExpr GhcPs, [LHsExpr GhcPs])
        gather list = \case
          L _ (HsApp _ l r) -> gather (r : list) l
          x -> (x, list)
      let (headE, paramEs) = gather [exp2] exp1
      let
        colsOrSequence = case headE of
          L _ (HsVar _ (L _ (Unqual occname))) ->
            docCols (ColApp $ Text.pack $ occNameString occname)
          _ -> docSeq
      headDoc <- docSharedWrapper layoutExpr headE
      paramDocs <- docSharedWrapper layoutExpr `mapM` paramEs
      hasComments <- hasAnyCommentsConnected exp2
      runFilteredAlternative $ do
        -- foo x y
        addAlternativeCond (not hasComments)
          $ colsOrSequence
          $ appSep (docForceSingleline headDoc)
          : spacifyDocs (docForceSingleline <$> paramDocs)
        -- foo x
        --     y
        addAlternativeCond allowFreeIndent $ docSeq
          [ appSep (docForceSingleline headDoc)
          , docSetBaseY
          $ docAddBaseY BrIndentRegular
          $ docLines
          $ docForceSingleline
          <$> paramDocs
          ]
        -- foo
        --   x
        --   y
        addAlternative $ docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docForceSingleline headDoc)
          (docNonBottomSpacing $ docLines paramDocs)
        -- ( multi
        --   line
        --   function
        -- )
        --   x
        --   y
        addAlternative $ docAddBaseY BrIndentRegular $ docPar
          headDoc
          (docNonBottomSpacing $ docLines paramDocs)
    HsApp _ exp1 exp2 -> do
      -- TODO: if expDoc1 is some literal, we may want to create a docCols here.
      expDoc1 <- docSharedWrapper layoutExpr exp1
      expDoc2 <- docSharedWrapper layoutExpr exp2
      docAlt
        [ -- func arg
          docSeq
          [appSep $ docForceSingleline expDoc1, docForceSingleline expDoc2]
        , -- func argline1
          --   arglines
          -- e.g.
          -- func Abc
          --   { member1 = True
          --   , member2 = 13
          --   }
          docSetParSpacing -- this is most likely superfluous because
                           -- this is a sequence of a one-line and a par-space
                           -- anyways, so it is _always_ par-spaced.
        $ docAddBaseY BrIndentRegular
        $ docSeq
            [appSep $ docForceSingleline expDoc1, docForceParSpacing expDoc2]
        , -- func
          --   arg
          docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docForceSingleline expDoc1)
          (docNonBottomSpacing expDoc2)
        , -- fu
          --   nc
          --   ar
          --     gument
          docAddBaseY BrIndentRegular $ docPar expDoc1 expDoc2
        ]
    HsAppType _ exp1 (HsWC _ ty1) -> do
      t <- docSharedWrapper layoutType ty1
      e <- docSharedWrapper layoutExpr exp1
      docAlt
        [ docSeq
          [ docForceSingleline e
          , docSeparator
          , docLit $ Text.pack "@"
          , docForceSingleline t
          ]
        , docPar e (docSeq [docLit $ Text.pack "@", t])
        ]
    OpApp _ expLeft@(L _ OpApp{}) expOp expRight -> do
      let
        gather
          :: [(LHsExpr GhcPs, LHsExpr GhcPs)]
          -> LHsExpr GhcPs
          -> (LHsExpr GhcPs, [(LHsExpr GhcPs, LHsExpr GhcPs)])
        gather opExprList = \case
          (L _ (OpApp _ l1 op1 r1)) -> gather ((op1, r1) : opExprList) l1
          final -> (final, opExprList)
        (leftOperand, appList) = gather [] expLeft
      leftOperandDoc <- docSharedWrapper layoutExpr leftOperand
      appListDocs <- appList `forM` \(x, y) ->
        [ (xD, yD)
        | xD <- docSharedWrapper layoutExpr x
        , yD <- docSharedWrapper layoutExpr y
        ]
      opLastDoc <- docSharedWrapper layoutExpr expOp
      expLastDoc <- docSharedWrapper layoutExpr expRight
      allowSinglelinePar <- do
        hasComLeft <- hasAnyCommentsConnected expLeft
        hasComOp <- hasAnyCommentsConnected expOp
        pure $ not hasComLeft && not hasComOp
      let
        allowPar = case (expOp, expRight) of
          (L _ (HsVar _ (L _ (Unqual occname))), _)
            | occNameString occname == "$" -> True
          (_, L _ (HsApp _ _ (L _ HsVar{}))) -> False
          _ -> True
      runFilteredAlternative $ do
        -- > one + two + three
        -- or
        -- > one + two + case x of
        -- >   _ -> three
        addAlternativeCond allowSinglelinePar $ docSeq
          [ appSep $ docForceSingleline leftOperandDoc
          , docSeq $ appListDocs <&> \(od, ed) -> docSeq
            [appSep $ docForceSingleline od, appSep $ docForceSingleline ed]
          , appSep $ docForceSingleline opLastDoc
          , (if allowPar then docForceParSpacing else docForceSingleline)
            expLastDoc
          ]
        -- this case rather leads to some unfortunate layouting than to anything
        -- useful; disabling for now. (it interfers with cols stuff.)
        -- addAlternative
        --   $ docSetBaseY
        --   $ docPar
        --     leftOperandDoc
        --     ( docLines
        --      $ (appListDocs <&> \(od, ed) -> docCols ColOpPrefix [appSep od, docSetBaseY ed])
        --       ++ [docCols ColOpPrefix [appSep opLastDoc, docSetBaseY expLastDoc]]
        --     )
        -- > one
        -- >   + two
        -- >   + three
        addAlternative $ docPar
          leftOperandDoc
          (docLines
          $ (appListDocs <&> \(od, ed) ->
              docCols ColOpPrefix [appSep od, docSetBaseY ed]
            )
          ++ [docCols ColOpPrefix [appSep opLastDoc, docSetBaseY expLastDoc]]
          )
    OpApp _ expLeft expOp expRight -> do
      expDocLeft <- docSharedWrapper layoutExpr expLeft
      expDocOp <- docSharedWrapper layoutExpr expOp
      expDocRight <- docSharedWrapper layoutExpr expRight
      let
        allowPar = case (expOp, expRight) of
          (L _ (HsVar _ (L _ (Unqual occname))), _)
            | occNameString occname == "$" -> True
          (_, L _ (HsApp _ _ (L _ HsVar{}))) -> False
          _ -> True
      let
        leftIsDoBlock = case expLeft of
          L _ HsDo{} -> True
          _ -> False
      runFilteredAlternative $ do
        -- one-line
        addAlternative $ docSeq
          [ appSep $ docForceSingleline expDocLeft
          , appSep $ docForceSingleline expDocOp
          , docForceSingleline expDocRight
          ]
        -- -- line + freely indented block for right expression
        -- addAlternative
        --   $ docSeq
        --   [ appSep $ docForceSingleline expDocLeft
        --   , appSep $ docForceSingleline expDocOp
        --   , docSetBaseY $ docAddBaseY BrIndentRegular expDocRight
        --   ]
        -- two-line
        addAlternative $ do
          let
            expDocOpAndRight = docForceSingleline $ docCols
              ColOpPrefix
              [appSep $ expDocOp, docSetBaseY expDocRight]
          if leftIsDoBlock
            then docLines [expDocLeft, expDocOpAndRight]
            else docAddBaseY BrIndentRegular
              $ docPar expDocLeft expDocOpAndRight
              -- TODO: in both cases, we don't force expDocLeft to be
              -- single-line, which has certain.. interesting consequences.
              -- At least, the "two-line" label is not entirely
              -- accurate.
        -- one-line + par
        addAlternativeCond allowPar $ docSeq
          [ appSep $ docForceSingleline expDocLeft
          , appSep $ docForceSingleline expDocOp
          , docForceParSpacing expDocRight
          ]
        -- more lines
        addAlternative $ do
          let
            expDocOpAndRight =
              docCols ColOpPrefix [appSep expDocOp, docSetBaseY expDocRight]
          if leftIsDoBlock
            then docLines [expDocLeft, expDocOpAndRight]
            else docAddBaseY BrIndentRegular
              $ docPar expDocLeft expDocOpAndRight
    NegApp _ op _ -> do
      opDoc <- docSharedWrapper layoutExpr op
      docSeq [docLit $ Text.pack "-", opDoc]
    HsPar _ innerExp -> do
      innerExpDoc <- docSharedWrapper (docWrapNode lexpr . layoutExpr) innerExp
      docAlt
        [ docSeq
          [ docLit $ Text.pack "("
          , docForceSingleline innerExpDoc
          , docLit $ Text.pack ")"
          ]
        , docSetBaseY $ docLines
          [ docCols
            ColOpPrefix
            [ docLit $ Text.pack "("
            , docAddBaseY (BrIndentSpecial 2) innerExpDoc
            ]
          , docLit $ Text.pack ")"
          ]
        ]
    SectionL _ left op -> do -- TODO: add to testsuite
      leftDoc <- docSharedWrapper layoutExpr left
      opDoc <- docSharedWrapper layoutExpr op
      docSeq [leftDoc, docSeparator, opDoc]
    SectionR _ op right -> do -- TODO: add to testsuite
      opDoc <- docSharedWrapper layoutExpr op
      rightDoc <- docSharedWrapper layoutExpr right
      docSeq [opDoc, docSeparator, rightDoc]
    ExplicitTuple _ args boxity -> do
      let
        argExprs = args <&> \arg -> case arg of
          (L _ (Present _ e)) -> (arg, Just e)
          (L _ (Missing NoExtField)) -> (arg, Nothing)
      argDocs <- forM argExprs $ docSharedWrapper $ \(arg, exprM) ->
        docWrapNode arg $ maybe docEmpty layoutExpr exprM
      hasComments <-
        orM
          (hasCommentsBetween lexpr AnnOpenP AnnCloseP
          : map hasAnyCommentsBelow args
          )
      let
        (openLit, closeLit) = case boxity of
          Boxed -> (docLit $ Text.pack "(", docLit $ Text.pack ")")
          Unboxed -> (docParenHashLSep, docParenHashRSep)
      case splitFirstLast argDocs of
        FirstLastEmpty ->
          docSeq [openLit, docNodeAnnKW lexpr (Just AnnOpenP) closeLit]
        FirstLastSingleton e -> docAlt
          [ docCols
            ColTuple
            [ openLit
            , docNodeAnnKW lexpr (Just AnnOpenP) $ docForceSingleline e
            , closeLit
            ]
          , docSetBaseY $ docLines
            [ docSeq
              [ openLit
              , docNodeAnnKW lexpr (Just AnnOpenP) $ docForceSingleline e
              ]
            , closeLit
            ]
          ]
        FirstLast e1 ems eN -> runFilteredAlternative $ do
          addAlternativeCond (not hasComments)
            $ docCols ColTuple
            $ [docSeq [openLit, docForceSingleline e1]]
            ++ (ems <&> \e -> docSeq [docCommaSep, docForceSingleline e])
            ++ [ docSeq
                   [ docCommaSep
                   , docNodeAnnKW lexpr (Just AnnOpenP) (docForceSingleline eN)
                   , closeLit
                   ]
               ]
          addAlternative
            $ let
                start = docCols ColTuples [appSep openLit, e1]
                linesM = ems <&> \d -> docCols ColTuples [docCommaSep, d]
                lineN = docCols
                  ColTuples
                  [docCommaSep, docNodeAnnKW lexpr (Just AnnOpenP) eN]
                end = closeLit
              in docSetBaseY $ docLines $ [start] ++ linesM ++ [lineN, end]
    HsCase _ cExp (MG _ (L _ []) _) -> do
      cExpDoc <- docSharedWrapper layoutExpr cExp
      docAlt
        [ docAddBaseY BrIndentRegular $ docSeq
          [ appSep $ docLit $ Text.pack "case"
          , appSep $ docForceSingleline cExpDoc
          , docLit $ Text.pack "of {}"
          ]
        , docPar
          (docAddBaseY BrIndentRegular
          $ docPar (docLit $ Text.pack "case") cExpDoc
          )
          (docLit $ Text.pack "of {}")
        ]
    HsCase _ cExp (MG _ lmatches@(L _ matches) _) -> do
      cExpDoc <- docSharedWrapper layoutExpr cExp
      binderDoc <- docLit $ Text.pack "->"
      funcPatDocs <-
        docWrapNode lmatches
        $ layoutPatternBind Nothing binderDoc
        `mapM` matches
      docAlt
        [ docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docSeq
            [ appSep $ docLit $ Text.pack "case"
            , appSep $ docForceSingleline cExpDoc
            , docLit $ Text.pack "of"
            ]
          )
          (docSetBaseAndIndent
          $ docNonBottomSpacing
          $ docLines
          $ return
          <$> funcPatDocs
          )
        , docPar
          (docAddBaseY BrIndentRegular
          $ docPar (docLit $ Text.pack "case") cExpDoc
          )
          (docAddBaseY BrIndentRegular $ docPar
            (docLit $ Text.pack "of")
            (docSetBaseAndIndent
            $ docNonBottomSpacing
            $ docLines
            $ return
            <$> funcPatDocs
            )
          )
        ]
    HsIf _ ifExpr thenExpr elseExpr -> do
      ifExprDoc <- docSharedWrapper layoutExpr ifExpr
      thenExprDoc <- docSharedWrapper layoutExpr thenExpr
      elseExprDoc <- docSharedWrapper layoutExpr elseExpr
      hasComments <- hasAnyCommentsBelow lexpr
      let
        maySpecialIndent = case indentPolicy of
          IndentPolicyLeft -> BrIndentRegular
          IndentPolicyMultiple -> BrIndentRegular
          IndentPolicyFree -> BrIndentSpecial 3
      -- TODO: some of the alternatives (especially last and last-but-one)
      -- overlap.
      docSetIndentLevel $ runFilteredAlternative $ do
        -- if _ then _ else _
        addAlternativeCond (not hasComments) $ docSeq
          [ appSep $ docLit $ Text.pack "if"
          , appSep $ docForceSingleline ifExprDoc
          , appSep $ docLit $ Text.pack "then"
          , appSep $ docForceSingleline thenExprDoc
          , appSep $ docLit $ Text.pack "else"
          , docForceSingleline elseExprDoc
          ]
        -- either
        --   if expr
        --   then foo
        --     bar
        --   else foo
        --     bar
        -- or
        --   if expr
        --   then
        --     stuff
        --   else
        --     stuff
        -- note that this has par-spacing
        addAlternative $ docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docSeq
            [ docNodeAnnKW lexpr Nothing $ appSep $ docLit $ Text.pack "if"
            , docNodeAnnKW lexpr (Just AnnIf) $ docForceSingleline ifExprDoc
            ]
          )
          (docLines
            [ docAddBaseY BrIndentRegular
            $ docNodeAnnKW lexpr (Just AnnThen)
            $ docNonBottomSpacing
            $ docAlt
                [ docSeq
                  [ appSep $ docLit $ Text.pack "then"
                  , docForceParSpacing thenExprDoc
                  ]
                , docAddBaseY BrIndentRegular
                  $ docPar (docLit $ Text.pack "then") thenExprDoc
                ]
            , docAddBaseY BrIndentRegular $ docNonBottomSpacing $ docAlt
              [ docSeq
                [ appSep $ docLit $ Text.pack "else"
                , docForceParSpacing elseExprDoc
                ]
              , docAddBaseY BrIndentRegular
                $ docPar (docLit $ Text.pack "else") elseExprDoc
              ]
            ]
          )
        -- either
        --   if multi
        --      line
        --      condition
        --   then foo
        --     bar
        --   else foo
        --     bar
        -- or
        --   if multi
        --      line
        --      condition
        --   then
        --     stuff
        --   else
        --     stuff
        -- note that this does _not_ have par-spacing
        addAlternative $ docAddBaseY BrIndentRegular $ docPar
          (docAddBaseY maySpecialIndent $ docSeq
            [ docNodeAnnKW lexpr Nothing $ appSep $ docLit $ Text.pack "if"
            , docNodeAnnKW lexpr (Just AnnIf) $ ifExprDoc
            ]
          )
          (docLines
            [ docAddBaseY BrIndentRegular
            $ docNodeAnnKW lexpr (Just AnnThen)
            $ docAlt
                [ docSeq
                  [ appSep $ docLit $ Text.pack "then"
                  , docForceParSpacing thenExprDoc
                  ]
                , docAddBaseY BrIndentRegular
                  $ docPar (docLit $ Text.pack "then") thenExprDoc
                ]
            , docAddBaseY BrIndentRegular $ docAlt
              [ docSeq
                [ appSep $ docLit $ Text.pack "else"
                , docForceParSpacing elseExprDoc
                ]
              , docAddBaseY BrIndentRegular
                $ docPar (docLit $ Text.pack "else") elseExprDoc
              ]
            ]
          )
        addAlternative $ docSetBaseY $ docLines
          [ docAddBaseY maySpecialIndent $ docSeq
            [ docNodeAnnKW lexpr Nothing $ appSep $ docLit $ Text.pack "if"
            , docNodeAnnKW lexpr (Just AnnIf) $ ifExprDoc
            ]
          , docNodeAnnKW lexpr (Just AnnThen)
          $ docAddBaseY BrIndentRegular
          $ docPar (docLit $ Text.pack "then") thenExprDoc
          , docAddBaseY BrIndentRegular
            $ docPar (docLit $ Text.pack "else") elseExprDoc
          ]
    HsMultiIf _ cases -> do
      clauseDocs <- cases `forM` layoutGrhs
      binderDoc <- docLit $ Text.pack "->"
      hasComments <- hasAnyCommentsBelow lexpr
      docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
        (docLit $ Text.pack "if")
        (layoutPatternBindFinal
          Nothing
          binderDoc
          Nothing
          clauseDocs
          Nothing
          hasComments
        )
    HsLet _ binds exp1 -> do
      expDoc1 <- docSharedWrapper layoutExpr exp1
      -- We jump through some ugly hoops here to ensure proper sharing.
      hasComments <- hasAnyCommentsBelow lexpr
      mBindDocs <- fmap (fmap pure) <$> layoutLocalBinds binds
      let
        ifIndentFreeElse :: a -> a -> a
        ifIndentFreeElse x y = case indentPolicy of
          IndentPolicyLeft -> y
          IndentPolicyMultiple -> y
          IndentPolicyFree -> x
      -- this `docSetBaseAndIndent` might seem out of place (especially the
      -- Indent part; setBase is necessary due to the use of docLines below),
      -- but is here due to ghc-exactprint's DP handling of "let" in
      -- particular.
      -- Just pushing another indentation level is a straightforward approach
      -- to making brittany idempotent, even though the result is non-optimal
      -- if "let" is moved horizontally as part of the transformation, as the
      -- comments before the first let item are moved horizontally with it.
      docSetBaseAndIndent $ case mBindDocs of
        Just [bindDoc] -> runFilteredAlternative $ do
          addAlternativeCond (not hasComments) $ docSeq
            [ appSep $ docLit $ Text.pack "let"
            , docNodeAnnKW lexpr (Just AnnLet) $ appSep $ docForceSingleline
              bindDoc
            , appSep $ docLit $ Text.pack "in"
            , docForceSingleline expDoc1
            ]
          addAlternative $ docLines
            [ docNodeAnnKW lexpr (Just AnnLet) $ docAlt
              [ docSeq
                [ appSep $ docLit $ Text.pack "let"
                , ifIndentFreeElse docSetBaseAndIndent docForceSingleline
                  $ bindDoc
                ]
              , docAddBaseY BrIndentRegular $ docPar
                (docLit $ Text.pack "let")
                (docSetBaseAndIndent bindDoc)
              ]
            , docAlt
              [ docSeq
                [ appSep $ docLit $ Text.pack $ ifIndentFreeElse "in " "in"
                , ifIndentFreeElse
                  docSetBaseAndIndent
                  docForceSingleline
                  expDoc1
                ]
              , docAddBaseY BrIndentRegular
                $ docPar (docLit $ Text.pack "in") (docSetBaseY expDoc1)
              ]
            ]
        Just bindDocs@(_ : _) -> runFilteredAlternative $ do
          --either
          --  let
          --    a = b
          --    c = d
          --  in foo
          --    bar
          --    baz
          --or
          --  let
          --    a = b
          --    c = d
          --  in
          --    fooooooooooooooooooo
          let
            noHangingBinds =
              [ docNonBottomSpacing $ docAddBaseY BrIndentRegular $ docPar
                (docLit $ Text.pack "let")
                (docSetBaseAndIndent $ docLines bindDocs)
              , docSeq
                [ docLit $ Text.pack "in "
                , docAddBaseY BrIndentRegular $ docForceParSpacing expDoc1
                ]
              ]
          addAlternative $ case indentPolicy of
            IndentPolicyLeft -> docLines noHangingBinds
            IndentPolicyMultiple -> docLines noHangingBinds
            IndentPolicyFree -> docLines
              [ docNodeAnnKW lexpr (Just AnnLet) $ docSeq
                [ appSep $ docLit $ Text.pack "let"
                , docSetBaseAndIndent $ docLines bindDocs
                ]
              , docSeq [appSep $ docLit $ Text.pack "in ", docSetBaseY expDoc1]
              ]
          addAlternative $ docLines
            [ docNodeAnnKW lexpr (Just AnnLet)
            $ docAddBaseY BrIndentRegular
            $ docPar
                (docLit $ Text.pack "let")
                (docSetBaseAndIndent $ docLines $ bindDocs)
            , docAddBaseY BrIndentRegular
              $ docPar (docLit $ Text.pack "in") (docSetBaseY $ expDoc1)
            ]
        _ -> docSeq [appSep $ docLit $ Text.pack "let in", expDoc1]
      -- docSeq [appSep $ docLit "let in", expDoc1]
    HsDo _ stmtCtx (L _ stmts) -> case stmtCtx of
      DoExpr _ -> do
        stmtDocs <- docSharedWrapper layoutStmt `mapM` stmts
        docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docLit $ Text.pack "do")
          (docSetBaseAndIndent $ docNonBottomSpacing $ docLines stmtDocs)
      MDoExpr _ -> do
        stmtDocs <- docSharedWrapper layoutStmt `mapM` stmts
        docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
          (docLit $ Text.pack "mdo")
          (docSetBaseAndIndent $ docNonBottomSpacing $ docLines stmtDocs)
      x
        | case x of
          ListComp -> True
          MonadComp -> True
          _ -> False
        -> do
          stmtDocs <- docSharedWrapper layoutStmt `mapM` stmts
          hasComments <- hasAnyCommentsBelow lexpr
          runFilteredAlternative $ do
            addAlternativeCond (not hasComments) $ docSeq
              [ docNodeAnnKW lexpr Nothing $ appSep $ docLit $ Text.pack "["
              , docNodeAnnKW lexpr (Just AnnOpenS)
              $ appSep
              $ docForceSingleline
              $ List.last stmtDocs
              , appSep $ docLit $ Text.pack "|"
              , docSeq
              $ List.intersperse docCommaSep
              $ docForceSingleline
              <$> List.init stmtDocs
              , docLit $ Text.pack " ]"
              ]
            addAlternative
              $ let
                  start = docCols
                    ColListComp
                    [ docNodeAnnKW lexpr Nothing $ appSep $ docLit $ Text.pack
                      "["
                    , docSetBaseY
                    $ docNodeAnnKW lexpr (Just AnnOpenS)
                    $ List.last stmtDocs
                    ]
                  (s1 : sM) = List.init stmtDocs
                  line1 =
                    docCols ColListComp [appSep $ docLit $ Text.pack "|", s1]
                  lineM = sM <&> \d -> docCols ColListComp [docCommaSep, d]
                  end = docLit $ Text.pack "]"
                in docSetBaseY $ docLines $ [start, line1] ++ lineM ++ [end]
      _ -> do
        -- TODO
        unknownNodeError "HsDo{} unknown stmtCtx" lexpr
    ExplicitList _ _ elems@(_ : _) -> do
      elemDocs <- elems `forM` docSharedWrapper layoutExpr
      hasComments <- hasAnyCommentsBelow lexpr
      case splitFirstLast elemDocs of
        FirstLastEmpty -> docSeq
          [ docLit $ Text.pack "["
          , docNodeAnnKW lexpr (Just AnnOpenS) $ docLit $ Text.pack "]"
          ]
        FirstLastSingleton e -> docAlt
          [ docSeq
            [ docLit $ Text.pack "["
            , docNodeAnnKW lexpr (Just AnnOpenS) $ docForceSingleline e
            , docLit $ Text.pack "]"
            ]
          , docSetBaseY $ docLines
            [ docSeq
              [ docLit $ Text.pack "["
              , docSeparator
              , docSetBaseY $ docNodeAnnKW lexpr (Just AnnOpenS) e
              ]
            , docLit $ Text.pack "]"
            ]
          ]
        FirstLast e1 ems eN -> runFilteredAlternative $ do
          addAlternativeCond (not hasComments)
            $ docSeq
            $ [docLit $ Text.pack "["]
            ++ List.intersperse
                 docCommaSep
                 (docForceSingleline
                 <$> (e1 : ems ++ [docNodeAnnKW lexpr (Just AnnOpenS) eN])
                 )
            ++ [docLit $ Text.pack "]"]
          addAlternative
            $ let
                start = docCols ColList [appSep $ docLit $ Text.pack "[", e1]
                linesM = ems <&> \d -> docCols ColList [docCommaSep, d]
                lineN = docCols
                  ColList
                  [docCommaSep, docNodeAnnKW lexpr (Just AnnOpenS) eN]
                end = docLit $ Text.pack "]"
              in docSetBaseY $ docLines $ [start] ++ linesM ++ [lineN] ++ [end]
    ExplicitList _ _ [] -> docLit $ Text.pack "[]"
    RecordCon _ lname fields -> case fields of
      HsRecFields fs Nothing -> do
        let nameDoc = docWrapNode lname $ docLit $ lrdrNameToText lname
        rFs <-
          fs `forM` \lfield@(L _ (HsRecField (L _ fieldOcc) rFExpr pun)) -> do
            let FieldOcc _ lnameF = fieldOcc
            rFExpDoc <- if pun
              then return Nothing
              else Just <$> docSharedWrapper layoutExpr rFExpr
            return $ (lfield, lrdrNameToText lnameF, rFExpDoc)
        recordExpression False indentPolicy lexpr nameDoc rFs
      HsRecFields [] (Just (L _ 0)) -> do
        let t = lrdrNameToText lname
        docWrapNode lname $ docLit $ t <> Text.pack " { .. }"
      HsRecFields fs@(_ : _) (Just (L _ dotdoti)) | dotdoti == length fs -> do
        let nameDoc = docWrapNode lname $ docLit $ lrdrNameToText lname
        fieldDocs <-
          fs `forM` \fieldl@(L _ (HsRecField (L _ fieldOcc) fExpr pun)) -> do
            let FieldOcc _ lnameF = fieldOcc
            fExpDoc <- if pun
              then return Nothing
              else Just <$> docSharedWrapper layoutExpr fExpr
            return (fieldl, lrdrNameToText lnameF, fExpDoc)
        recordExpression True indentPolicy lexpr nameDoc fieldDocs
      _ -> unknownNodeError "RecordCon with puns" lexpr
    RecordUpd _ rExpr fields -> do
      rExprDoc <- docSharedWrapper layoutExpr rExpr
      rFs <-
        fields `forM` \lfield@(L _ (HsRecField (L _ ambName) rFExpr pun)) -> do
          rFExpDoc <- if pun
            then return Nothing
            else Just <$> docSharedWrapper layoutExpr rFExpr
          return $ case ambName of
            Unambiguous _ n -> (lfield, lrdrNameToText n, rFExpDoc)
            Ambiguous _ n -> (lfield, lrdrNameToText n, rFExpDoc)
      recordExpression False indentPolicy lexpr rExprDoc rFs
    ExprWithTySig _ exp1 (HsWC _ (HsIB _ typ1)) -> do
      expDoc <- docSharedWrapper layoutExpr exp1
      typDoc <- docSharedWrapper layoutType typ1
      docSeq [appSep expDoc, appSep $ docLit $ Text.pack "::", typDoc]
    ArithSeq _ Nothing info -> case info of
      From e1 -> do
        e1Doc <- docSharedWrapper layoutExpr e1
        docSeq
          [ docLit $ Text.pack "["
          , appSep $ docForceSingleline e1Doc
          , docLit $ Text.pack "..]"
          ]
      FromThen e1 e2 -> do
        e1Doc <- docSharedWrapper layoutExpr e1
        e2Doc <- docSharedWrapper layoutExpr e2
        docSeq
          [ docLit $ Text.pack "["
          , docForceSingleline e1Doc
          , appSep $ docLit $ Text.pack ","
          , appSep $ docForceSingleline e2Doc
          , docLit $ Text.pack "..]"
          ]
      FromTo e1 eN -> do
        e1Doc <- docSharedWrapper layoutExpr e1
        eNDoc <- docSharedWrapper layoutExpr eN
        docSeq
          [ docLit $ Text.pack "["
          , appSep $ docForceSingleline e1Doc
          , appSep $ docLit $ Text.pack ".."
          , docForceSingleline eNDoc
          , docLit $ Text.pack "]"
          ]
      FromThenTo e1 e2 eN -> do
        e1Doc <- docSharedWrapper layoutExpr e1
        e2Doc <- docSharedWrapper layoutExpr e2
        eNDoc <- docSharedWrapper layoutExpr eN
        docSeq
          [ docLit $ Text.pack "["
          , docForceSingleline e1Doc
          , appSep $ docLit $ Text.pack ","
          , appSep $ docForceSingleline e2Doc
          , appSep $ docLit $ Text.pack ".."
          , docForceSingleline eNDoc
          , docLit $ Text.pack "]"
          ]
    ArithSeq{} -> briDocByExactInlineOnly "ArithSeq" lexpr
    HsBracket{} -> do
      -- TODO
      briDocByExactInlineOnly "HsBracket{}" lexpr
    HsRnBracketOut{} -> do
      -- TODO
      briDocByExactInlineOnly "HsRnBracketOut{}" lexpr
    HsTcBracketOut{} -> do
      -- TODO
      briDocByExactInlineOnly "HsTcBracketOut{}" lexpr
    HsSpliceE _ (HsQuasiQuote _ _ quoter _loc content) -> do
      allocateNode $ BDFPlain
        (Text.pack
        $ "["
        ++ showOutputable quoter
        ++ "|"
        ++ showOutputable content
        ++ "|]"
        )
    HsSpliceE{} -> do
      -- TODO
      briDocByExactInlineOnly "HsSpliceE{}" lexpr
    HsProc{} -> do
      -- TODO
      briDocByExactInlineOnly "HsProc{}" lexpr
    HsStatic{} -> do
      -- TODO
      briDocByExactInlineOnly "HsStatic{}" lexpr
    HsTick{} -> do
      -- TODO
      briDocByExactInlineOnly "HsTick{}" lexpr
    HsBinTick{} -> do
      -- TODO
      briDocByExactInlineOnly "HsBinTick{}" lexpr
    HsConLikeOut{} -> do
      -- TODO
      briDocByExactInlineOnly "HsWrap{}" lexpr
    ExplicitSum{} -> do
      -- TODO
      briDocByExactInlineOnly "ExplicitSum{}" lexpr
    HsPragE{} -> do
      -- TODO
      briDocByExactInlineOnly "HsPragE{}" lexpr

recordExpression
  :: (Data.Data.Data lExpr, Data.Data.Data name)
  => Bool
  -> IndentPolicy
  -> GenLocated SrcSpan lExpr
  -> ToBriDocM BriDocNumbered
  -> [ ( GenLocated SrcSpan name
       , Text
       , Maybe (ToBriDocM BriDocNumbered)
       )
     ]
  -> ToBriDocM BriDocNumbered
recordExpression False _ lexpr nameDoc [] = docSeq
  [ docNodeAnnKW lexpr (Just AnnOpenC)
    $ docSeq [nameDoc, docLit $ Text.pack "{"]
  , docLit $ Text.pack "}"
  ]
recordExpression True _ lexpr nameDoc [] = docSeq -- this case might still be incomplete, and is probably not used
         -- atm anyway.
  [ docNodeAnnKW lexpr (Just AnnOpenC)
    $ docSeq [nameDoc, docLit $ Text.pack "{"]
  , docLit $ Text.pack " .. }"
  ]
recordExpression dotdot indentPolicy lexpr nameDoc rFs@(rF1 : rFr) = do
  let (rF1f, rF1n, rF1e) = rF1
  runFilteredAlternative $ do
    -- container { fieldA = blub, fieldB = blub }
    addAlternative $ docSeq
      [ docNodeAnnKW lexpr Nothing $ appSep $ docForceSingleline nameDoc
      , appSep $ docLit $ Text.pack "{"
      , docSeq $ List.intersperse docCommaSep $ rFs <&> \case
        (lfield, fieldStr, Just fieldDoc) -> docWrapNode lfield $ docSeq
          [ appSep $ docLit fieldStr
          , appSep $ docLit $ Text.pack "="
          , docForceSingleline fieldDoc
          ]
        (lfield, fieldStr, Nothing) -> docWrapNode lfield $ docLit fieldStr
      , if dotdot
        then docSeq [docCommaSep, docLit $ Text.pack "..", docSeparator]
        else docSeparator
      , docLit $ Text.pack "}"
      ]
    -- hanging single-line fields
    -- container { fieldA = blub
    --           , fieldB = blub
    --           }
    addAlternativeCond (indentPolicy == IndentPolicyFree) $ docSeq
      [ docNodeAnnKW lexpr Nothing $ docForceSingleline $ appSep nameDoc
      , docSetBaseY
      $ docLines
      $ let
          line1 = docCols
            ColRec
            [ appSep $ docLit $ Text.pack "{"
            , docWrapNodePrior rF1f $ appSep $ docLit rF1n
            , case rF1e of
              Just x -> docWrapNodeRest rF1f $ docSeq
                [appSep $ docLit $ Text.pack "=", docForceSingleline x]
              Nothing -> docEmpty
            ]
          lineR = rFr <&> \(lfield, fText, fDoc) ->
            docWrapNode lfield $ docCols
              ColRec
              [ docCommaSep
              , appSep $ docLit fText
              , case fDoc of
                Just x ->
                  docSeq [appSep $ docLit $ Text.pack "=", docForceSingleline x]
                Nothing -> docEmpty
              ]
          dotdotLine = if dotdot
            then docCols
              ColRec
              [ docNodeAnnKW lexpr (Just AnnOpenC) docCommaSep
              , docNodeAnnKW lexpr (Just AnnDotdot) $ docLit $ Text.pack ".."
              ]
            else docNodeAnnKW lexpr (Just AnnOpenC) docEmpty
          lineN = docLit $ Text.pack "}"
        in [line1] ++ lineR ++ [dotdotLine, lineN]
      ]
    -- non-hanging with expressions placed to the right of the names
    -- container
    -- { fieldA = blub
    -- , fieldB = potentially
    --     multiline
    -- }
    addAlternative $ docSetParSpacing $ docAddBaseY BrIndentRegular $ docPar
      (docNodeAnnKW lexpr Nothing nameDoc)
      (docNonBottomSpacing
      $ docLines
      $ let
          line1 = docCols
            ColRec
            [ appSep $ docLit $ Text.pack "{"
            , docWrapNodePrior rF1f $ appSep $ docLit rF1n
            , docWrapNodeRest rF1f $ case rF1e of
              Just x -> runFilteredAlternative $ do
                addAlternativeCond (indentPolicy == IndentPolicyFree) $ do
                  docSeq [appSep $ docLit $ Text.pack "=", docSetBaseY x]
                addAlternative $ do
                  docSeq
                    [appSep $ docLit $ Text.pack "=", docForceParSpacing x]
                addAlternative $ do
                  docAddBaseY BrIndentRegular
                    $ docPar (docLit $ Text.pack "=") x
              Nothing -> docEmpty
            ]
          lineR = rFr <&> \(lfield, fText, fDoc) ->
            docWrapNode lfield $ docCols
              ColRec
              [ docCommaSep
              , appSep $ docLit fText
              , case fDoc of
                Just x -> runFilteredAlternative $ do
                  addAlternativeCond (indentPolicy == IndentPolicyFree) $ do
                    docSeq [appSep $ docLit $ Text.pack "=", docSetBaseY x]
                  addAlternative $ do
                    docSeq
                      [appSep $ docLit $ Text.pack "=", docForceParSpacing x]
                  addAlternative $ do
                    docAddBaseY BrIndentRegular
                      $ docPar (docLit $ Text.pack "=") x
                Nothing -> docEmpty
              ]
          dotdotLine = if dotdot
            then docCols
              ColRec
              [ docNodeAnnKW lexpr (Just AnnOpenC) docCommaSep
              , docNodeAnnKW lexpr (Just AnnDotdot) $ docLit $ Text.pack ".."
              ]
            else docNodeAnnKW lexpr (Just AnnOpenC) docEmpty
          lineN = docLit $ Text.pack "}"
        in [line1] ++ lineR ++ [dotdotLine, lineN]
      )

litBriDoc :: HsLit GhcPs -> BriDocFInt
litBriDoc = \case
  HsChar (SourceText t) _c -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ ['\'', c, '\'']
  HsCharPrim (SourceText t) _c -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ ['\'', c, '\'']
  HsString (SourceText t) _fastString -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ FastString.unpackFS fastString
  HsStringPrim (SourceText t) _byteString -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ Data.ByteString.Char8.unpack byteString
  HsInt _ (IL (SourceText t) _ _) -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsIntPrim (SourceText t) _i -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsWordPrim (SourceText t) _i -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsInt64Prim (SourceText t) _i -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsWord64Prim (SourceText t) _i -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsInteger (SourceText t) _i _type -> BDFLit $ Text.pack t -- BDFLit $ Text.pack $ show i
  HsRat _ (FL (SourceText t) _ _) _type -> BDFLit $ Text.pack t
  HsFloatPrim _ (FL (SourceText t) _ _) -> BDFLit $ Text.pack t
  HsDoublePrim _ (FL (SourceText t) _ _) -> BDFLit $ Text.pack t
  _ -> error "litBriDoc: literal with no SourceText"

overLitValBriDoc :: OverLitVal -> BriDocFInt
overLitValBriDoc = \case
  HsIntegral (IL (SourceText t) _ _) -> BDFLit $ Text.pack t
  HsFractional (FL (SourceText t) _ _) -> BDFLit $ Text.pack t
  HsIsString (SourceText t) _ -> BDFLit $ Text.pack t
  _ -> error "overLitValBriDoc: literal with no SourceText"