{-# LANGUAGE CPP, DeriveDataTypeable, FlexibleContexts, FlexibleInstances, FunctionalDependencies, MultiParamTypeClasses, RecordWildCards, ScopedTypeVariables, TemplateHaskell #-}
{-# OPTIONS_GHC -Wall #-}
-- | Access to things that Debian policy says should be in a valid
-- control file.  The pure functions will not throw ControlFileError
-- if they are operating on a DebianControl value returned by
-- validateDebianControl.  However, they might if they are created
-- using unsafeDebianControl.
module Debian.Control.Policy
    ( -- * Validated debian control file type
      DebianControl(unDebianControl)
    , validateDebianControl
    , unsafeDebianControl
    , parseDebianControlFromFile
    , parseDebianControl
    , ControlFileError(..)
    -- * Class of things that contain one DebianControl value
    , HasDebianControl(debianControl)
    -- * Pure functions that operate on validated control files
    , debianSourceParagraph
    , debianBinaryParagraphs
    , debianPackageParagraphs
    , debianPackageNames
    , debianSourcePackageName
    , debianBinaryPackageNames
    , debianRelations
    , debianBuildDeps
    , debianBuildDepsIndep
    ) where

import Control.Exception (Exception, throw)
import Control.Monad.Catch (MonadCatch, try)
import Data.List (intercalate)
import Data.Text (Text)
import Data.Typeable (Typeable)
import Data.ListLike (toList)
import Debian.Control.Common (Control'(..), Paragraph'(..), Field'(..), fieldValue, ControlFunctions(parseControlFromFile, parseControl))
import Debian.Control.Text ()
import Debian.Loc (__LOC__)
import Debian.Pretty (prettyShow)
import Debian.Relation (SrcPkgName(..), BinPkgName(..), Relations, parseRelations)
import Debian.Relation.Text ()
import Language.Haskell.TH (Loc(..))
import Prelude hiding (ioError)
-- import qualified Debug.ShowPlease as Please
import Text.Parsec.Error (ParseError)

-- | Opaque (constructor not exported) type to hold a validated Debian
-- Control File
data DebianControl = DebianControl {unDebianControl :: Control' Text}

instance Show DebianControl where
    show c = "(parseDebianControl \"\" " ++ show (prettyShow (unDebianControl c)) ++ ")"

-- | Validate and return a control file in an opaque wrapper.  May
-- throw a ControlFileError.  Currently we only verify that it has a
-- Source field in the first paragraph and one or more subsequent
-- paragraphs each with a Package field, and no syntax errors in the
-- build dependencies (though they may be absent.)
validateDebianControl :: MonadCatch m => Control' Text -> m (Either ControlFileError DebianControl)
validateDebianControl ctl =
    try (do _ <- return $ debianPackageNames (DebianControl ctl)
            _ <- return $ debianBuildDeps (DebianControl ctl)
            _ <- return $ debianBuildDepsIndep (DebianControl ctl)
            return ()) >>=
    return . either Left (\ _ -> Right $ DebianControl ctl)

unsafeDebianControl :: Control' Text -> DebianControl
unsafeDebianControl = DebianControl

parseDebianControl :: MonadCatch m => String -> Text -> m (Either ControlFileError DebianControl)
parseDebianControl sourceName s = either (return . Left . ParseControlError [$__LOC__]) validateDebianControl (parseControl sourceName s)

parseDebianControlFromFile :: FilePath -> IO (Either ControlFileError DebianControl)
parseDebianControlFromFile controlPath =
  try (parseControlFromFile controlPath) >>=
  either (return . Left . IOError [$__LOC__])
         (either (return . Left . ParseControlError [$__LOC__]) validateDebianControl)

-- | Class of things that contain a validated Debian control file.
class HasDebianControl a where
    debianControl :: a -> DebianControl

instance HasDebianControl DebianControl where
    debianControl = id

class HasControl a where
    control :: a -> Control' Text

instance HasControl (Control' Text) where
    control = id

instance HasControl DebianControl where
    control = unDebianControl

-- | Errors that control files might throw, with source file name and
-- line number generated by template haskell.
data ControlFileError
    = NoParagraphs        {locs :: [Loc]}
    | NoBinaryParagraphs  {locs :: [Loc]}
    | MissingField        {locs :: [Loc], field :: String}
    | ParseRelationsError {locs :: [Loc], parseError :: ParseError}
    | ParseControlError   {locs :: [Loc], parseError :: ParseError}
    | IOError             {locs :: [Loc], ioError :: IOError}
    deriving Typeable

instance Show ControlFileError where
    show (NoParagraphs {..}) = intercalate ", " (map showLoc locs) ++ ": NoParagraphs"
    show (NoBinaryParagraphs {..}) = intercalate ", " (map showLoc locs) ++ ": NoBinaryParagraphs"
    show (MissingField {..}) = intercalate ", " (map showLoc locs) ++ ": MissingField " ++ show field
    show (ParseRelationsError {..}) = intercalate ", " (map showLoc locs) ++ ": ParseRelationsError " ++ show parseError
    show (ParseControlError {..}) = intercalate ", " (map showLoc locs) ++ ": ParseControlError " ++ show parseError
    show (IOError {..}) = intercalate ", " (map showLoc locs) ++ ": IOError " ++ show ioError

showLoc :: Loc -> String
showLoc x = show (loc_filename x) ++ "(line " ++ show (fst (loc_start x)) ++ ", column " ++ show (snd (loc_start x)) ++ ")"

-- instance Please.Show ControlFileError where
--     show (IOError e) = "(IOError " ++ Please.show e ++ ")"
--     show (ParseRelationsError e) = "(ParseRelationsError " ++ Please.show e ++ ")"
--     show (ParseControlError e) = "(ParseControlError " ++ Please.show e ++ ")"
--     show x = show x

instance Exception ControlFileError

instance Eq ControlFileError where
    _ == _ = False

debianPackageParagraphs :: HasDebianControl a => a -> (Paragraph' Text, [Paragraph' Text])
debianPackageParagraphs ctl =
    case removeCommentParagraphs ctl of
      DebianControl (Control [_]) -> throw $ NoBinaryParagraphs [$__LOC__]
      DebianControl (Control []) -> throw $ NoParagraphs [$__LOC__]
      DebianControl (Control (sourceParagraph : binParagraphs)) -> (sourceParagraph, binParagraphs)

-- | Comment paragraphs are rare, but they happen.
removeCommentParagraphs :: HasDebianControl a => a -> DebianControl
removeCommentParagraphs c =
    DebianControl (Control (filter (not . isCommentParagraph) (unControl (unDebianControl (debianControl c)))))
    where
      isCommentParagraph (Paragraph fields) = all isCommentField fields
      isCommentField (Comment _) = True
      isCommentField _ = False

debianSourceParagraph :: HasDebianControl a => a -> Paragraph' Text
debianSourceParagraph = fst . debianPackageParagraphs

debianBinaryParagraphs :: HasDebianControl a => a -> [Paragraph' Text]
debianBinaryParagraphs = snd . debianPackageParagraphs

debianPackageNames :: HasDebianControl a => a -> (SrcPkgName, [BinPkgName])
debianPackageNames c =
  let (srcParagraph, binParagraphs) = debianPackageParagraphs c in
  (mapFieldValue (SrcPkgName . toList) "Source" srcParagraph, map (mapFieldValue (BinPkgName . toList) "Package") binParagraphs)

debianSourcePackageName :: HasDebianControl a => a -> SrcPkgName
debianSourcePackageName = fst . debianPackageNames

debianBinaryPackageNames :: HasDebianControl a => a -> [BinPkgName]
debianBinaryPackageNames = snd . debianPackageNames

debianBuildDepsIndep :: HasDebianControl a => a -> Maybe Relations
debianBuildDepsIndep ctl = either throw id $ debianRelations "Build-Depends-Indep" (debianControl ctl)

debianBuildDeps :: HasDebianControl a => a -> Maybe Relations
debianBuildDeps ctl = either throw id $ debianRelations "Build-Depends" (debianControl ctl)

-- | Version of fieldValue that may throw a ControlFileError.  We only
-- use this internally on fields that we already validated.
fieldValue' :: ControlFunctions text => String -> Paragraph' text -> text
fieldValue' fieldName paragraph = maybe (throw $ MissingField [$__LOC__] fieldName) id $ fieldValue fieldName paragraph

-- | This could access fields we haven't validated, so
-- it can return an error.  Additionally, the field might
-- be absent, in which case it returns Nothing.
debianRelations :: HasDebianControl a => String -> a -> Either ControlFileError (Maybe Relations)
debianRelations fieldName ctl = maybe (Right Nothing) (either (Left . ParseRelationsError [$__LOC__]) (Right . Just) . parseRelations) $ fieldValue fieldName (debianSourceParagraph ctl)

-- | Apply a function to the text from a named field in a control file paragraph.
mapFieldValue :: (Text -> a) -> String -> Paragraph' Text -> a
mapFieldValue f fieldName paragraph = f $ fieldValue' fieldName paragraph