{-# language DataKinds    #-}
{-# language GADTs        #-}
{-# language TypeFamilies #-}
{-# language ViewPatterns #-}
{- |
Module      :  FXPak.Internal
Copyright   :  (c) Christina Wuest 2021
License     :  BSD-style

Maintainer  :  tina@wuest.me
Stability   :  experimental
Portability :  non-portable

Internals for FXPak - ensures only valid packets can be encoded.
-}

module FXPak.Internal where

import Prelude

import qualified Data.ByteString.Char8 as BS

import Data.Bits ( (.|.), shiftL )

-- | Flags to be encoded as a 1-byte bit map
-- Note: No checking of flag validity is done in the original usb2snesw software
-- and as such research into which combinations produce expected results is
-- still underway
data Flag = SkipReset | OnlyReset | ClearX | SetX | StreamBurst | NoResponse | Data64Bytes deriving ( Eq, Show, Enum, Bounded )

-- | List of Flag data
-- Note: Since this will be reduced to a single bit map, flag duplication is not
-- considered invalid
type Flags = [Flag]

-- | Given a list of Flags, produce the bit map expected by the FXPak
fromFlags :: Flags -> Int
fromFlags = (foldl (.|.) 0) . (fmap $ (shiftL 1) . fromEnum)


-- | The context in which a packet's command operates
data Context = File | SNES | MSU | Config deriving ( Eq, Show, Enum, Bounded )
data Context' (c :: Context) where
    -- | File contexts involve manipulation of the FXPak's filesystem, including
    -- booting files directly
    File'   :: Context' 'File
    -- | SNES contexts involve the system's memory, allowing reading and writing
    -- of RAM
    SNES'   :: Context' 'SNES
    MSU'    :: Context' 'MSU
    Config' :: Context' 'Config

-- | View Pattern to extract a Context from its wrapping Context'
context :: Context' c -> Context
context File'   = File
context SNES'   = SNES
context MSU'    = MSU
context Config' = Config

-- | Represents the operation to be performed by the FXPak
data Opcode = Get | Put | VGet | VPut
            | List | Mkdir | Delete | Move
            | Reset | Boot | PowerCycle | Info | MenuReset | Stream | Time
            | Response
            deriving ( Eq, Show, Enum, Bounded )
data Opcode' (o :: Opcode) where
    -- | Get the contents of a file or target memory
    Get'        :: Opcode' 'Get
    -- | Write a file to the filesystem or a byte to memory
    Put'        :: Opcode' 'Put
    -- | Read and return between 1 and 4 regions of memory
    VGet'       :: Opcode' 'VGet
    -- | Write between 1 and 4 bytes to system memory
    VPut'       :: Opcode' 'VPut
    -- | List files at a given path
    List'       :: Opcode' 'List
    -- | Make a new directory at the given path
    Mkdir'      :: Opcode' 'Mkdir
    -- | Delete a node in the FXPak's filesystem
    Delete'     :: Opcode' 'Delete
    -- | Move a node in the FXPak's filesystem from one location to another
    Move'       :: Opcode' 'Move
    -- | Reset the SNES/SFC
    Reset'      :: Opcode' 'Reset
    -- | Boot a file at the target location
    Boot'       :: Opcode' 'Boot
    -- | Reset the SNES/SFC
    PowerCycle' :: Opcode' 'PowerCycle
    -- | Return information about the running FXPak
    Info'       :: Opcode' 'Info
    -- | Reset the SNES/SFC, returning to the FXPak's main menu
    MenuReset'  :: Opcode' 'MenuReset
    Stream'     :: Opcode' 'Stream
    Time'       :: Opcode' 'Time
    -- | Indicates a response packet
    -- A user should never set this for an outgoing packet.
    Response'   :: Opcode' 'Response

-- | View Pattern to extract a Opcode from its wrapping Opcode'
opcode :: Opcode' o -> Opcode
opcode Get'        = Get
opcode Put'        = Put
opcode VGet'       = VGet
opcode VPut'       = VPut
opcode List'       = List
opcode Mkdir'      = Mkdir
opcode Delete'     = Delete
opcode Move'       = Move
opcode Reset'      = Reset
opcode Boot'       = Boot
opcode PowerCycle' = PowerCycle
opcode Info'       = Info
opcode MenuReset'  = MenuReset
opcode Stream'     = Stream
opcode Time'       = Time
opcode Response'   = Response

-- | Represents an address to fetch memory from and a length to read
data AddressGet = AddressGet { start      :: Int
                             , dataLength :: Int
                             } deriving ( Show )

-- | Represents a value and the address to which it should be written
data AddressSet = AddressSet { target :: Int
                             , value  :: Int
                             } deriving ( Show )

-- | Arguments indicating the desired action taken by a given operation
data Arguments = None
               | Path FilePath
               | PathContents FilePath BS.ByteString
               | PathRename FilePath FilePath
               | GetBytes AddressGet
               | GetBytes2 (AddressGet, AddressGet)
               | GetBytes3 (AddressGet, AddressGet, AddressGet)
               | GetBytes4 (AddressGet, AddressGet, AddressGet, AddressGet)
               | SetByte AddressSet
               | SetByte2 (AddressSet, AddressSet)
               | SetByte3 (AddressSet, AddressSet, AddressSet)
               | SetByte4 (AddressSet, AddressSet, AddressSet, AddressSet)
               deriving ( Show )
data Arguments' (a :: Arguments) where
    -- | No arguments - valid only with the Reset, MenuReset, Info, Stream, and
    -- PowerCycle opcodes
    None' :: Arguments' 'None
    -- | Path to a given object - valid for Get, List, Mkdir, Delete, and Boot
    -- in the File context
    Path' :: FilePath -> Arguments' ('Path (a :: FilePath))
    -- | Path with an accompanying ByteString - valid only for Put in the File
    -- context
    PathContents' :: FilePath -> BS.ByteString -> Arguments' ('PathContents (a :: FilePath) (b :: BS.ByteString))
    -- | Source and destination paths - valid only for Move in the File context
    PathRename' :: FilePath -> FilePath -> Arguments' ('PathRename (a :: FilePath) a)
    -- | An address and length of data to be read for non-File context Get and
    -- VGet opcodes
    GetBytes' :: AddressGet -> Arguments' ('GetBytes (a :: AddressGet))
    -- | Two address/length pairs to be read for non-File context VGet
    GetBytes2' :: AddressGet -> AddressGet -> Arguments' ('GetBytes2 (a :: (AddressGet, AddressGet)))
    -- | Three address/length pairs to be read for non-File context VGet
    GetBytes3' :: AddressGet -> AddressGet -> AddressGet -> Arguments' ('GetBytes3 (a :: (AddressGet, AddressGet, AddressGet)))
    -- | Four address/length pairs to be read for non-File context VGet
    GetBytes4' :: AddressGet -> AddressGet -> AddressGet -> AddressGet -> Arguments' ('GetBytes4 (a :: (AddressGet, AddressGet, AddressGet, AddressGet)))
    -- | A target address and byte to be written for non-File context Put and
    -- VPut opcodes
    SetByte' :: AddressSet -> Arguments' ('SetByte (a :: AddressSet))
    -- | Two address/data pairs to be written for non-File context VPut
    SetByte2' :: AddressSet -> AddressSet -> Arguments' ('SetByte2 (a :: (AddressSet, AddressSet)))
    -- | Three address/data pairs to be written for non-File context VPut
    SetByte3' :: AddressSet -> AddressSet -> AddressSet -> Arguments' ('SetByte3 (a :: (AddressSet, AddressSet, AddressSet)))
    -- | Four address/data pairs to be written for non-File context VPut
    SetByte4' :: AddressSet -> AddressSet -> AddressSet -> AddressSet -> Arguments' ('SetByte4 (a :: (AddressSet, AddressSet, AddressSet, AddressSet)))

-- | View Pattern to extract an Arguments datum from its wrapping Arguments'
arguments :: Arguments' a -> Arguments
arguments None' = None
arguments (Path' a) = Path a
arguments (PathContents' a b) = PathContents a b
arguments (PathRename' a b) = PathRename a b
arguments (GetBytes' a) = GetBytes a
arguments (GetBytes2' a b) = GetBytes2 (a, b)
arguments (GetBytes3' a b c) = GetBytes3 (a, b, c)
arguments (GetBytes4' a b c d) = GetBytes4 (a, b, c, d)
arguments (SetByte' a) = SetByte a
arguments (SetByte2' a b) = SetByte2 (a, b)
arguments (SetByte3' a b c) = SetByte3 (a, b, c)
arguments (SetByte4' a b c d) = SetByte4 (a, b, c, d)

-- | ValidPacket allows a constraint to be added to functions which would create
-- Packet data, guaranteeing that they are only able to generate packets
-- conforming to the expected specifications of the FXPak
type family ValidPacket (c :: Context) (o :: Opcode) (a :: Arguments) :: Bool where
    ValidPacket 'File 'Get ('Path _) = 'True
    ValidPacket 'File 'Put ('PathContents _ _) = 'True
    ValidPacket 'File 'List ('Path _) = 'True
    ValidPacket 'File 'Mkdir ('Path _) = 'True
    ValidPacket 'File 'Delete ('Path _) = 'True
    ValidPacket 'File 'Move ('PathRename _ _) = 'True
    ValidPacket 'File 'Boot ('Path _) = 'True

    ValidPacket 'File 'Get _ = 'False
    ValidPacket 'File 'Put _ = 'False
    ValidPacket 'File 'VGet _ = 'False
    ValidPacket 'File 'VPut _ = 'False

    ValidPacket _ 'Get ('GetBytes _) = 'True
    ValidPacket _ 'Put ('SetByte _) = 'True
    ValidPacket _ 'VGet ('GetBytes _) = 'True
    ValidPacket _ 'VGet ('GetBytes2 _) = 'True
    ValidPacket _ 'VGet ('GetBytes3 _) = 'True
    ValidPacket _ 'VGet ('GetBytes4 _) = 'True
    ValidPacket _ 'VPut ('SetByte _) = 'True
    ValidPacket _ 'VPut ('SetByte2 _) = 'True
    ValidPacket _ 'VPut ('SetByte3 _) = 'True
    ValidPacket _ 'VPut ('SetByte4 _) = 'True

    ValidPacket _ 'Reset 'None = 'True
    ValidPacket _ 'MenuReset 'None = 'True
    ValidPacket _ 'Info 'None = 'True
    ValidPacket _ 'Stream 'None = 'True
    ValidPacket _ 'PowerCycle 'None = 'True

    ValidPacket _ _ _ = 'False

-- | Represents a Packet to be sent to the FXPak
data Packet = Packet Opcode Context Flags Arguments deriving ( Show )

-- | Smart Constructor for a Packet, guaranteeing validity via the ValidPacket
-- constraint
packet :: (ValidPacket c o a ~ 'True) => (Context' c) -> (Opcode' o) -> Flags -> (Arguments' a) -> Packet
packet (context -> c) (opcode -> o) flags (arguments -> a) = Packet o c flags a
