{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- |
Module      :  ELynx.Tools.Reproduction
Description :  Functions to ease reproduction of analyses
Copyright   :  (c) Dominik Schrempf 2019
License     :  GPL-3

Maintainer  :  dominik.schrempf@gmail.com
Stability   :  unstable
Portability :  portable

Creation date: Tue Nov 19 15:07:09 2019.

XXX: This module is actively developed. It is not yet used by slynx nor tlynx.

-}

module ELynx.Tools.Reproduction
  ( Reproducible (..)
  , Reproduction (..)
  , readR
  , writeR
  ) where

import           Control.Monad         (zipWithM)
import           Crypto.Hash.SHA256    (hash)
import           Data.Aeson            (FromJSON, ToJSON,
                                        eitherDecodeFileStrict', encodeFile)
import           Data.Bifunctor        (first)
import qualified Data.ByteString.Char8 as B
import           Data.Either           (either)
import           GHC.Generics          (Generic)
import           Options.Applicative   (Parser, briefDesc, defaultPrefs,
                                        execParserPure, getParseResult, info)
import           System.Environment    (getArgs, getProgName)

-- | Reproducible commands have a set of input files that have to be checked for
-- consistency.
class Reproducible a where
  inFiles :: a -> [FilePath]
  parser  :: a -> Parser a

-- | Necessary information for a reproducible run. Notably, the input files are
-- checked for consistency!
data Reproduction a = Reproduction
  { progName  :: String         -- ^ Program name.
  , args      :: [String]       -- ^ Command line arguments without program name.
  , filePaths :: [FilePath]     -- ^ File paths of in files.
  , checkSums :: [String]       -- ^ SHA256 sums of in files.
  , cmd       :: a              -- ^ Command argument.
  } deriving (Generic)

instance ToJSON a => ToJSON (Reproduction a) where

instance FromJSON a => FromJSON (Reproduction a)

-- Does the command line fit the provided command?
checkArgs :: (Eq a, Show a, Reproducible a)
          => [String] -> a -> IO (Either String ())
checkArgs as c = do
  let p    = parser c
      pres = execParserPure defaultPrefs (info p briefDesc) as
  return $ case getParseResult pres of
    Nothing  ->
      Left $ unlines [ "Could not parse command line string:"
                     , concat as ]
    Just c'  ->
      if c' /= c
      then Left $ unlines [ "Command line string and command arguments do not fit:"
                          , concat as
                          , show c ]
      else Right ()

-- Does the file match the checksum?
checkFile :: FilePath -> B.ByteString -> IO (Either String ())
checkFile fp h = do
  h' <- hashFile fp
  return $ if h' == h
    then Right ()
    else Left $ unlines [ "SHA256 sum does not match for a file."
                        , fp ++ " has check sum " ++ B.unpack h'
                        , "Stored sum is " ++ B.unpack h ]

-- | Check if command line arguments and files check sums are matching.
checkReproduction :: (Eq a, Show a, Reproducible a)
                  => Reproduction a -> IO (Either String ())
checkReproduction (Reproduction _ as fs ss c) = do
  chA  <- checkArgs as c
  chFs <- zipWithM checkFile fs (map B.pack ss)
  let ch = sequence_ (chA : chFs)
  return $ first ("Failed validating the reproduction file.\n" ++) ch

-- | Read an ELynx reproduction file. Check consistency of arguments and input files.
readR :: forall a . (Eq a, Show a, Reproducible a, FromJSON a)
      => FilePath -> IO (Reproduction a)
readR fp = do
  res <- eitherDecodeFileStrict' fp :: IO (Either String (Reproduction a))
  case res of
    Left err -> do
      putStrLn "Failed reading the ELynx reproduction file."
      putStrLn "The following error was encountered."
      error err
    Right r  -> do
      ch <- checkReproduction r
      return $ either error (const r) ch

-- | Helper function.
hashFile :: FilePath -> IO B.ByteString
hashFile f = hash <$> B.readFile f

-- | Write an ELynx reproduction file. Check arguments.
writeR :: (Eq a, Show a, Reproducible a, ToJSON a) => FilePath -> a -> IO ()
writeR fp c = do
  p  <- getProgName
  as <- getArgs
  let fs = inFiles c
  cs <- mapM hashFile fs
  let
    cs' = map B.unpack cs
    r   = Reproduction p as fs cs' c
  -- XXX: Actually, it is only necessary to to checkArgs here. But let's just be safe.
  ch <- checkReproduction r
  either error (const $ encodeFile fp r) ch