{-|
Module      : Data.Fits
Description : Types for FITS Data Units
Copyright   : (c) Zac Slade, 2018
License     : BSD2
Maintainer  : krakrjak@gmail.com
Stability   : experimental

Definitions for the data types needed to parse an HDU in a FITS block.
-}

{-# LANGUAGE PartialTypeSignatures, DataKinds, ExistentialQuantification
  , ScopedTypeVariables, GADTs
  , OverloadedStrings, TypeOperators, TypeFamilies #-}
module Data.Fits
    ( -- * Data payload functions
      parsePix
    , pixsUnwrapI
    , pixsUnwrapD

      -- * Main data types
    , HeaderDataUnit(..)
    , HeaderData(..)
    , BitPixFormat(..)
    , Pix(..)

      -- ** Header Data Types
    , SimpleFormat(..)
    , Axis(..)
    , StringType(..)
    , StringValue(..)
    , NumberType(..)
    , NumberModifier(..)
    , NumberValue(..)

      -- * Utility
    , isBitPixInt
    , isBitPixFloat
    , bitPixToWordSize

      -- ** Constants
    , hduRecordLength
    , hduMaxRecords
    , hduBlockSize

    ) where

---- text
import qualified Data.Text as T
---- bytestring
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL

---- base
import Numeric.Natural ( Natural )

---- ghc
import GHC.TypeNats (KnownNat, Nat)

---- text
import Data.Text ( Text )

---- bytestring
import Data.ByteString ( ByteString )

---- default
import Data.Default( Default, def )

---- binaryi
import Data.Binary
import Data.Binary.Get

-- | A single record in the HDU is an eighty byte word.
{-@ type HDURecordLength = {v:Int | v = 80} @-}
{-@ hduRecordLength :: HDURecordLength @-}
hduRecordLength :: Int
hduRecordLength = 80

{-| The maximum amount of eighty byte records is thirty-six per the
    standard.
-}
{-@ type HDUMaxRecords = {v:Int | v = 36} @-}
{-@ hduMaxRecords :: HDUMaxRecords @-}
hduMaxRecords :: Int
hduMaxRecords = 36

{-| The size of an HDU block is fixed at thirty-six eighty byte words. In
    other words 2,880 bytes. These blocks are padded with zeros to this
    boundary.
-}
{-@ type HDUBlockSize = {v:Int | v = 2880} @-}
{-@ hduBlockSize :: HDUBlockSize @-}
hduBlockSize :: Int
hduBlockSize = hduRecordLength * hduMaxRecords

{-| There are many types of strings defined in the FITS documentation.
    Refered to as "character string(s)" in the documentation, they can be
    null, empty, undefined, or contain characters (printable ASCII only).
-}
data StringType = NullString      -- ^ The NULL character string (e.g. AUTHOR=)
                | EmptyString     -- ^ An empty string (e.g. AUTHOR="")
                | DataString      -- ^ Plain ASCII data

instance Show StringType where
    show NullString      = "Null String"
    show EmptyString     = "Empty quoted String"
    show DataString      = "String"

-- | A 'StringValue' is a type paired with a possible value.
data StringValue = StringValue
    { stringType :: StringType  -- ^ Which 'StringType' is this value?
    , stringValue :: Maybe Text -- ^ The payload of a character string
    }

{-| The default instance of 'StringValue' is an undefined string with no
    text.
-}
instance Default StringValue where
    def = StringValue NullString Nothing

instance Show StringValue where
        show (StringValue NullString _) = show NullString
        show (StringValue EmptyString _) = show EmptyString
        show (StringValue DataString Nothing) = "No good " ++ show DataString
        show (StringValue DataString (Just s)) = show DataString ++ T.unpack s

{-| The FITS standard allows for the encoding of unsigned integers, signed
    integers, real numbers, and complex numbers. They are always ASCII
    encoded. See 5.2 of the standard for more details.
-}
data NumberType =
      IntegerType -- ^ HDU ASCII encoded integer number
    | RealType    -- ^ HDU ASCII encoded real number
    | ComplexType -- ^ HDU ASCII encoded complex number

-- | Utility data type to help with the ASCII representation of numbers
data NumberModifier =
      Positive -- ^ HDU positive number value
    | Negative -- ^ HDU negative number value
    | Zero     -- ^ HDU numeric value is zero, could be positive or negative

{-| 'NumberValue' contains an encoded numeric record from a data field.
  This data type still needs to be converted into more useful Haskell data
  types.
-}
data NumberValue = NumberValue
    { numberType        :: NumberType
      -- ^ Key to decoding the structure
    , realModifier      :: NumberModifier
      -- ^ Encoding the sign of the real part
    , realPart          :: Text
      -- ^ All 'NumberType' have a real part (sign stripped)
    , imaginaryModifier :: Maybe NumberModifier
      -- ^ Encoding the sign of the imaginary part
    , imaginaryPart     :: Maybe Text
      -- ^ Only 'ComplexType' have an imaginary part (sign stripped)
    , exponentModifier  :: Maybe NumberModifier
      -- ^ 'Positive', 'Negative', or 'Zero'
    , exponent          :: Maybe Int
      -- ^ All 'NumberType' may have an exponent
    }

{-| The default instance for 'NumberValue' is a 32-bit integer of zero with
    no imaginary or exponent parts.
-}
instance Default NumberValue where
    def = NumberValue IntegerType Zero "0" Nothing Nothing Nothing Nothing

{-| The standard defines two possible values for the SIMPLE keyword, T and
    F. The T refers to a 'Conformant' format while F refers to
    a 'NonConformant' format. At this time only the 'Conformant', T, format
    is supported.
-}
data SimpleFormat = Conformant
                    -- ^ Value of SIMPLE=T in the header. /supported/
                  | NonConformant
                    -- ^ Value of SIMPLE=F in the header. /unsupported/

-- | 'Axis' represents a single NAXIS record.
data Axis = Axis
    { axisNumber       :: Int -- ^ The axis number under consideration
    , axisElementCount :: Int -- ^ The number of elements in this axis
    }

-- | The default instance for 'Axis' is NAXIS=0 with zero elements.
instance Default Axis where
    def = Axis 0 0

{-| The 'BitPixFormat' is the nitty gritty of how the 'Axis' data is layed
    out in the file. The standard recognizes six formats: unsigned 8 bit
    integer, two's complement binary integers at 16, 32, and 64 bits along
    with 32 and 64 bit IEEE floating point formats.
-}
data BitPixFormat =
      EightBitInt       -- ^ BITPIX = 8; unsigned binary integer of 8 bits
    | SixteenBitInt     -- ^ BITPIX = 16; two's complement binary integer of 16 bits
    | ThirtyTwoBitInt   -- ^ BITPIX = 32; two's complement binary integer of 32 bits
    | SixtyFourBitInt   -- ^ BITPIX = 64; two's complement binary integer of 64 bits
    | ThirtyTwoBitFloat -- ^ BITPIX = -32; IEEE single precision floating point of 32 bits
    | SixtyFourBitFloat -- ^ BITPIX = -64; IEEE double precision floating point of 64 bits

instance Show BitPixFormat where
        show EightBitInt       = "8 bit unsigned integer"
        show SixteenBitInt     = "16 bit signed integer"
        show ThirtyTwoBitInt   = "32 bit signed integer"
        show SixtyFourBitInt   = "64 bit signed interger"
        show ThirtyTwoBitFloat = "32 bit IEEE single precision float"
        show SixtyFourBitFloat = "64 bit IEEE double precision float"

{-| This utility function can be used to get the word count for data in an
    HDU.
-}
bitPixToWordSize :: BitPixFormat -> Natural
bitPixToWordSize EightBitInt       = 8
bitPixToWordSize SixteenBitInt     = 16
bitPixToWordSize ThirtyTwoBitInt   = 32
bitPixToWordSize ThirtyTwoBitFloat = 32
bitPixToWordSize SixtyFourBitInt   = 64
bitPixToWordSize SixtyFourBitFloat = 64

{-| This utility function can be used to get the size in bytes of the
-   format.
-}
bitPixToByteSize :: BitPixFormat -> Natural
bitPixToByteSize EightBitInt       = 1
bitPixToByteSize SixteenBitInt     = 2
bitPixToByteSize ThirtyTwoBitInt   = 4
bitPixToByteSize ThirtyTwoBitFloat = 4
bitPixToByteSize SixtyFourBitInt   = 8
bitPixToByteSize SixtyFourBitFloat = 8

{- | This utility functions quickly lets you know if you are dealing with
     integer data.
-}
isBitPixInt :: BitPixFormat -> Bool
isBitPixInt EightBitInt     = True
isBitPixInt SixteenBitInt   = True
isBitPixInt ThirtyTwoBitInt = True
isBitPixInt SixtyFourBitInt = True
isBitPixInt _ = False

{- | This utility functions quickly lets you know if you are dealing with
     floating point data.
-}
isBitPixFloat :: BitPixFormat -> Bool
isBitPixFloat ThirtyTwoBitFloat = True
isBitPixFloat SixtyFourBitFloat = True
isBitPixFloat _ = False

{- | Following `BitPixFormat` we have a tag for integer and floating point
     values. We box them up to ease parsing.
-}
data Pix = PB Int | PI16 Int | PI32 Int | PI64 Int | PF Double | PD Double

{- | Removes the `Pix` tag from an `Int` type within. -}
unPixI :: Pix -> Int
unPixI (PB b)   = b
unPixI (PI16 i) = i
unPixI (PI32 i) = i
unPixI (PI64 i) = i
unPixI _        = error "Pix are not stored as integers, invalid unpacking"

{- | Removes the `Pix` tag from a `Double` type within. -}
unPixD :: Pix -> Double
unPixD (PF d)   = d
unPixD (PD d)   = d
unPixD _        = error "Pix are not stored as floating point values, invalid unpacking"

{- | Remove the Pix wrapper for integer `Pix` lists.  -}
pixsUnwrapI :: BitPixFormat -> [Pix] -> [Int]
pixsUnwrapI EightBitInt       pxs = map unPixI pxs
pixsUnwrapI SixteenBitInt     pxs = map unPixI pxs
pixsUnwrapI ThirtyTwoBitInt   pxs = map unPixI pxs
pixsUnwrapI SixtyFourBitInt   pxs = map unPixI pxs
pixsUnwrapI _ _ = error "BitPixFormat is not an integer type"

{- | Remove the `Pix` wrapper for floating point `Pix` lists.  -}
pixsUnwrapD :: BitPixFormat -> [Pix] -> [Double]
pixsUnwrapD ThirtyTwoBitFloat pxs = map unPixD pxs
pixsUnwrapD SixtyFourBitFloat pxs = map unPixD pxs
pixsUnwrapD _ _ = error "BitPixFormat is not a floating point type"

getPix :: BitPixFormat -> Get Pix
getPix EightBitInt       = PB . fromIntegral <$> getInt8
getPix SixteenBitInt     = PI16 . fromIntegral <$> getInt16be
getPix ThirtyTwoBitInt   = PI32 . fromIntegral <$> getInt32be
getPix SixtyFourBitInt   = PI64 . fromIntegral <$> getInt64be
getPix ThirtyTwoBitFloat = PF . realToFrac <$> getFloatbe
getPix SixtyFourBitFloat = PD . realToFrac <$> getDoublebe

getPixs :: Int -> BitPixFormat -> Get [Pix]
getPixs c bpf = do
    empty <- isEmpty
    if empty
      then return []
      else do
        p <- getPix bpf
        ps <- getPixs (c - 1) bpf
        return (p:ps)

{- | This is the main low-level function which parses the data portion of an
     HDU. You need and element count, a format and a bytestring. The resulting
     list is produced in column-row major order as specified in the standard.
-}
parsePix :: Int -> BitPixFormat -> BL.ByteString -> IO [Pix]
parsePix c bpf bs = return $ runGet (getPixs c bpf) bs

{- `pixDimsByCol` takes a list of Axis and gives a column-row major list of
    axes dimensions.
-}
pixDimsByCol :: [Axis] -> [Int]
pixDimsByCol = map axisElementCount

{- `pixDimsByRow` takes a list of Axis and gives a row-column major list of
    axes dimensions.
-}
pixDimsByRow :: [Axis] -> [Int]
pixDimsByRow = reverse . pixDimsByCol

{-| The header part of the HDU is vital carrying not only authorship
    metadata, but also specifying how to make sense of the binary payload
    that starts 2,880 bytes after the start of the 'HeaderData'.
-}

data HeaderData = HeaderData
    { simpleFormat :: SimpleFormat
      -- ^ SIMPLE
    , bitPixFormat :: BitPixFormat
      -- ^ BITPIX
    , axes :: [Axis]
      -- ^ Axes metadata
    , objectIdentifier :: StringValue
      -- ^ OBJECT
    , observationDate :: StringValue
      -- ^ DATE
    , originIdentifier :: StringValue
      -- ^ OBJECT
    , telescopeIdentifier :: StringValue
      -- ^ TELESCOP
    , instrumentIdentifier :: StringValue
      -- ^ INSTRUME
    , observerIdentifier :: StringValue
      -- ^ OBSERVER
    , authorIdentifier :: StringValue
      -- ^ CREATOR
    , referenceString :: StringValue
      -- ^ REFERENC
    }

instance Default HeaderData where
    def = HeaderData NonConformant EightBitInt []
        (def :: StringValue) (def :: StringValue) (def :: StringValue)
        (def :: StringValue) (def :: StringValue) (def :: StringValue)
        (def :: StringValue) (def :: StringValue)

{-| The 'HeaderDataUnit' is the full HDU. Both the header information is
    encoded alongside the 'Axis' payload.
-}
data HeaderDataUnit = HeaderDataUnit
    { headerData :: HeaderData
      -- ^ Just the header part of the HDU
    , payloadData :: ByteString
      -- ^ The actual data payload
    }