{-# LANGUAGE CPP #-}
module Codec.Picture.Png.Internal.Type( PngIHdr( .. )
                             , PngFilter( .. )
                             , PngInterlaceMethod( .. )
                             , PngPalette
                             , PngImageType( .. )
                             , PngPhysicalDimension( .. )
                             , PngGamma( .. )
                             , PngUnit( .. )
                             , APngAnimationControl( .. )
                             , APngFrameDisposal( .. )
                             , APngBlendOp( .. )
                             , APngFrameControl( .. )
                             , parsePalette
                             , pngComputeCrc
                             , pLTESignature
                             , iDATSignature
                             , iENDSignature
                             , tRNSSignature
                             , tEXtSignature
                             , zTXtSignature
                             , gammaSignature
                             , pHYsSignature
                             , animationControlSignature
                             
                             , ChunkSignature
                             , PngRawImage( .. )
                             , PngChunk( .. )
                             , PngRawChunk( .. )
                             , PngLowLevel( .. )
                             , chunksWithSig
                             , mkRawChunk
                             ) where
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative( (<$>), (<*>), pure )
#endif
import Control.Monad( when, replicateM )
import Data.Bits( xor, (.&.), unsafeShiftR )
import Data.Binary( Binary(..), Get, get )
import Data.Binary.Get( getWord8
                      , getWord32be
                      , getLazyByteString
                      )
import Data.Binary.Put( runPut
                      , putWord8
                      , putWord32be
                      , putLazyByteString
                      )
import Data.Vector.Unboxed( Vector, fromListN, (!) )
import qualified Data.Vector.Storable as V
import Data.List( foldl' )
import Data.Word( Word32, Word16, Word8 )
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as LS
import Codec.Picture.Types
import Codec.Picture.InternalHelper
type ChunkSignature = L.ByteString
data PngIHdr = PngIHdr
    { width             :: !Word32       
    , height            :: !Word32       
    , bitDepth          :: !Word8        
    , colourType        :: !PngImageType 
    , compressionMethod :: !Word8        
    , filterMethod      :: !Word8        
    , interlaceMethod   :: !PngInterlaceMethod   
    }
    deriving Show
data PngUnit
    = PngUnitUnknown 
    | PngUnitMeter   
instance Binary PngUnit where
  get = do
    v <- getWord8
    pure $ case v of
      0 -> PngUnitUnknown
      1 -> PngUnitMeter
      _ -> PngUnitUnknown
  put v = case v of
    PngUnitUnknown -> putWord8 0
    PngUnitMeter -> putWord8 1
data PngPhysicalDimension = PngPhysicalDimension
    { pngDpiX     :: !Word32
    , pngDpiY     :: !Word32
    , pngUnit     :: !PngUnit
    }
instance Binary PngPhysicalDimension where
  get = PngPhysicalDimension <$> getWord32be <*> getWord32be <*> get
  put (PngPhysicalDimension dpx dpy unit) =
    putWord32be dpx >> putWord32be dpy >> put unit
newtype PngGamma = PngGamma { getPngGamma :: Double }
instance Binary PngGamma where
  get = PngGamma . (/ 100000) . fromIntegral <$> getWord32be
  put = putWord32be . ceiling . (100000 *) . getPngGamma
data APngAnimationControl = APngAnimationControl
    { animationFrameCount :: !Word32
    , animationPlayCount  :: !Word32
    }
    deriving Show
data APngFrameDisposal
      
      
      
    = APngDisposeNone
      
      
      
    | APngDisposeBackground
      
      
      
    | APngDisposePrevious
    deriving Show
data APngBlendOp
      
    = APngBlendSource
      
    | APngBlendOver
    deriving Show
data APngFrameControl = APngFrameControl
    { frameSequenceNum      :: !Word32 
    , frameWidth            :: !Word32 
    , frameHeight           :: !Word32 
    , frameLeft             :: !Word32 
    , frameTop              :: !Word32 
    , frameDelayNumerator   :: !Word16
    , frameDelayDenuminator :: !Word16
    , frameDisposal         :: !APngFrameDisposal
    , frameBlending         :: !APngBlendOp
    }
    deriving Show
data PngImageType =
      PngGreyscale
    | PngTrueColour
    | PngIndexedColor
    | PngGreyscaleWithAlpha
    | PngTrueColourWithAlpha
    deriving Show
data PngRawImage = PngRawImage
    { header       :: PngIHdr
    , chunks       :: [PngRawChunk]
    }
type PngPalette = Palette' PixelRGB8
parsePalette :: PngRawChunk -> Either String PngPalette
parsePalette plte
 | chunkLength plte `mod` 3 /= 0 = Left "Invalid palette size"
 | otherwise = Palette' pixelCount . V.fromListN (3 * pixelCount) <$> pixels
    where pixelUnpacker = replicateM (fromIntegral pixelCount * 3) get
          pixelCount = fromIntegral $ chunkLength plte `div` 3
          pixels = runGet pixelUnpacker (chunkData plte)
data PngRawChunk = PngRawChunk
    { chunkLength :: Word32
    , chunkType   :: ChunkSignature
    , chunkCRC    :: Word32
    , chunkData   :: L.ByteString
    }
mkRawChunk :: ChunkSignature -> L.ByteString -> PngRawChunk
mkRawChunk sig binaryData = PngRawChunk
  { chunkLength = fromIntegral $ L.length binaryData
  , chunkType   = sig
  , chunkCRC    = pngComputeCrc [sig, binaryData]
  , chunkData   = binaryData
  }
data PngChunk = PngChunk
    { pngChunkData        :: L.ByteString  
    , pngChunkSignature   :: ChunkSignature 
    }
data PngLowLevel a = PngLowLevel
    { pngImage  :: Image a      
    , pngChunks :: [PngChunk]   
    }
data PngFilter =
    
      FilterNone
    
    | FilterSub
    
    | FilterUp
    
    
    | FilterAverage
    
    
    | FilterPaeth
    deriving (Enum, Show)
data PngInterlaceMethod =
      
      
      PngNoInterlace
      
    | PngInterlaceAdam7
    deriving (Enum, Show)
instance Binary PngFilter where
    put = putWord8 . toEnum . fromEnum
    get = getWord8 >>= \w -> case w of
        0 -> return FilterNone
        1 -> return FilterSub
        2 -> return FilterUp
        3 -> return FilterAverage
        4 -> return FilterPaeth
        _ -> fail "Invalid scanline filter"
instance Binary PngRawImage where
    put img = do
        putLazyByteString pngSignature
        put $ header img
        mapM_ put $ chunks img
    get = parseRawPngImage
instance Binary PngRawChunk where
    put chunk = do
        putWord32be $ chunkLength chunk
        putLazyByteString $ chunkType chunk
        when (chunkLength chunk /= 0)
             (putLazyByteString $ chunkData chunk)
        putWord32be $ chunkCRC chunk
    get = do
        size <- getWord32be
        chunkSig <- getLazyByteString (fromIntegral $ L.length iHDRSignature)
        imgData <- if size == 0
            then return L.empty
            else getLazyByteString (fromIntegral size)
        crc <- getWord32be
        let computedCrc = pngComputeCrc [chunkSig, imgData]
        when (computedCrc `xor` crc /= 0)
             (fail $ "Invalid CRC : " ++ show computedCrc ++ ", "
                                      ++ show crc)
        return PngRawChunk {
            chunkLength = size,
            chunkData = imgData,
            chunkCRC = crc,
            chunkType = chunkSig
        }
instance Binary PngIHdr where
    put hdr = do
        putWord32be 13
        let inner = runPut $ do
                putLazyByteString iHDRSignature
                putWord32be $ width hdr
                putWord32be $ height hdr
                putWord8    $ bitDepth hdr
                put $ colourType hdr
                put $ compressionMethod hdr
                put $ filterMethod hdr
                put $ interlaceMethod hdr
            crc = pngComputeCrc [inner]
        putLazyByteString inner
        putWord32be crc
    get = do
        _size <- getWord32be
        ihdrSig <- getLazyByteString (L.length iHDRSignature)
        when (ihdrSig /= iHDRSignature)
             (fail "Invalid PNG file, wrong ihdr")
        w <- getWord32be
        h <- getWord32be
        depth <- get
        colorType <- get
        compression <- get
        filtermethod <- get
        interlace <- get
        _crc <- getWord32be
        return PngIHdr {
            width = w,
            height = h,
            bitDepth = depth,
            colourType = colorType,
            compressionMethod = compression,
            filterMethod = filtermethod,
            interlaceMethod = interlace
        }
parseChunks :: Get [PngRawChunk]
parseChunks = do
    chunk <- get
    if chunkType chunk == iENDSignature
       then return [chunk]
       else (chunk:) <$> parseChunks
instance Binary PngInterlaceMethod where
    get = getWord8 >>= \w -> case w of
        0 -> return PngNoInterlace
        1 -> return PngInterlaceAdam7
        _ -> fail "Invalid interlace method"
    put PngNoInterlace    = putWord8 0
    put PngInterlaceAdam7 = putWord8 1
parseRawPngImage :: Get PngRawImage
parseRawPngImage = do
    sig <- getLazyByteString (L.length pngSignature)
    when (sig /= pngSignature)
         (fail "Invalid PNG file, signature broken")
    ihdr <- get
    chunkList <- parseChunks
    return PngRawImage { header = ihdr, chunks = chunkList }
pngSignature :: ChunkSignature
pngSignature = L.pack [137, 80, 78, 71, 13, 10, 26, 10]
signature :: String -> ChunkSignature
signature = LS.pack
iHDRSignature :: ChunkSignature
iHDRSignature = signature "IHDR"
pLTESignature :: ChunkSignature
pLTESignature = signature "PLTE"
iDATSignature :: ChunkSignature
iDATSignature = signature "IDAT"
iENDSignature :: ChunkSignature
iENDSignature = signature "IEND"
tRNSSignature :: ChunkSignature
tRNSSignature = signature "tRNS"
gammaSignature :: ChunkSignature
gammaSignature = signature "gAMA"
pHYsSignature :: ChunkSignature
pHYsSignature = signature "pHYs"
tEXtSignature :: ChunkSignature
tEXtSignature = signature "tEXt"
zTXtSignature :: ChunkSignature
zTXtSignature = signature "zTXt"
animationControlSignature :: ChunkSignature
animationControlSignature = signature "acTL"
instance Binary PngImageType where
    put PngGreyscale = putWord8 0
    put PngTrueColour = putWord8 2
    put PngIndexedColor = putWord8 3
    put PngGreyscaleWithAlpha = putWord8 4
    put PngTrueColourWithAlpha = putWord8 6
    get = get >>= imageTypeOfCode
imageTypeOfCode :: Word8 -> Get PngImageType
imageTypeOfCode 0 = return PngGreyscale
imageTypeOfCode 2 = return PngTrueColour
imageTypeOfCode 3 = return PngIndexedColor
imageTypeOfCode 4 = return PngGreyscaleWithAlpha
imageTypeOfCode 6 = return PngTrueColourWithAlpha
imageTypeOfCode _ = fail "Invalid png color code"
pngCrcTable :: Vector Word32
pngCrcTable = fromListN 256 [ foldl' updateCrcConstant c [zero .. 7] | c <- [0 .. 255] ]
    where zero = 0 :: Int 
          updateCrcConstant c _ | c .&. 1 /= 0 = magicConstant `xor` (c `unsafeShiftR` 1)
                                | otherwise = c `unsafeShiftR` 1
          magicConstant = 0xedb88320 :: Word32
pngComputeCrc :: [L.ByteString] -> Word32
pngComputeCrc = (0xFFFFFFFF `xor`) . L.foldl' updateCrc 0xFFFFFFFF . L.concat
    where updateCrc crc val =
              let u32Val = fromIntegral val
                  lutVal = pngCrcTable ! (fromIntegral ((crc `xor` u32Val) .&. 0xFF))
              in lutVal `xor` (crc `unsafeShiftR` 8)
chunksWithSig :: PngRawImage -> ChunkSignature -> [LS.ByteString]
chunksWithSig rawImg sig =
  [chunkData chunk | chunk <- chunks rawImg, chunkType chunk == sig]