{-# LANGUAGE GADTs              #-}
{-# LANGUAGE RankNTypes         #-}
{-# LANGUAGE RecordWildCards    #-}
{-# LANGUAGE ConstraintKinds    #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE CPP                #-}

module Command.Checksum ( checksum )  where

import Control.Monad
import Data.Char                ( toLower )
import Data.Monoid
import Data.String
import Options.Applicative
import Raaz
import System.Exit
import System.IO


------------------------- Supported checksum algorithm -------------------------------

-- You can add new hash algorithms by adding a constructor for the
-- GADT, Algorithm h. You should also enable the command by adding a mkCmd line in the checksum


checksum :: Parser (IO ())
#if MIN_VERSION_optparse_applicative(0,13,0)
checksum = subparser $ commandGroup "Checksum Computation"
#else
checksum = subparser $ mempty
#endif
           <> mkCmd Blake2b
           <> mkCmd Blake2s
           <> mkCmd Sha512
           <> mkCmd Sha256
           -- Add the mkCmd here and also add an appropriate
           -- constructor for the Algorithm type. If you add a hash
           -- Foo then the associated command will be foosum.

-- | The hash algorithm to use.
data Algorithm h  where
  Blake2b  :: Algorithm BLAKE2b
  Blake2s  :: Algorithm BLAKE2s
  Sha256   :: Algorithm SHA256
  Sha512   :: Algorithm SHA512
  -- Add new hash algorithm here.


deriving instance Show (Algorithm h)
           -- Add a mkCmd here for the new hash algorithm

mkCmd :: Digest h => Algorithm h -> Mod CommandFields (IO ())
mkCmd algo = command cmd inf
  where inf = info (helper <*> opts) $ fullDesc <> hdr <> desc
        opts     = run algo <$> optParse
        algoname = map toLower $ show algo
        cmd      = algoname ++ "sum"
        hdr      = header $ unwords [ "raaz", cmd, "- File checksums using", algoname]
        desc     = progDesc $ "Compute or verify the file checksums using " ++ algoname


------------------------ Types and constriants -------------------------------------


-- | This constraint class consolidates the constraints on the checksum algorithms.
type Digest h = (Hash h, Recommendation h, Show h, IsString h)



data Option = Option { checkDigest   :: Bool -- ^ true if we need verification.
                     , reportOkey    :: Bool -- ^ whether to print success
                     , reportFailure :: Bool -- ^ whether to print failure
                     , inputFiles    :: [FilePath]
                     }

--------------------- The checksum type ----------------------------------------

data Checksum h  = Checksum {filePath :: FilePath, fileDigest  :: h}


instance Show h => Show (Checksum h) where
  show (Checksum{..}) = show fileDigest ++ "  " ++ filePath

parse :: Digest h => Algorithm h -> String -> Checksum h
parse algo inp = Checksum { filePath   = drop 2 rest
                          , fileDigest = parseDigest algo digest
                          }
  where parseDigest    :: Digest h => Algorithm h -> String -> h
        parseDigest _  = fromString
        (digest, rest) = break (==' ') inp -- break at the space.

-- | Parse the lines into checksum.
parseMany :: Digest h => Algorithm h -> String -> [Checksum h]
parseMany algo = map (parse algo) . lines

---------- The main combinators that does the actual work -------------

-- | The workhorse for this command.
run :: Digest h => Algorithm h -> Option -> IO ()
run algo opt@(Option{..})
  | checkDigest = runVerify algo opt
  | otherwise   = runCompute algo inputFiles


--------------------------- Compute mode ---------------------------------------

runCompute :: Digest h
           => Algorithm h    -- Algorithm to use
           -> [FilePath]     -- files for which checksums need to be computed.
           -> IO ()
runCompute algo files
  | null files = computeStdin algo >>= print  -- No files means compute it for stdin.
  | otherwise  = mapM_ printToken files       -- Print the token for each file.
  where printToken = compute algo >=> print



-- | Compute the checksum of a file.
compute :: Digest h
        => Algorithm h  -- ^ The hashing algorithm to use.
        -> FilePath     -- ^ The file to compute the token for.
        -> IO (Checksum h)
compute _ fp = Checksum fp <$> hashFile fp


-- | Compute the checksum of standard input
computeStdin :: Digest h => Algorithm h -> IO (Checksum h)
computeStdin _ = Checksum "-" <$> hashSource stdin

----------------------------------- Verify Mode ---------------------------------------

runVerify :: Digest h => Algorithm h -> Option -> IO ()
runVerify algo opt@(Option{..}) = do
  nFails <- if null inputFiles then getContents >>= verifyLines
            else sum <$> mapM verifyFile inputFiles

  when reportFailure $ putStrLn $ show nFails ++ " failures."
  when (nFails > 0)  exitFailure
  where
    verifyLines     = verifyList opt . parseMany algo
    verifyFile fp   = withFile fp ReadMode $ hGetContents  >=> verifyLines


verify :: Digest h
       => Option
       -> Checksum  h
       -> IO Bool
verify (Option{..}) (Checksum{..}) = do
  digest <- hashFile filePath
  let result = digest == fileDigest
      okey   = when reportOkey    $ putStrLn $ filePath ++ ": OK"
      failed = when reportFailure $ putStrLn $ filePath ++ ": FAILED"
    in do if result then okey else failed
          return result

verifyList :: Digest h
           => Option
           -> [Checksum h]
           -> IO Int
verifyList opt   = fmap countFailures . mapM (verify opt)
  where countFailures = Prelude.length . filter not


----------------------------- Option parsers ----------------------------------

optParse   :: Parser Option
optParse  =  verbosityOpt <*> (Option <$> checkOpt <*> pure True <*> pure True <*> files)

  where checkOpt = switch
                   $  long "check"
                   <> short 'c'
                   <> help "Verify the input checksums instead of compute"
        files = many $ argument str $ metavar "FILE.."


verbosityOpt :: Parser (Option -> Option)
verbosityOpt = (.) <$> statusOpt <*> quietOpt
  where statusOnly opt = opt { reportOkey = False, reportFailure = False }
        quietMode  opt = opt { reportOkey = False }

        statusOpt = flag id statusOnly
                    $  short 's'
                    <> long  "status"
                    <> help "Do not print anything, only return the verification status"

        quietOpt  = flag id quietMode
                    $  short 'q'
                    <> long "quite"
                    <> help "Do not print OK, print only failures"
