{-# LANGUAGE CPP                       #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies              #-}
{-# LANGUAGE RecordWildCards           #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE MultiParamTypeClasses     #-}
{-# LANGUAGE ScopedTypeVariables       #-}
{-# LANGUAGE ConstraintKinds           #-}

-- | This module exposes the low-level internal details of
-- cryptographic hashes. Do not import this module unless you want to
-- implement a new hash or give a new implementation of an existing
-- hash.
module Raaz.Hash.Internal
       ( -- * Cryptographic hashes and their implementations.
         -- $hashdoc$
         Hash(..)
       , hash, hashFile, hashSource
         -- ** Computing hashes using non-standard implementations.
       , hash', hashFile', hashSource'
         -- * Hash implementations.
       , HashI(..), SomeHashI(..), HashM
         -- ** Implementation of truncated hashes.
       , truncatedI
         -- * Memory used by most hashes.
       , HashMemory(..), extractLength, updateLength
       -- * Some low level functions.
       , completeHashing
       ) where


#if !MIN_VERSION_base(4,8,0)
import           Control.Applicative
#endif

import qualified Data.ByteString      as B
import qualified Data.ByteString.Lazy as L
import           Data.Word
import           Foreign.Storable
import           System.IO
import           System.IO.Unsafe     (unsafePerformIO)

import Raaz.Core

-- $hashdoc$
--
-- Each cryptographic hash is a distinct type and are instances of a
-- the type class `Hash`. The standard idiom that we follow for hash
-- implementations are the following:
--
-- [`HashI`:] This type captures implementations of a the hash. This
-- type is parameterised over the memory element used by the
-- implementation.
--
-- [`SomeHashI`:] This type is the existentially quantified version of
-- `HashI` over its memory element. Thus it exposes only the interface
-- and not the internals of the implementation. The `Implementation`
-- associated type of a hash is the type `SomeHashI`
--
-- To support a new hash, a developer needs to:
--
-- 1. Define a new type which captures the result of hashing. This
--    type should be an instance of the class `Hash`.
--
-- 2. Define an implementation, i.e. a value of the type `SomeHashI`.
--
-- 3. Define a recommended implementation, i.e. an instance of the
--    type class `Raaz.Core.Primitives.Recommendation`

-------------------- Hash Implementations --------------------------

-- | The Hash implementation. Implementations should ensure the
-- following.
--
-- 1. The action @compress impl ptr blks@ should only read till the
-- @blks@ offset starting at ptr and never write any data.
--
-- 2. The action @padFinal impl ptr byts@ should touch at most
-- @⌈byts/blocksize⌉ + padBlocks@ blocks starting at ptr. It should
-- not write anything till the @byts@ offset but may write stuff
-- beyond that.
--
-- An easy to remember this rule is to remember that computing hash of
-- a payload should not modify the payload.
--
data HashI h m = HashI
  { hashIName           :: String
  , hashIDescription    :: String
  , compress       :: Pointer -> BLOCKS h  -> MT m ()
                      -- ^ compress the blocks,
  , compressFinal  :: Pointer -> BYTES Int -> MT m ()
                      -- ^ pad and process the final bytes,
  , compressStartAlignment :: Alignment
  }

instance BlockAlgorithm (HashI h m) where
  bufferStartAlignment = compressStartAlignment

-- | The constraints that a memory used by a hash implementation
-- should satisfy.
type HashM h m = (Initialisable m (), Extractable m h, Primitive h)

-- | Some implementation of a given hash. The existentially
-- quantification allows us freedom to choose the best memory type
-- suitable for each implementations.
data SomeHashI h = forall m . HashM h m =>
     SomeHashI (HashI h m)

instance Describable (HashI h m) where
  name        = hashIName
  description = hashIDescription


instance Describable (SomeHashI h) where
  name         (SomeHashI hI) = name hI
  description  (SomeHashI hI) = description hI

instance BlockAlgorithm (SomeHashI h) where
  bufferStartAlignment (SomeHashI imp) = bufferStartAlignment imp

-- | Certain hashes are essentially bit-truncated versions of other
-- hashes. For example, SHA224 is obtained from SHA256 by dropping the
-- last 32-bits. This combinator can be used build an implementation
-- of truncated hash from the implementation of its parent hash.
truncatedI :: (BLOCKS htrunc -> BLOCKS h)
           -> (mtrunc        -> m)
           -> HashI h m -> HashI htrunc mtrunc
truncatedI coerce unMtrunc (HashI{..})
  = HashI { hashIName        = hashIName
          , hashIDescription = hashIDescription
          , compress         = comp
          , compressFinal    = compF
          , compressStartAlignment = compressStartAlignment
          }
  where comp  ptr = onSubMemory unMtrunc . compress ptr . coerce
        compF ptr = onSubMemory unMtrunc . compressFinal ptr

---------------------- The Hash class ---------------------------------

-- | Type class capturing a cryptographic hash.
class ( Primitive h
      , EndianStore h
      , Encodable h
      , Eq h
      , Implementation h ~ SomeHashI h
      ) => Hash h where
  -- | Cryptographic hashes can be computed for messages that are not
  -- a multiple of the block size. This combinator computes the
  -- maximum size of padding that can be attached to a message.
  additionalPadBlocks :: h -> BLOCKS h

---------------------- Helper combinators --------------------------

-- | Compute the hash of a pure byte source like, `B.ByteString`.
hash :: ( Hash h, Recommendation h, PureByteSource src )
     => src  -- ^ Message
     -> h
hash = unsafePerformIO . hashSource
{-# INLINEABLE hash #-}
{-# SPECIALIZE hash :: (Hash h, Recommendation h) => B.ByteString -> h #-}
{-# SPECIALIZE hash :: (Hash h, Recommendation h) => L.ByteString -> h #-}

-- | Compute the hash of file.
hashFile :: ( Hash h, Recommendation h)
         => FilePath  -- ^ File to be hashed
         -> IO h
hashFile fileName = withBinaryFile fileName ReadMode hashSource
{-# INLINEABLE hashFile #-}


-- | Compute the hash of a generic byte source.
hashSource :: ( Hash h, Recommendation h, ByteSource src )
           => src  -- ^ Message
           -> IO h
hashSource = go undefined
  where go :: (Hash h, Recommendation h, ByteSource src) => h -> src -> IO h
        go h = hashSource' $ recommended h

{-# INLINEABLE hashSource #-}
{-# SPECIALIZE hashSource :: (Hash h, Recommendation h) => Handle -> IO h #-}


-- | Similar to `hash` but the user can specify the implementation to
-- use.
hash' :: ( PureByteSource src
         , Hash h
         )
      => Implementation h -- ^ Implementation
      -> src              -- ^ the message as a byte source.
      -> h
hash' imp = unsafePerformIO . hashSource' imp
{-# INLINEABLE hash' #-}

-- TODO: For bytestrings (strict and lazy) we can do better. We can
-- avoid copying as the bytestring exposes the underlying
-- pointer. However, there is a huge cost if the underlying pointer
-- does not start at the machine alignment boundary. The idea
-- therefore is to process strict bytestring is multiples of blocks
-- directly if the starting pointer is aligned.
--
-- More details in the bug report #256.
--
-- https://github.com/raaz-crypto/raaz/issues/256
--

-- | Similar to hashFile' but the user can specify the implementation
-- to use.
hashFile' :: Hash h
          => Implementation h  -- ^ Implementation
          -> FilePath          -- ^ File to be hashed
          -> IO h
hashFile' imp fileName = withBinaryFile fileName ReadMode $ hashSource' imp
{-# INLINEABLE hashFile' #-}


-- | Similar to @hashSource@ but the user can specify the
-- implementation to use.
hashSource' :: (Hash h, ByteSource src)
            => Implementation h
            -> src
            -> IO h
hashSource' (SomeHashI impl) src =
  insecurely $ initialise () >> completeHashing impl src


-- | Gives a memory action that completes the hashing procedure with
-- the rest of the source. Useful to compute the hash of a source with
-- some prefix (like in the HMAC procedure).
completeHashing :: (Hash h, ByteSource src, HashM h m)
                => HashI h m
                -> src
                -> MT m h
completeHashing imp@(HashI{..}) src =
  allocate $ \ ptr -> let
    comp                = compress ptr bufSize
    finish bytes        = compressFinal ptr bytes >> extract
    in processChunks comp finish src bufSize ptr
  where bufSize             = atLeast l1Cache + 1
        totalSize           = bufSize + additionalPadBlocks undefined
        allocate            = liftPointerAction $ allocBufferFor (SomeHashI imp) totalSize

----------------------- Hash memory ----------------------------------

-- | Computing cryptographic hashes usually involves chunking the
-- message into blocks and compressing one block at a time. Usually
-- this compression makes use of the hash of the previous block and
-- the length of the message seen so far to compressing the current
-- block. Most implementations therefore need to keep track of only
-- hash and the length of the message seen so. This memory can be used
-- in such situations.
data HashMemory h =
  HashMemory
  { hashCell          :: MemoryCell h
                         -- ^ Cell to store the hash
  , messageLengthCell :: MemoryCell (BITS Word64)
                         -- ^ Cell to store the length
  }

instance Storable h => Memory (HashMemory h) where

  memoryAlloc     = HashMemory <$> memoryAlloc <*> memoryAlloc

  unsafeToPointer = unsafeToPointer . hashCell

instance Storable h => Initialisable (HashMemory h) h where
  initialise h = do
    onSubMemory hashCell          $ initialise h
    onSubMemory messageLengthCell $ initialise (0 :: BITS Word64)

instance Storable h => Extractable (HashMemory h) h where
  extract = onSubMemory hashCell extract

-- | Extract the length of the message hashed so far.
extractLength :: MT (HashMemory h) (BITS Word64)
extractLength = onSubMemory messageLengthCell extract
{-# INLINE extractLength #-}


-- | Update the message length by a given amount.
updateLength :: LengthUnit u => u -> MT (HashMemory h) ()
{-# INLINE updateLength #-}
updateLength u = onSubMemory messageLengthCell $ modify (nBits +)
  where nBits :: BITS Word64
        nBits =  inBits u