{-# LANGUAGE CPP                       #-}
{-# LANGUAGE DeriveAnyClass            #-}
{-# LANGUAGE DeriveFoldable            #-}
{-# LANGUAGE DeriveFunctor             #-}
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings         #-}

#if __GLASGOW_HASKELL__ >= 800
{-# LANGUAGE DeriveLift                #-}
{-# LANGUAGE StandaloneDeriving        #-}
#else
{-# LANGUAGE TemplateHaskell           #-}
#endif

module Network.Top.Types(
  -- *Top access point configuration
  Config(..),cfgIP,cfgPort,cfgPath,def

  -- *Connection Protocols
  --,byPattern
  ,ByPattern(..)
  ,ByType(..),byTypeRouter
  ,ByAny(..),byAny
  ,Echo(..)

  -- *Connection
  ,App
  ,Connection(..)
  ,inputWithTimeout

  -- *WebSocket Connection
  ,WSApp
  ,WSConnection
  ,chatsProtocol,chatsProtocolT

  -- *CHATS
  ,WSChannelResult
  ,ChannelSelectionResult(..)

  -- *Network Addresses
  ,WebSocketAddress(..),SocketAddress(..),IP4Address(..),IP6Address,HostAddress(..),HostPort(..)

  -- *Re-exports
  -- ,module ZM
  -- ,B.ByteString
  ) where

import qualified Data.ByteString                as B
import           Data.Default.Class
import           Data.List
import           Data.Pattern.Types
import           Data.Text                      (Text)
import qualified Data.Text                      as T
import           Data.Text.Encoding
import           Data.Word
import           Network.Top.Util
import           System.Timeout
import           Text.PrettyPrint.HughesPJClass (text)
import           ZM

#if __GLASGOW_HASKELL__ < 800
import           Language.Haskell.TH.Lift
#else
import           Language.Haskell.TH.Syntax (Lift) -- ,lift)
#endif

-- |Top's access point configuration
newtype Config = Config {accessPoint::WebSocketAddress IP4Address}

-- |Return Top's access point IP
cfgIP :: Config -> String
cfgIP = prettyShow . socketAddress . host . accessPoint

-- |Return Top's access point Port
cfgPort :: Config -> Int
cfgPort = fromIntegral . port . socketPort . host . accessPoint

-- |Return Top's access point Path
cfgPath :: Config -> String
cfgPath = path . accessPoint

-- |The configuration for the default Top router
instance Default Config where
  def = Config $ WebSocketAddress False (SocketAddress (DNSAddress "quid2.net") (HostPort 80)) "/ws"

---------------- Routing Protocols

-- |Return the value of the ByType router identifier for the given type
byTypeRouter :: Type AbsRef -> TypedBLOB
byTypeRouter t =
  let TypeApp f _ = absType (Proxy::Proxy (ByType ()))
  in typedBLOB_ (TypeApp f t) ByType

-- |Echo protocol: any value sent in is returned verbatim to the sender (useful for testing purposes)
-- Client can specify if received messages should be logged (for debugging purposes)
data Echo a = Echo { echoDebug :: Bool }
            deriving (Eq, Ord, Show, Generic, Flat)
instance Model a => Model (Echo a)

{-|
A routing protocol specified by a type.

Once a connection is established, clients:

   * can send messages of the given type

   * will receive all messages of the same type sent by other agents
-}
data ByType a = ByType
  deriving (Eq, Ord, Show, Generic, Flat)
instance Model a =>  Model (ByType a)

{-|
A routing protocol to receive all messages.

The ByAny type parameter indicates the type of the messages exchanged on the channel (usually:TypedBLOB).

Once a connection is established, clients:

   * can send messages of any type, as values of the ByAny type argument (for example: an Int value encoded as the corresponding TypedBLOB value)

   * will receive all messages sent by other agents
-}
data ByAny a = ByAny deriving (Eq, Ord, Show, Generic, Flat)
instance Model a =>  Model (ByAny a)

-- |Shortcut to specify
byAny :: ByAny TypedBLOB
byAny = ByAny :: ByAny TypedBLOB

---------- Connection

-- |An application that connects to a channel of type a and eventually returns an IO r
type App a r = Connection a -> IO r

-- |A typed bidirectional connection/channel
data Connection a = Connection {
   -- |Block read till a value is received
   input::IO a

   -- |Block write till a value is sent
   ,output::a -> IO ()

   -- |Close the connection
   ,close :: IO ()
 }

-- |Return a value received on the connection
-- or Nothing if no value is received in the specified number of seconds
--
-- NOTE: In case of timeout, the connection will be closed.
inputWithTimeout :: Int -> Connection a -> IO (Maybe a)
inputWithTimeout secs conn = timeout (seconds secs) (input conn)

---------- WebSocket Connection

-- |An application that connects to a WebSocket channel of type a and eventually returns an IO r
type WSApp r = App B.ByteString r

-- A typed connection
-- data Connection a = Connection WS.Connection

-- CHECK: use Input/Output from pipes-concurrency instead?
-- data WSConnection = WSConnection {sendMsg :: B.ByteString -> IO (),receiveMsg :: IO B.ByteString}
-- |A WebSocket connection
type WSConnection = Connection B.ByteString

-- data Connection a = Connection {
--    -- |Block read till a value is received
--    -- returns Nothing if the connection is closed
--    input::IO (Maybe a)

--    -- |Output a value
--    -- returns True if output succeeded, False otherwise
--    ,output::a -> IO Bool
--  }


---------- CHATS protocol

type WSChannelResult = ChannelSelectionResult (WebSocketAddress IP4Address)

-- |The value returned by an access point, after receiving a routing channel setup request.
data ChannelSelectionResult addr =
                                 -- |The channel has been permanently setup to the requested
                                 -- protocol
                                  Success
                                 |
                                 -- |The access point is unable or unwilling to open a connection
                                 -- with the requested routing protocol
                                  Failure { reason :: String }
                                 |
                                 -- |User should retry with the same transport protocol at the
                                 -- indicated address
                                  RetryAt addr
  deriving (Eq, Ord, Show, Generic, Flat)
instance Model a => Model (ChannelSelectionResult a)

-- |CHATS binary identifier
chatsProtocol :: B.ByteString
chatsProtocol = encodeUtf8 chatsProtocolT

-- |CHATS textual identifier
chatsProtocolT :: T.Text
chatsProtocolT = "chats"

---------- Network Addresses

-- |The full address of a <https://en.wikipedia.org/wiki/WebSocket WebSocket> endpoint
data WebSocketAddress ip =
       WebSocketAddress
         {
         -- |True if the connection is wss (secure), False if is ws
         secure :: Bool
         -- |Host endpoint, example: SocketAddress (DNSAddress "quid2.net") (HostPort 80)
         , host :: SocketAddress ip
         -- |Path to the WebSocket access point, example: "/ws"
         , path :: String
         }
  deriving (Eq, Ord, Show, Generic, Flat)
instance Model ip => Model (WebSocketAddress ip)

-- |The address of a <https://en.wikipedia.org/wiki/Network_socket network socket>
data SocketAddress ip = SocketAddress { socketAddress :: HostAddress ip, socketPort :: HostPort }
  deriving (Eq, Ord, Show, Generic, Flat)
instance Model ip => Model (SocketAddress ip)

-- |A Sockets port (e.g. 80)
data HostPort = HostPort {port::Word16} deriving (Eq, Ord, Show, Generic,Flat,Model)

-- |A host address, either an IP or a DNS domain
data HostAddress ip = IPAddress ip
                    | DNSAddress String
  deriving (Eq, Ord, Show, Generic, Flat)
instance Model ip => Model (HostAddress ip)

-- |An IP4 address
data IP4Address = IP4Address Word8 Word8 Word8 Word8 deriving (Eq, Ord, Show, Generic, Flat, Model)

-- |An IP6 address
data IP6Address = IP6Address Word16 Word16 Word16 Word16 Word16 Word16 Word16 Word16 deriving (Eq, Ord, Show, Generic, Flat, Model)

---------- Pretty instances
-- Easier to define here than in a separate file
instance Pretty IP4Address where
  pPrint (IP4Address w1 w2 w3 w4) = text . intercalate "." . map hex $ [w1, w2, w3, w4]

instance Pretty ip => Pretty (HostAddress ip) where
  pPrint (IPAddress ip) = pPrint ip
  pPrint (DNSAddress t) = text t


-- Lift instances (needed by TH)
#if __GLASGOW_HASKELL__ < 800
--deriveLift ''ByPattern
--deriveLift ''Match
deriveLift ''Pat
-- deriveLift ''WildCard
deriveLift ''PRef
#else
-- deriving instance Lift a => Lift (ByPattern a)
--deriving instance Lift (ByPattern a)
--deriving instance Lift Match
-- deriving instance Lift a => Lift (Type a)
-- deriving instance Lift AbsRef
-- deriving instance Lift (SHA3_256_6 a)
deriving instance Lift a => Lift (Pat a)
-- deriving instance Lift WildCard
deriving instance Lift PRef
#endif

-- Call/Return protocol
-- data Call a = Call a CallBack
--             | Return CallBack a
-- type CallBack = [Word8]

-- instance Invariant Conn where
--   invmap _ _ ConnOpening = ConnOpening
--   invmap f g (ConnOpen i o) = ConnOpen (fmap f i) (o . g)
--   invmap _ _ ConnClosed = ConnClosed


-- combine:: Connection a -> Connection b -> Connection (Either a b)
-- combine c1 c2 = Connection ci co
--   where
--     ci = do -- BAD: this blocks on the first connection
--       i1 <- input c1
--       case i1 of
--         Nothing -> (Right <$>) <$> input c2
--         Just v  -> return . Just . Left $ v
--     co (Left a)  = output c1 a
--     co (Right b) = output c2 b