{-# LANGUAGE DeriveGeneric #-}
-----------------------------------------------------------------------------
-- |
-- Module      :  Distribution.Client.World
-- Copyright   :  (c) Peter Robinson 2009
-- License     :  BSD-like
--
-- Maintainer  :  thaldyron@gmail.com
-- Stability   :  provisional
-- Portability :  portable
--
-- Interface to the world-file that contains a list of explicitly
-- requested packages. Meant to be imported qualified.
--
-- A world file entry stores the package-name, package-version, and
-- user flags.
-- For example, the entry generated by
-- # cabal install stm-io-hooks --flags="-debug"
-- looks like this:
-- # stm-io-hooks -any --flags="-debug"
-- To rebuild/upgrade the packages in world (e.g. when updating the compiler)
-- use
-- # cabal install world
--
-----------------------------------------------------------------------------
module Distribution.Client.World (
    WorldPkgInfo(..),
    insert,
    delete,
    getContents,
  ) where

import Prelude (sequence)
import Distribution.Client.Compat.Prelude hiding (getContents)

import Distribution.Types.Dependency
import Distribution.Types.Flag
         ( FlagAssignment, unFlagAssignment
         , unFlagName, parsecFlagAssignmentNonEmpty )
import Distribution.Simple.Utils
         ( die', info, chattyTry, writeFileAtomic )
import qualified Distribution.Compat.CharParsing as P
import qualified Text.PrettyPrint as Disp

import Data.List
         ( unionBy, deleteFirstsBy )
import System.IO.Error
         ( isDoesNotExistError )
import qualified Data.ByteString.Lazy.Char8 as B


data WorldPkgInfo = WorldPkgInfo Dependency FlagAssignment
  deriving (Show,Eq, Generic)

-- | Adds packages to the world file; creates the file if it doesn't
-- exist yet. Version constraints and flag assignments for a package are
-- updated if already present. IO errors are non-fatal.
insert :: Verbosity -> FilePath -> [WorldPkgInfo] -> IO ()
insert = modifyWorld $ unionBy equalUDep

-- | Removes packages from the world file.
-- Note: Currently unused as there is no mechanism in Cabal (yet) to
-- handle uninstalls. IO errors are non-fatal.
delete :: Verbosity -> FilePath -> [WorldPkgInfo] -> IO ()
delete = modifyWorld $ flip (deleteFirstsBy equalUDep)

-- | WorldPkgInfo values are considered equal if they refer to
-- the same package, i.e., we don't care about differing versions or flags.
equalUDep :: WorldPkgInfo -> WorldPkgInfo -> Bool
equalUDep (WorldPkgInfo (Dependency pkg1 _ _) _)
          (WorldPkgInfo (Dependency pkg2 _ _) _) = pkg1 == pkg2

-- | Modifies the world file by applying an update-function ('unionBy'
-- for 'insert', 'deleteFirstsBy' for 'delete') to the given list of
-- packages. IO errors are considered non-fatal.
modifyWorld :: ([WorldPkgInfo] -> [WorldPkgInfo]
                -> [WorldPkgInfo])
                        -- ^ Function that defines how
                        -- the list of user packages are merged with
                        -- existing world packages.
            -> Verbosity
            -> FilePath               -- ^ Location of the world file
            -> [WorldPkgInfo] -- ^ list of user supplied packages
            -> IO ()
modifyWorld _ _         _     []   = return ()
modifyWorld f verbosity world pkgs =
  chattyTry "Error while updating world-file. " $ do
    pkgsOldWorld <- getContents verbosity world
    -- Filter out packages that are not in the world file:
    let pkgsNewWorld = nubBy equalUDep $ f pkgs pkgsOldWorld
    -- 'Dependency' is not an Ord instance, so we need to check for
    -- equivalence the awkward way:
    if not (all (`elem` pkgsOldWorld) pkgsNewWorld &&
            all (`elem` pkgsNewWorld) pkgsOldWorld)
      then do
        info verbosity "Updating world file..."
        writeFileAtomic world . B.pack $ unlines
            [ (prettyShow pkg) | pkg <- pkgsNewWorld]
      else
        info verbosity "World file is already up to date."


-- | Returns the content of the world file as a list
getContents :: Verbosity -> FilePath -> IO [WorldPkgInfo]
getContents verbosity world = do
  content <- safelyReadFile world
  let result = map simpleParsec (lines $ B.unpack content)
  case sequence result of
    Nothing -> die' verbosity "Could not parse world file."
    Just xs -> return xs
  where
  safelyReadFile :: FilePath -> IO B.ByteString
  safelyReadFile file = B.readFile file `catchIO` handler
    where
      handler e | isDoesNotExistError e = return B.empty
                | otherwise             = ioError e


instance Pretty WorldPkgInfo where
  pretty (WorldPkgInfo dep flags) = pretty dep Disp.<+> dispFlags (unFlagAssignment flags)
    where
      dispFlags [] = Disp.empty
      dispFlags fs = Disp.text "--flags="
                  <<>> Disp.doubleQuotes (flagAssToDoc fs)
      flagAssToDoc = foldr (\(fname,val) flagAssDoc ->
                             (if not val then Disp.char '-'
                                         else Disp.char '+')
                             <<>> Disp.text (unFlagName fname)
                             Disp.<+> flagAssDoc)
                           Disp.empty

instance Parsec WorldPkgInfo where
  parsec = do
      dep <- parsec
      P.spaces
      flagAss <- P.option mempty parseFlagAssignment
      return $ WorldPkgInfo dep flagAss
    where
      parseFlagAssignment :: CabalParsing m => m FlagAssignment
      parseFlagAssignment = do
          _ <- P.string "--flags="
          inDoubleQuotes parsecFlagAssignmentNonEmpty
        where
          inDoubleQuotes = P.between (P.char '"') (P.char '"')
