-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Client.GenBounds
-- Copyright   :  (c) Doug Beardsley 2015
-- License     :  BSD-like
--
-- Maintainer  :  cabal-devel@gmail.com
-- Stability   :  provisional
-- Portability :  portable
--
-- The cabal gen-bounds command for generating PVP-compliant version bounds.
-----------------------------------------------------------------------------
module Distribution.Client.GenBounds (
    genBounds
  ) where

import Data.Version
         ( Version(..), showVersion )
import Distribution.Client.Init
         ( incVersion )
import Distribution.Client.Freeze
         ( getFreezePkgs )
import Distribution.Client.Sandbox.Types
         ( SandboxPackageInfo(..) )
import Distribution.Client.Setup
         ( GlobalFlags(..), FreezeFlags(..), RepoContext )
import Distribution.Package
         ( Package(..), Dependency(..), PackageName(..)
         , packageName, packageVersion )
import Distribution.PackageDescription
         ( buildDepends )
import Distribution.PackageDescription.Configuration
         ( finalizePackageDescription )
import Distribution.PackageDescription.Parse
         ( readPackageDescription )
import Distribution.Simple.Compiler
         ( Compiler, PackageDBStack, compilerInfo )
import Distribution.Simple.Program
         ( ProgramConfiguration )
import Distribution.Simple.Utils
         ( tryFindPackageDesc )
import Distribution.System
         ( Platform )
import Distribution.Verbosity
         ( Verbosity )
import Distribution.Version
         ( LowerBound(..), UpperBound(..), VersionRange(..), asVersionIntervals
         , orLaterVersion, earlierVersion, intersectVersionRanges )
import System.Directory
         ( getCurrentDirectory )

-- | Does this version range have an upper bound?
hasUpperBound :: VersionRange -> Bool
hasUpperBound vr =
    case asVersionIntervals vr of
      [] -> False
      is -> if snd (last is) == NoUpperBound then False else True

-- | Given a version, return an API-compatible (according to PVP) version range.
--
-- Example: @0.4.1.2@ produces the version range @>= 0.4.1 && < 0.5@.
--
-- This version is slightly different than the one in
-- 'Distribution.Client.Init'.  This one uses a.b.c as the lower bound because
-- the user could be using a new function introduced in a.b.c which would make
-- ">= a.b" incorrect.
pvpize :: Version -> VersionRange
pvpize v = orLaterVersion (vn 3)
           `intersectVersionRanges`
           earlierVersion (incVersion 1 (vn 2))
  where
    vn n = (v { versionBranch = take n (versionBranch v) })

-- | Show the PVP-mandated version range for this package. The @padTo@ parameter
-- specifies the width of the package name column.
showBounds :: Package pkg => Int -> pkg -> String
showBounds padTo p = unwords $
    (padAfter padTo $ unPackageName $ packageName p) :
    map showInterval (asVersionIntervals $ pvpize $ packageVersion p)
  where
    padAfter :: Int -> String -> String
    padAfter n str = str ++ replicate (n - length str) ' '

    showInterval :: (LowerBound, UpperBound) -> String
    showInterval (LowerBound _ _, NoUpperBound) =
      error "Error: expected upper bound...this should never happen!"
    showInterval (LowerBound l _, UpperBound u _) =
      unwords [">=", showVersion l, "&& <", showVersion u]

-- | Entry point for the @gen-bounds@ command.
genBounds
    :: Verbosity
    -> PackageDBStack
    -> RepoContext
    -> Compiler
    -> Platform
    -> ProgramConfiguration
    -> Maybe SandboxPackageInfo
    -> GlobalFlags
    -> FreezeFlags
    -> IO ()
genBounds verbosity packageDBs repoCtxt comp platform conf mSandboxPkgInfo
      globalFlags freezeFlags = do

    let cinfo = compilerInfo comp

    cwd <- getCurrentDirectory
    path <- tryFindPackageDesc cwd
    gpd <- readPackageDescription verbosity path
    let epd = finalizePackageDescription [] (const True) platform cinfo [] gpd
    case epd of
      Left _ -> putStrLn "finalizePackageDescription failed"
      Right (pd,_) -> do
        let needBounds = filter (not . hasUpperBound . depVersion) $
                         buildDepends pd

        if (null needBounds)
          then putStrLn
               "Congratulations, all your dependencies have upper bounds!"
          else go needBounds
  where
     go needBounds = do
       pkgs  <- getFreezePkgs
                  verbosity packageDBs repoCtxt comp platform conf
                  mSandboxPkgInfo globalFlags freezeFlags

       putStrLn boundsNeededMsg

       let isNeeded pkg = unPackageName (packageName pkg)
                          `elem` map depName needBounds
       let thePkgs = filter isNeeded pkgs

       let padTo = maximum $ map (length . unPackageName . packageName) pkgs
       mapM_ (putStrLn . (++",") . showBounds padTo) thePkgs

     depName :: Dependency -> String
     depName (Dependency (PackageName nm) _) = nm

     depVersion :: Dependency -> VersionRange
     depVersion (Dependency _ vr) = vr

-- | The message printed when some dependencies are found to be lacking proper
-- PVP-mandated bounds.
boundsNeededMsg :: String
boundsNeededMsg = unlines
  [ ""
  , "The following packages need bounds and here is a suggested starting point."
  , "You can copy and paste this into the build-depends section in your .cabal"
  , "file and it should work (with the appropriate removal of commas)."
  , ""
  , "Note that version bounds are a statement that you've successfully built and"
  , "tested your package and expect it to work with any of the specified package"
  , "versions (PROVIDED that those packages continue to conform with the PVP)."
  , "Therefore, the version bounds generated here are the most conservative"
  , "based on the versions that you are currently building with.  If you know"
  , "your package will work with versions outside the ranges generated here,"
  , "feel free to widen them."
  , ""
  ]
