{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE DeriveGeneric       #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# OPTIONS_GHC -Wno-orphans     #-}

module Data.TTN.Types where

import Control.Monad (forM_, mzero)

import Data.Aeson ( decodeStrict
                  , eitherDecodeStrict
                  , Value(..)
                  , FromJSON(..)
                  , ToJSON(..)
                  , pairs
                  , withText
                  , (.:)
                  , (.:?)
                  , (.=)
                  , object)

import Data.ByteString.Char8 (ByteString)
import Data.Text (Text)

import GHC.Generics

import Data.Time.RFC3339
import Data.Time.LocalTime

import qualified Data.ByteString.Char8
import qualified Data.Text
import qualified System.Exit
import qualified System.IO

data TTNZonedTime = TTNZonedTime { unwrap :: ZonedTime }
  deriving (Eq, Show, Generic)

instance ToJSON TTNZonedTime where
  toJSON     = toJSON . (formatTimeRFC3339 :: ZonedTime -> Text) . unwrap
  toEncoding =  toEncoding . (formatTimeRFC3339 :: ZonedTime -> Text) . unwrap

instance FromJSON TTNZonedTime where
  parseJSON = withText "TTNZonedTime" $ \x ->
    case parseTimeRFC3339 x of
      Nothing -> fail $ "Parsing RFC3339 value failed, got:" ++ show x
      Just t -> return $ TTNZonedTime t

instance Eq ZonedTime where
  ZonedTime t1 tz1 == ZonedTime t2 tz2 = t1 == t2 && tz1 == tz2

-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/event.go#L65
data Config = Config {
    configFrequency  :: Maybe Double
  , configDataRate   :: Maybe Text
  , configCounter    :: Maybe Integer
  , configAirtime    :: Maybe Double
  , configPower      :: Maybe Double
  , configModulation :: Maybe Text
  } deriving (Eq, Show, Generic)

instance FromJSON Config where
  parseJSON (Object v) = Config
    <$> v .:? "frequency"
    <*> v .:? "data_rate"
    <*> v .:? "counter"
    <*> v .:? "airtime"
    <*> v .:? "power"
    <*> v .:? "modulation"
  parseJSON _          = mzero

instance ToJSON Config where
  toJSON     (Config {..}) = object [
      "frequency"   .= configFrequency
    , "data_rate"   .= configDataRate
    , "counter"     .= configCounter
    , "airtime"     .= configAirtime
    , "power"       .= configPower
    , "modulation"  .= configModulation
    ]
  toEncoding (Config {..}) = pairs (
       "frequency"  .= configFrequency
    <> "data_rate"  .= configDataRate
    <> "counter"    .= configCounter
    <> "airtime"    .= configAirtime
    <> "power"      .= configPower
    <> "modulation" .= configModulation
    )


-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/gateway_metadata.go
-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/location_metadata.go
data GatewaysElt = GatewaysElt {
    gatewaysEltGtwId                  :: Maybe Text
  , gatewaysEltGtwTrusted             :: Maybe Bool
  , gatewaysEltTimestamp              :: Maybe Integer
  , gatewaysEltFineTimestamp          :: Maybe Int
  , gatewaysEltFineTimestampEncrypted :: Maybe Text
  , gatewaysEltTime                   :: Maybe TTNZonedTime
  , gatewaysEltAntenna                :: Maybe Text
  , gatewaysEltChannel                :: Double
  , gatewaysEltRSSI                   :: Double
  , gatewaysEltSNR                    :: Double
  , gatewaysEltRFChain                :: Int
  , gatewaysEltLatitude               :: Maybe Double
  , gatewaysEltLongitude              :: Maybe Double
  , gatewaysEltAltitude               :: Maybe Int
  , gatewaysEltAccuracy               :: Maybe Int
  , gatewaysEltSource                 :: Maybe Text
  } deriving (Eq, Show, Generic)

instance FromJSON GatewaysElt where
  parseJSON (Object v) = GatewaysElt
    <$> v .:? "gtw_id"
    <*> v .:? "gtw_trusted"
    <*> v .:? "timestamp"
    <*> v .:? "fine_timestamp"
    <*> v .:? "fine_timestamp_encrypted"
    <*> v .:? "time"
    <*> v .:? "antenna"
    <*> v .:   "channel"
    <*> v .:   "rssi"
    <*> v .:   "snr"
    <*> v .:   "rf_chain"
    <*> v .:? "latitude"
    <*> v .:? "longitude"
    <*> v .:? "altitude"
    <*> v .:? "accuracy"
    <*> v .:? "source"
  parseJSON _          = mzero

instance ToJSON GatewaysElt where
  toJSON     (GatewaysElt {..}) = object [
      "gtw_id"                    .= gatewaysEltGtwId
    , "gtw_trusted"               .= gatewaysEltGtwTrusted
    , "timestamp"                 .= gatewaysEltTimestamp
    , "fine_timestamp"            .= gatewaysEltFineTimestamp
    , "fine_timestamp_encrypted"  .= gatewaysEltFineTimestampEncrypted
    , "time"                      .= gatewaysEltTime
    , "antenna"                   .= gatewaysEltAntenna
    , "channel"                   .= gatewaysEltChannel
    , "rssi"                      .= gatewaysEltRSSI
    , "snr"                       .= gatewaysEltSNR
    , "rf_chain"                  .= gatewaysEltRFChain
    , "latitude"                  .= gatewaysEltLatitude
    , "longitude"                 .= gatewaysEltLongitude
    , "altitude"                  .= gatewaysEltAltitude
    , "accuracy"                  .= gatewaysEltAccuracy
    , "source"                    .= gatewaysEltSource
    ]
  toEncoding (GatewaysElt {..}) = pairs (
       "gtw_id"                   .= gatewaysEltGtwId
    <> "gtw_trusted"              .= gatewaysEltGtwTrusted
    <> "timestamp"                .= gatewaysEltTimestamp
    <> "fine_timestamp"           .= gatewaysEltFineTimestamp
    <> "fine_timestamp_encrypted" .= gatewaysEltFineTimestampEncrypted
    <> "time"                     .= gatewaysEltTime
    <> "antenna"                  .= gatewaysEltAntenna
    <> "channel"                  .= gatewaysEltChannel
    <> "rssi"                     .= gatewaysEltRSSI
    <> "snr"                      .= gatewaysEltSNR
    <> "rf_chain"                 .= gatewaysEltRFChain
    <> "latitude"                 .= gatewaysEltLatitude
    <> "longitude"                .= gatewaysEltLongitude
    <> "altitude"                 .= gatewaysEltAltitude
    <> "accuracy"                 .= gatewaysEltAccuracy
    <> "source"                   .= gatewaysEltSource
    )


-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/metadata.go
data Metadata = Metadata {
    metadataTime       :: Maybe TTNZonedTime
  , metadataFrequency  :: Maybe Double
  , metadataModulation :: Maybe Text
  , metadataDataRate   :: Maybe Text
  , metadataBitRate    :: Maybe Int
  , metadataAirtime    :: Maybe Double
  , metadataCodingRate :: Maybe Text
  , metadataGateways   :: [GatewaysElt]
  } deriving (Eq, Show, Generic)

instance FromJSON Metadata where
  parseJSON (Object v) = Metadata <$>
        v .:? "time"
    <*> v .:? "frequency"
    <*> v .:? "modulation"
    <*> v .:? "data_rate"
    <*> v .:? "bit_rate"
    <*> v .:? "airtime"
    <*> v .:? "coding_rate"
    <*> v .:   "gateways"
  parseJSON _          = mzero

instance ToJSON Metadata where
  toJSON     (Metadata {..}) = object [
      "time"         .= metadataTime
    , "frequency"    .= metadataFrequency
    , "modulation"   .= metadataModulation
    , "data_rate"    .= metadataDataRate
    , "bit_rate"     .= metadataBitRate
    , "coding_rate"  .= metadataCodingRate
    , "airtime"      .= metadataAirtime
    , "gateways"     .= metadataGateways
    ]
  toEncoding (Metadata {..}) = pairs (
       "time" .= metadataTime
    <> "frequency"   .= metadataFrequency
    <> "modulation"  .= metadataModulation
    <> "data_rate"   .= metadataDataRate
    <> "bit_rate"    .= metadataBitRate
    <> "coding_rate" .= metadataCodingRate
    <> "airtime"     .= metadataAirtime
    <> "gateways"    .= metadataGateways
    )


data Message = Message {
    messageDevId :: Text,
    messageAppId :: Text,
    messagePort  :: Double
  } deriving (Eq, Show, Generic)

instance FromJSON Message where
  parseJSON (Object v) = Message
    <$> v .: "dev_id"
    <*> v .: "app_id"
    <*> v .: "port"
  parseJSON _          = mzero

instance ToJSON Message where
  toJSON     (Message {..}) = object [
      "dev_id"  .= messageDevId
    , "app_id"  .= messageAppId
    , "port"    .= messagePort
    ]
  toEncoding (Message {..}) = pairs (
       "dev_id" .= messageDevId
    <> "app_id" .= messageAppId
    <> "port"   .= messagePort
    )


-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/uplink_message.go
data Uplink = Uplink {
    uplinkConfig         :: Maybe Config
  , uplinkGatewayId      :: Maybe Text
  , uplinkDevId          :: Maybe Text
  , uplinkPayload        :: Maybe Text
  , uplinkCounter        :: Maybe Integer
  , uplinkIsRetry        :: Maybe Bool
  , uplinkMetadata       :: Maybe Metadata
  , uplinkPayloadRaw     :: Maybe Text
  , uplinkMessage        :: Maybe Message
  , uplinkAppId          :: Maybe Text
  , uplinkConfirmed      :: Maybe Bool
  , uplinkHardwareSerial :: Maybe Text
  , uplinkPort           :: Maybe Double
  } deriving (Eq, Show, Generic)

instance FromJSON Uplink where
  parseJSON (Object v) = Uplink
    <$> v .:? "config"
    <*> v .:? "gateway_id"
    <*> v .:? "dev_id"
    <*> v .:? "payload"
    <*> v .:? "counter"
    <*> v .:? "is_retry"
    <*> v .:? "metadata"
    <*> v .:? "payload_raw"
    <*> v .:? "message"
    <*> v .:? "app_id"
    <*> v .:? "confirmed"
    <*> v .:? "hardware_serial"
    <*> v .:? "port"
  parseJSON _          = mzero

instance ToJSON Uplink where
  toJSON     (Uplink {..}) = object [
      "config"           .= uplinkConfig
    , "gateway_id"       .= uplinkGatewayId
    , "dev_id"           .= uplinkDevId
    , "payload"          .= uplinkPayload
    , "counter"          .= uplinkCounter
    , "is_retry"         .= uplinkIsRetry
    , "metadata"         .= uplinkMetadata
    , "payload_raw"      .= uplinkPayloadRaw
    , "message"          .= uplinkMessage
    , "app_id"           .= uplinkAppId
    , "confirmed"        .= uplinkConfirmed
    , "hardware_serial"  .= uplinkHardwareSerial
    , "port"             .= uplinkPort
    ]
  toEncoding (Uplink {..}) = pairs (
       "config"          .= uplinkConfig
    <> "gateway_id"      .= uplinkGatewayId
    <> "dev_id"          .= uplinkDevId
    <> "payload"         .= uplinkPayload
    <> "counter"         .= uplinkCounter
    <> "is_retry"        .= uplinkIsRetry
    <> "metadata"        .= uplinkMetadata
    <> "payload_raw"     .= uplinkPayloadRaw
    <> "message"         .= uplinkMessage
    <> "app_id"          .= uplinkAppId
    <> "confirmed"       .= uplinkConfirmed
    <> "hardware_serial" .= uplinkHardwareSerial
    <> "port"            .= uplinkPort
    )

data Schedule = ScheduleReplace | ScheduleFirst | ScheduleLast
  deriving (Eq, Show)

instance FromJSON Schedule where
  parseJSON = withText "Schedule" $ \x ->
    case x of
      "replace" -> return ScheduleReplace
      "first"   -> return ScheduleFirst
      "last"    -> return ScheduleLast
      w         -> fail $ "Unknown schedule" ++ (Data.Text.unpack w)

instance ToJSON Schedule where
  toJSON ScheduleReplace = "replace"
  toJSON ScheduleFirst   = "first"
  toJSON ScheduleLast    = "last"

-- https://github.com/TheThingsNetwork/ttn/blob/develop/core/types/downlink_message.go
data Downlink = Downlink {
    downlinkAppId          :: Text
  , downlinkDevId          :: Text
  , downlinkPort           :: Double
  , downlinkPayloadRaw     :: Text
  , downlinkConfirmed      :: Bool
  , downlinkSchedule       :: Maybe Schedule
  } deriving (Show, Eq, Generic)

instance FromJSON Downlink where
  parseJSON (Object v) = Downlink
    <$> v .:   "app_id"
    <*> v .:   "dev_id"
    <*> v .:   "port"
    <*> v .:   "payload_raw"
    <*> v .:   "confirmed"
    <*> v .:? "schedule"
  parseJSON _          = mzero

instance ToJSON Downlink where
  toJSON     (Downlink {..}) = object [
      "app_id"           .= downlinkAppId
    , "dev_id"           .= downlinkDevId
    , "port"             .= downlinkPort
    , "payload_raw"      .= downlinkPayloadRaw
    , "confirmed"        .= downlinkConfirmed
    , "schedule"         .= downlinkSchedule
    ]
  toEncoding (Downlink {..}) = pairs (
       "app_id"          .= downlinkAppId
    <> "dev_id"          .= downlinkDevId
    <> "port"            .= downlinkPort
    <> "payload_raw"     .= downlinkPayloadRaw
    <> "confirmed"       .= downlinkConfirmed
    <> "schedule"        .= downlinkSchedule
    )


data Error = Error { errorMsg :: Text } deriving (Show)

instance FromJSON Error where
  parseJSON (Object v) = Error <$> v .: "error"
  parseJSON _          = mzero

instance ToJSON Error where
  toJSON (Error {..}) = object [ "error" .= errorMsg ]
  toEncoding (Error {..}) = pairs ( "error" .= errorMsg )

-- used by ttn-client
data EventType =
    Up
  | Down
  | DownAcked
  | DownSent
  | DownScheduled
  | Activation
  | Create
  | Update
  | Delete
  | Unknown
  deriving (Eq, Ord, Show, Generic)

data Event =
    Event EventType Uplink
  | ClientError String
  deriving (Eq, Show, Generic)

instance FromJSON EventType where
instance ToJSON EventType where
instance FromJSON Event where
instance ToJSON Event where

parse :: ByteString -> Either String Uplink
parse = eitherDecodeStrict

parseError :: ByteString -> Either String Error
parseError = eitherDecodeStrict

parseFile :: FilePath -> IO Uplink
parseFile filename = do
  input <- Data.ByteString.Char8.readFile filename
  case decodeStrict input of
    Nothing -> fatal $ case (decodeStrict input :: Maybe Value) of
                         Nothing -> "Invalid JSON file: "     ++ filename
                         Just _v -> "Mismatched JSON value from file: " ++ filename
    Just r  -> return (r :: Uplink)
  where
    fatal :: String -> IO a
    fatal msg = do System.IO.hPutStrLn System.IO.stderr msg
                   System.Exit.exitFailure

parseMany :: [String] -> IO ()
parseMany filenames = do
  forM_ filenames (\f -> parseFile f >>= (\p -> p `seq` putStrLn $ "Successfully parsed " ++ f))