{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      :  Distribution.Client.Init.Format
-- Copyright   :  (c) Brent Yorgey 2009
-- License     :  BSD-like
--
-- Maintainer  :  cabal-devel@haskell.org
-- Stability   :  provisional
-- Portability :  portable
--
-- Pretty printing and field formatting utilities used for file creation.
--
module Distribution.Client.Init.Format
( -- * cabal file formatters
  listFieldS
, field
, fieldD
, commentedOutWithComments
, withComments
, annNoComments
, postProcessFieldLines
  -- * stanza generation
, mkCommonStanza
, mkLibStanza
, mkExeStanza
, mkTestStanza
, mkPkgDescription
) where


import Distribution.Pretty
import Distribution.Fields
import Distribution.Client.Init.Types
import Distribution.License
import Text.PrettyPrint
import Distribution.Solver.Compat.Prelude hiding (empty)
import Distribution.PackageDescription.FieldGrammar
import Distribution.Simple.Utils hiding (cabalVersion)
import Distribution.Utils.Path
import Distribution.Package (unPackageName)
import qualified Distribution.SPDX.License as SPDX
import Distribution.CabalSpecVersion
import Distribution.FieldGrammar.Newtypes (SpecLicense(SpecLicense))


-- | Construct a 'PrettyField' from a field that can be automatically
--   converted to a 'Doc' via 'display'.
field
    :: Pretty b
    => FieldName
    -> (a -> b)
    -> a
    -> [String]
    -> Bool
    -> WriteOpts
    -> PrettyField FieldAnnotation
field fieldName modifier fieldContents =
    fieldD fieldName (pretty $ modifier fieldContents)

-- | Construct a 'PrettyField' from a 'Doc' Flag.
fieldD
    :: FieldName -- ^ Name of the field
    -> Doc       -- ^ Field contents
    -> [String]  -- ^ Comment to explain the field
    -> Bool      -- ^ Should the field be included (commented out) even if blank?
    -> WriteOpts
    -> PrettyField FieldAnnotation
fieldD fieldName fieldContents fieldComments includeField opts
      -- If the "--no-comments" or "--minimal" flag is set, strip comments.
    | hasNoComments || isMinimal = contents NoComment
    | otherwise                  = contents $ commentPositionFor fieldName fieldComments
  where
    commentPositionFor fn
      | fn == "cabal-version" = CommentAfter
      | otherwise = CommentBefore

    isMinimal = _optMinimal opts
    hasNoComments = _optNoComments opts

    contents
        -- If there is no content, optionally produce a commented out field.
      | fieldContents == empty = fieldSEmptyContents
      | otherwise              = fieldSWithContents

    fieldSEmptyContents cs
      | not includeField || isMinimal = PrettyEmpty
      | otherwise = PrettyField
        (commentedOutWithComments cs)
        fieldName
        empty

    fieldSWithContents cs =
      PrettyField (withComments cs) fieldName fieldContents


-- | A field annotation instructing the pretty printer to comment out the field
--   and any contents, with no comments.
commentedOutWithComments :: CommentPosition -> FieldAnnotation
commentedOutWithComments (CommentBefore cs) = FieldAnnotation True . CommentBefore $ map commentNoTrailing cs
commentedOutWithComments (CommentAfter  cs) = FieldAnnotation True . CommentAfter  $ map commentNoTrailing cs
commentedOutWithComments NoComment = FieldAnnotation True NoComment

-- | A field annotation with the specified comment lines.
withComments :: CommentPosition -> FieldAnnotation
withComments (CommentBefore cs) = FieldAnnotation False . CommentBefore $ map commentNoTrailing cs
withComments (CommentAfter  cs) = FieldAnnotation False . CommentAfter  $ map commentNoTrailing cs
withComments NoComment = FieldAnnotation False NoComment

-- | A field annotation with no comments.
annNoComments :: FieldAnnotation
annNoComments = FieldAnnotation False NoComment

postProcessFieldLines :: FieldAnnotation -> [String] -> [String]
postProcessFieldLines ann
  | annCommentedOut ann = fmap commentNoTrailing
  | otherwise = id

-- -------------------------------------------------------------------- --
-- Stanzas

-- The common stanzas are hardcoded for simplicity purposes,
-- see https://github.com/haskell/cabal/pull/7558#discussion_r693173846
mkCommonStanza :: WriteOpts -> PrettyField FieldAnnotation
mkCommonStanza opts = case specHasCommonStanzas $ _optCabalSpec opts of
  NoCommonStanzas -> PrettyEmpty
  _ -> PrettySection
    annNoComments
    "common"
    [text "warnings"]
    [field "ghc-options" text "-Wall" [] False opts]

mkLibStanza :: WriteOpts -> LibTarget -> PrettyField FieldAnnotation
mkLibStanza opts (LibTarget srcDirs lang expMods otherMods exts deps tools) =
  PrettySection annNoComments (toUTF8BS "library") []
    [ case specHasCommonStanzas $ _optCabalSpec opts of
        NoCommonStanzas -> PrettyEmpty
        _ -> field "import" (hsep . map text) ["warnings"]
          ["Import common warning flags."]
          False
          opts

    , field "exposed-modules" formatExposedModules (toList expMods)
      ["Modules exported by the library."]
      True
      opts

    , field "other-modules" formatOtherModules otherMods
      ["Modules included in this library but not exported."]
      True
      opts

    , field "other-extensions" formatOtherExtensions exts
      ["LANGUAGE extensions used by modules in this package."]
      True
      opts

    , field "build-depends" formatDependencyList deps
      ["Other library packages from which modules are imported."]
      True
      opts

    , field "hs-source-dirs" formatHsSourceDirs (unsafeMakeSymbolicPath <$> srcDirs)
      ["Directories containing source files."]
      True
      opts

    , field (buildToolTag opts) formatDependencyList tools
      ["Extra tools (e.g. alex, hsc2hs, ...) needed to build the source."]
      False
      opts

    , field "default-language" id lang
      ["Base language which the package is written in."]
      True
      opts
    ]

mkExeStanza :: WriteOpts -> ExeTarget -> PrettyField FieldAnnotation
mkExeStanza opts (ExeTarget exeMain appDirs lang otherMods exts deps tools) =
    PrettySection annNoComments (toUTF8BS "executable") [exeName]
      [ case specHasCommonStanzas $ _optCabalSpec opts of
          NoCommonStanzas -> PrettyEmpty
          _ -> field "import" (hsep . map text) ["warnings"]
            ["Import common warning flags."]
            False
            opts

      , field "main-is" unsafeFromHs exeMain
         [".hs or .lhs file containing the Main module."]
         True
        opts

      , field "other-modules" formatOtherModules otherMods
        [ "Modules included in this executable, other than Main." ]
        True
        opts

      , field "other-extensions" formatOtherExtensions exts
        ["LANGUAGE extensions used by modules in this package."]
        True
        opts
      , field "build-depends" formatDependencyList deps
        ["Other library packages from which modules are imported."]
        True
        opts

      , field "hs-source-dirs" formatHsSourceDirs
        (unsafeMakeSymbolicPath <$> appDirs)
        ["Directories containing source files."]
        True
        opts

      , field (buildToolTag opts) formatDependencyList tools
        ["Extra tools (e.g. alex, hsc2hs, ...) needed to build the source."]
        False
        opts

      , field "default-language" id lang
        ["Base language which the package is written in."]
        True
        opts
      ]
    where
      exeName = pretty $ _optPkgName opts

mkTestStanza :: WriteOpts -> TestTarget -> PrettyField FieldAnnotation
mkTestStanza opts (TestTarget testMain dirs lang otherMods exts deps tools) =
    PrettySection annNoComments (toUTF8BS "test-suite") [suiteName]
       [ case specHasCommonStanzas $ _optCabalSpec opts of
           NoCommonStanzas -> PrettyEmpty
           _ -> field "import" (hsep . map text) ["warnings"]
             ["Import common warning flags."]
             False
             opts

       , field "default-language" id lang
         ["Base language which the package is written in."]
         True
         opts
       , field "other-modules" formatOtherModules otherMods
         [ "Modules included in this executable, other than Main." ]
         True
         opts

       , field "other-extensions" formatOtherExtensions exts
         ["LANGUAGE extensions used by modules in this package."]
         True
         opts

       , field "type" text "exitcode-stdio-1.0"
         ["The interface type and version of the test suite."]
         True
         opts

       , field "hs-source-dirs" formatHsSourceDirs
         (unsafeMakeSymbolicPath <$> dirs)
         ["Directories containing source files."]
         True
         opts

       , field "main-is" unsafeFromHs testMain
         ["The entrypoint to the test suite."]
         True
         opts

       , field  "build-depends" formatDependencyList deps
         ["Test dependencies."]
         True
         opts

       , field (buildToolTag opts) formatDependencyList tools
         ["Extra tools (e.g. alex, hsc2hs, ...) needed to build the source."]
         False
         opts
       ]
     where
       suiteName = text $ unPackageName (_optPkgName opts) ++ "-test"

mkPkgDescription :: WriteOpts -> PkgDescription -> [PrettyField FieldAnnotation]
mkPkgDescription opts pkgDesc =
    [ field "cabal-version" text (showCabalSpecVersion cabalSpec)
      [ "The cabal-version field refers to the version of the .cabal specification,"
      , "and can be different from the cabal-install (the tool) version and the"
      , "Cabal (the library) version you are using. As such, the Cabal (the library)"
      , "version used must be equal or greater than the version stated in this field."
      , "Starting from the specification version 2.2, the cabal-version field must be"
      , "the first thing in the cabal file."
      ]
      False
      opts

    , field "name" pretty (_pkgName pkgDesc)
      ["Initial package description '" ++ prettyShow (_optPkgName opts) ++ "' generated by"
      , "'cabal init'. For further documentation, see:"
      , "  http://haskell.org/cabal/users-guide/"
      , ""
      , "The name of the package."
      ]
      True
      opts

    , field  "version" pretty (_pkgVersion pkgDesc)
             ["The package version.",
              "See the Haskell package versioning policy (PVP) for standards",
              "guiding when and how versions should be incremented.",
              "https://pvp.haskell.org",
              "PVP summary:     +-+------- breaking API changes",
              "                 | | +----- non-breaking API additions",
              "                 | | | +--- code changes with no API change"]
      True
      opts

    , field "synopsis" text (_pkgSynopsis pkgDesc)
      ["A short (one-line) description of the package."]
      True
      opts

    , field "description" text ""
      ["A longer description of the package."]
      True
      opts

    , field "homepage" text (_pkgHomePage pkgDesc)
      ["URL for the project homepage or repository."]
      False
      opts

    , field "bug-reports" text ""
      ["A URL where users can report bugs."]
      False
      opts

    , field "license" pretty (_pkgLicense pkgDesc)
      ["The license under which the package is released."]
      True
      opts

    , case _pkgLicense pkgDesc of
        SpecLicense (Left  SPDX.NONE)          -> PrettyEmpty
        SpecLicense (Right AllRightsReserved)  -> PrettyEmpty
        SpecLicense (Right UnspecifiedLicense) -> PrettyEmpty
        _ -> field "license-file" text "LICENSE"
             ["The file containing the license text."]
             False
             opts

    , field "author" text (_pkgAuthor pkgDesc)
      ["The package author(s)."]
      True
      opts

    , field "maintainer" text (_pkgEmail pkgDesc)
      ["An email address to which users can send suggestions, bug reports, and patches."]
      True
      opts

    , field "copyright" text ""
      ["A copyright notice."]
      True
      opts

    , field "category" text (_pkgCategory pkgDesc)
      []
      False
      opts
    , field "build-type" text "Simple"
      []
      False
      opts
    , case _pkgExtraDocFiles pkgDesc of
        Nothing -> PrettyEmpty
        Just fs ->
          field "extra-doc-files" formatExtraSourceFiles  (toList fs)
          ["Extra doc files to be distributed with the package, such as a CHANGELOG or a README."]
          True
          opts

    , field "extra-source-files" formatExtraSourceFiles (toList $ _pkgExtraSrcFiles pkgDesc)
      ["Extra source files to be distributed with the package, such as examples, or a tutorial module."]
      True
      opts
    ]
  where
    cabalSpec = _pkgCabalVersion pkgDesc

-- -------------------------------------------------------------------- --
-- Utils

listFieldS :: [String] -> Doc
listFieldS = text . intercalate ", "

unsafeFromHs :: HsFilePath -> Doc
unsafeFromHs = text . _hsFilePath

buildToolTag :: WriteOpts -> FieldName
buildToolTag opts
  | _optCabalSpec opts < CabalSpecV3_0 = "build-tools"
  | otherwise = "build-tool-depends"

commentNoTrailing :: String -> String
commentNoTrailing "" = "--"
commentNoTrailing c  = "-- " ++ c
