-- | Data types for Osc messages, bundles and packets.
module Sound.Osc.Packet where

import Sound.Osc.Datum {- hosc -}

-- * Message

{- | Osc address pattern.  This is strictly an Ascii value, however it
  is very common to pattern match on addresses and matching on
  Data.ByteString.Char8 requires @OverloadedStrings@.
-}
type Address_Pattern = String

-- | An Osc message, an 'Address_Pattern' and a sequence of 'Datum'.
data Message = Message
  { messageAddress :: !Address_Pattern
  , messageDatum :: ![Datum]
  }
  deriving (Ord, Eq, Read, Show)

{- | 'Message' constructor.  It is an 'error' if the 'Address_Pattern'
doesn't conform to the Osc specification.
-}
message :: Address_Pattern -> [Datum] -> Message
message a xs =
  case a of
    '/' : _ -> Message a xs
    _ -> error "message: ill-formed address pattern"

messageSignature :: Message -> String
messageSignature = signatureFor . messageDatum

messageDescriptor :: Message -> Ascii
messageDescriptor = descriptor . messageDatum

-- * Bundle

{- | An Osc bundle, a 'Time' and a sequence of 'Message's.
The type parameter specifies the element type.
Ordinarily this is Message, which does not allow recursion.
-}
data BundleOf t = Bundle
  { bundleTime :: !Time
  , bundleMessages :: ![t]
  }
  deriving (Eq, Read, Show)

type Bundle = BundleOf Message

-- | Osc 'Bundle's can be ordered (time ascending).
instance Eq t => Ord (BundleOf t) where
  compare (Bundle a _) (Bundle b _) = compare a b

-- | 'Bundle' constructor. It is an 'error' if the 'Message' list is empty.
bundle :: Time -> [t] -> BundleOf t
bundle t xs =
  case xs of
    [] -> error "bundle: empty?"
    _ -> Bundle t xs

-- * Packet

-- | An Osc 'Packet' is either a 'Message' or a 'Bundle t'.
data PacketOf t
  = Packet_Message {packetMessage :: !Message}
  | Packet_Bundle {packetBundle :: !(BundleOf t)}
  deriving (Eq, Read, Show)

type Packet = PacketOf Message

-- | 'Packet_Bundle' of 'bundle'.
p_bundle :: Time -> [t] -> PacketOf t
p_bundle t = Packet_Bundle . bundle t

-- | 'Packet_Message' of 'message'.
p_message :: Address_Pattern -> [Datum] -> PacketOf t
p_message a = Packet_Message . message a

{- | Constant indicating a bundle to be executed immediately.
It has the Ntp64 representation of @1@.

>>> immediately == (1 / (2 ^ 32))
True
-}
immediately :: Time
immediately = 1 / 2 ^ (32 :: Int)

-- | The 'Time' of 'Packet', if the 'Packet' is a 'Message' this is 'immediately'.
packetTime :: PacketOf t -> Time
packetTime = at_packet (const immediately) bundleTime

-- | Retrieve the set of 'Message's from a 'Packet'.
packetMessages :: PacketOf Message -> [Message]
packetMessages = at_packet return bundleMessages

-- | If 'Packet' is a 'Message' add 'immediately' timestamp, else 'id'.
packet_to_bundle :: PacketOf Message -> BundleOf Message
packet_to_bundle = at_packet (\m -> Bundle immediately [m]) id

{- | If 'Packet' is a 'Message' or a 'Bundle' with an /immediate/ time
tag and with one element, return the 'Message', else 'Nothing'.
-}
packet_to_message :: PacketOf Message -> Maybe Message
packet_to_message p =
  case p of
    Packet_Bundle b ->
      case b of
        Bundle t [m] -> if t == immediately then Just m else Nothing
        _ -> Nothing
    Packet_Message m -> Just m

-- | Is 'Packet' immediate, ie. a 'Bundle' with timestamp 'immediately', or a plain Message.
packet_is_immediate :: PacketOf t -> Bool
packet_is_immediate = (== immediately) . packetTime

-- | Variant of 'either' for 'Packet'.
at_packet :: (Message -> a) -> (BundleOf t -> a) -> PacketOf t -> a
at_packet f g p =
  case p of
    Packet_Message m -> f m
    Packet_Bundle b -> g b

-- * Address Query

-- | Does 'Message' have the specified 'Address_Pattern'.
message_has_address :: Address_Pattern -> Message -> Bool
message_has_address x = (== x) . messageAddress

{- | Do any of the 'Message's at 'Bundle Message' have the specified
'Address_Pattern'.
-}
bundle_has_address :: Address_Pattern -> BundleOf Message -> Bool
bundle_has_address x = any (message_has_address x) . bundleMessages

{- | Does 'Packet' have the specified 'Address_Pattern', ie.
'message_has_address' or 'bundle_has_address'.
-}
packet_has_address :: Address_Pattern -> PacketOf Message -> Bool
packet_has_address x =
  at_packet
    (message_has_address x)
    (bundle_has_address x)
