Fix laws (#32)

Fix laws (#32)

* Fix succ/pred laws
* Add non-skipping laws
* Use pred/succ instead of arithmetic
* Use `any` and `all` per @paf31's suggestion
* Add law explanations

module Data.Enum
  ( class Enum, succ, pred
  , defaultSucc
  , defaultPred
  , enumFromTo
  , enumFromThenTo
  , upFrom
  , upFromIncluding
  , downFrom
  , Cardinality(..)
  , class BoundedEnum, cardinality, toEnum, fromEnum, toEnumWithDefaults
  , defaultCardinality
  , defaultToEnum
  , defaultFromEnum
  ) where

import Prelude

import Control.MonadPlus (guard)

import Data.Char (fromCharCode, toCharCode)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..), maybe, fromJust)
import Data.Newtype (class Newtype, unwrap)
import Data.NonEmpty (NonEmpty, (:|))
import Data.Tuple (Tuple(..))
import Data.Unfoldable (class Unfoldable, unfoldr)

import Partial.Unsafe (unsafePartial)

newtype Cardinality a = Cardinality Int

derive instance newtypeCardinality :: Newtype (Cardinality a) _

derive newtype instance eqCardinality :: Eq (Cardinality a)

derive newtype instance ordCardinality :: Ord (Cardinality a)

-- | Type class for enumerations.
-- |
-- | Laws:
-- | - Successor: `all (a < _) (succ a)`
-- | - Predecessor: `all (_ < a) (pred a)`
-- | - Succ retracts pred: `pred >=> succ >=> pred = pred`
-- | - Pred retracts succ: `succ >=> pred >=> succ = succ`
-- | - Non-skipping succ: `b <= a || any (_ <= b) (succ a)`
-- | - Non-skipping pred: `a <= b || any (b <= _) (pred a)`
-- |
-- | The retraction laws can intuitively be understood as saying that `succ` is
-- | the opposite of `pred`; if you apply `succ` and then `pred` to something,
-- | you should end up with what you started with (although of course this
-- | doesn't apply if you tried to `succ` the last value in an enumeration and
-- | therefore got `Nothing` out). The non-skipping laws can intuitively be
-- | understood as saying that `succ` shouldn't skip over any elements of your
-- | type. For example, without the non-skipping laws, it would be permissible
-- | to write an `Enum Int` instance where `succ x = Just (x+2)`, and similarly
-- | `pred x = Just (x-2)`.
class Ord a <= Enum a where
  succ :: a -> Maybe a
  pred :: a -> Maybe a

instance enumBoolean :: Enum Boolean where
  succ false = Just true
  succ _ = Nothing
  pred true = Just false
  pred _= Nothing

instance enumInt :: Enum Int where
  succ n = if n < top then Just (n + 1) else Nothing
  pred n = if n > bottom then Just (n - 1) else Nothing

instance enumChar :: Enum Char where
  succ = defaultSucc charToEnum toCharCode
  pred = defaultPred charToEnum toCharCode

charToEnum :: Int -> Maybe Char
charToEnum n | n >= bottom && n <= top = Just $ fromCharCode n
charToEnum _ = Nothing

instance enumUnit :: Enum Unit where
  succ = const Nothing
  pred = const Nothing

instance enumOrdering :: Enum Ordering where
  succ LT = Just EQ
  succ EQ = Just GT
  succ GT = Nothing
  pred LT = Nothing
  pred EQ = Just LT
  pred GT = Just EQ

instance enumMaybe :: BoundedEnum a => Enum (Maybe a) where
  succ Nothing = Just $ Just bottom
  succ (Just a) = Just <$> succ a
  pred Nothing = Nothing
  pred (Just a) = Just $ pred a

instance enumEither :: (BoundedEnum a, BoundedEnum b) => Enum (Either a b) where
  succ (Left a) = maybe (Just $ Right bottom) (Just <<< Left) (succ a)
  succ (Right b) = maybe (Nothing) (Just <<< Right) (succ b)
  pred (Left a) = maybe (Nothing) (Just <<< Left) (pred a)
  pred (Right b) = maybe (Just $ Left top) (Just <<< Right) (pred b)

instance enumTuple :: (Enum a, BoundedEnum b) => Enum (Tuple a b) where
  succ (Tuple a b) = maybe (flip Tuple bottom <$> succ a) (Just <<< Tuple a) (succ b)
  pred (Tuple a b) = maybe (flip Tuple top <$> pred a) (Just <<< Tuple a) (pred b)

-- | ```defaultSucc toEnum fromEnum = succ```
defaultSucc :: forall a. (Int -> Maybe a) -> (a -> Int) -> a -> Maybe a
defaultSucc toEnum' fromEnum' a = toEnum' (fromEnum' a + 1)

-- | ```defaultPred toEnum fromEnum = pred```
defaultPred :: forall a. (Int -> Maybe a) -> (a -> Int) -> a -> Maybe a
defaultPred toEnum' fromEnum' a = toEnum' (fromEnum' a - 1)

-- | Returns a successive sequence of elements from the lower bound to
-- | the upper bound (inclusive).
enumFromTo :: forall a u. Enum a => Unfoldable u => a -> a -> u a
enumFromTo from to = unfoldr go (Just from)
  where
    go mx = do
      x <- mx
      guard (x <= to)
      pure $ Tuple x (succ x)

-- | `[a,b..c]`
enumFromThenTo :: forall a. BoundedEnum a => a -> a -> a -> Array a
enumFromThenTo = unsafePartial \a b c ->
  let a' = fromEnum a
      b' = fromEnum b
      c' = fromEnum c
  in (toEnum >>> fromJust) <$> intStepFromTo (b' - a') a' c'

-- | Property: ```forall e in intStepFromTo step a b: a <= e <= b```
intStepFromTo :: Int -> Int -> Int -> Array Int
intStepFromTo step from to =
  unfoldr (\e ->
            if e <= to
            then Just $ Tuple e (e + step)  -- Output the value e, set the next state to (e + step)
            else Nothing                    -- End of the collection.
          ) from

diag :: forall a. a -> Tuple a a
diag a = Tuple a a

-- | Results in all successors from given Enum in some unfoldable.
-- | Note that given Enum is not included in the result.
upFrom :: forall a u. Enum a => Unfoldable u => a -> u a
upFrom = unfoldr (map diag <<< succ)

-- | Results in all successors of given Enum (including itself)
-- | in some NonEmpty unfoldable.
upFromIncluding :: ∀ a u. Enum a => Unfoldable u => a -> NonEmpty u a
upFromIncluding x = x :| upFrom x

downFrom :: forall a u. Enum a => Unfoldable u => a -> u a
downFrom = unfoldr (map diag <<< pred)

-- | Type class for finite enumerations.
-- |
-- | This should not be considered a part of a numeric hierarchy, as in Haskell.
-- | Rather, this is a type class for small, ordered sum types with
-- | statically-determined cardinality and the ability to easily compute
-- | successor and predecessor elements, e.g. `DayOfWeek`.
-- |
-- | Laws:
-- |
-- | - ```succ bottom >>= succ >>= succ ... succ [cardinality - 1 times] == top```
-- | - ```pred top    >>= pred >>= pred ... pred [cardinality - 1 times] == bottom```
-- | - ```forall a > bottom: pred a >>= succ == Just a```
-- | - ```forall a < top:  succ a >>= pred == Just a```
-- | - ```forall a > bottom: fromEnum <$> pred a = pred (fromEnum a)```
-- | - ```forall a < top:  fromEnum <$> succ a = succ (fromEnum a)```
-- | - ```e1 `compare` e2 == fromEnum e1 `compare` fromEnum e2```
-- | - ```toEnum (fromEnum a) = Just a```
class (Bounded a, Enum a) <= BoundedEnum a where
  cardinality :: Cardinality a
  toEnum :: Int -> Maybe a
  fromEnum :: a -> Int

instance boundedEnumBoolean :: BoundedEnum Boolean where
  cardinality = Cardinality 2
  toEnum 0 = Just false
  toEnum 1 = Just true
  toEnum _ = Nothing
  fromEnum false = 0
  fromEnum true = 1

instance boundedEnumChar :: BoundedEnum Char where
  cardinality = Cardinality (toCharCode top - toCharCode bottom)
  toEnum = charToEnum
  fromEnum = toCharCode

instance boundedEnumUnit :: BoundedEnum Unit where
  cardinality = Cardinality 1
  toEnum 0 = Just unit
  toEnum _ = Nothing
  fromEnum = const 0

instance boundedEnumOrdering :: BoundedEnum Ordering where
  cardinality = Cardinality 3
  toEnum 0 = Just LT
  toEnum 1 = Just EQ
  toEnum 2 = Just GT
  toEnum _ = Nothing
  fromEnum LT = 0
  fromEnum EQ = 1
  fromEnum GT = 2

instance boundedEnumMaybe :: BoundedEnum a => BoundedEnum (Maybe a) where
  cardinality = Cardinality $ unwrap (cardinality :: Cardinality a) + 1
  toEnum 0 = pure Nothing
  toEnum n = Just <$> toEnum (n - 1)
  fromEnum Nothing = 0
  fromEnum (Just e) = fromEnum e + 1

instance boundedEnumEither :: (BoundedEnum a, BoundedEnum b) => BoundedEnum (Either a b) where
  cardinality =
    Cardinality
      $ unwrap (cardinality :: Cardinality a)
      + unwrap (cardinality :: Cardinality b)
  toEnum n = to cardinality
    where
    to :: Cardinality a -> Maybe (Either a b)
    to (Cardinality ca)
      | n >= 0 && n < ca = Left <$> toEnum n
      | otherwise = Right <$> toEnum (n - ca)
  fromEnum (Left a) = fromEnum a
  fromEnum (Right b) = fromEnum b + unwrap (cardinality :: Cardinality a)

instance boundedEnumTuple :: (BoundedEnum a, BoundedEnum b) => BoundedEnum (Tuple a b) where
  cardinality =
    Cardinality
      $ unwrap (cardinality :: Cardinality a)
      * unwrap (cardinality :: Cardinality b)
  toEnum = to cardinality
    where
    to :: Cardinality b -> Int -> Maybe (Tuple a b)
    to (Cardinality cb) n = Tuple <$> toEnum (n / cb) <*> toEnum (n `mod` cb)
  fromEnum = from cardinality
    where
    from :: Cardinality b -> Tuple a b -> Int
    from (Cardinality cb) (Tuple a b) = fromEnum a * cb + fromEnum b

-- | Runs in `O(n)` where `n` is `fromEnum top`
defaultCardinality :: forall a. Bounded a => Enum a => Cardinality a
defaultCardinality = Cardinality $ defaultCardinality' 1 (bottom :: a) where
  defaultCardinality' i = maybe i (defaultCardinality' $ i + 1) <<< succ

  -- | Runs in `O(n)` where `n` is `fromEnum a`
defaultToEnum :: forall a. Bounded a => Enum a => Int -> Maybe a
defaultToEnum n
  | n < 0 = Nothing
  | n == 0 = Just bottom
  | otherwise = defaultToEnum (n - 1) >>= succ

  -- | Runs in `O(n)` where `n` is `fromEnum a`
defaultFromEnum :: forall a. Enum a => a -> Int
defaultFromEnum = maybe 0 (\prd -> defaultFromEnum prd + 1) <<< pred

-- | Like `toEnum` but returns the first argument if `x` is less than
-- | `fromEnum bottom` and the second argument if `x` is greater than
-- | `fromEnum top`.
-- |
-- | ``` purescript
-- | toEnumWithDefaults False True (-1) -- False
-- | toEnumWithDefaults False True 0    -- False
-- | toEnumWithDefaults False True 1    -- True
-- | toEnumWithDefaults False True 2    -- True
-- | ```
toEnumWithDefaults :: forall a. BoundedEnum a => a -> a -> Int -> a
toEnumWithDefaults b t x = case toEnum x of
  Just enum -> enum
  Nothing -> if x < fromEnum (bottom :: a) then b else t

module Data.Enum
  ( class Enum, succ, pred
  , defaultSucc
  , defaultPred
  , enumFromTo
  , enumFromThenTo
  , upFrom
  , upFromIncluding
  , downFrom
  , Cardinality(..)
  , class BoundedEnum, cardinality, toEnum, fromEnum, toEnumWithDefaults
  , defaultCardinality
  , defaultToEnum
  , defaultFromEnum
  ) where

import Prelude

import Control.MonadPlus (guard)

import Data.Char (fromCharCode, toCharCode)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..), maybe, fromJust)
import Data.Newtype (class Newtype, unwrap)
import Data.NonEmpty (NonEmpty, (:|))
import Data.Tuple (Tuple(..))
import Data.Unfoldable (class Unfoldable, unfoldr)

import Partial.Unsafe (unsafePartial)

newtype Cardinality a = Cardinality Int

derive instance newtypeCardinality :: Newtype (Cardinality a) _

derive newtype instance eqCardinality :: Eq (Cardinality a)

derive newtype instance ordCardinality :: Ord (Cardinality a)

-- | Type class for enumerations.
-- |
-- | Laws:
-- | - `succ a > pred a`
-- | - `pred a < succ a`
-- | - `pred >=> succ >=> pred = pred`
-- | - `succ >=> pred >=> succ = succ`
class Ord a <= Enum a where
  succ :: a -> Maybe a
  pred :: a -> Maybe a

instance enumBoolean :: Enum Boolean where
  succ false = Just true
  succ _ = Nothing
  pred true = Just false
  pred _= Nothing

instance enumInt :: Enum Int where
  succ n = if n < top then Just (n + 1) else Nothing
  pred n = if n > bottom then Just (n - 1) else Nothing

instance enumChar :: Enum Char where
  succ = defaultSucc charToEnum toCharCode
  pred = defaultPred charToEnum toCharCode

charToEnum :: Int -> Maybe Char
charToEnum n | n >= bottom && n <= top = Just $ fromCharCode n
charToEnum _ = Nothing

instance enumUnit :: Enum Unit where
  succ = const Nothing
  pred = const Nothing

instance enumOrdering :: Enum Ordering where
  succ LT = Just EQ
  succ EQ = Just GT
  succ GT = Nothing
  pred LT = Nothing
  pred EQ = Just LT
  pred GT = Just EQ

instance enumMaybe :: BoundedEnum a => Enum (Maybe a) where
  succ Nothing = Just $ Just bottom
  succ (Just a) = Just <$> succ a
  pred Nothing = Nothing
  pred (Just a) = Just $ pred a

instance enumEither :: (BoundedEnum a, BoundedEnum b) => Enum (Either a b) where
  succ (Left a) = maybe (Just $ Right bottom) (Just <<< Left) (succ a)
  succ (Right b) = maybe (Nothing) (Just <<< Right) (succ b)
  pred (Left a) = maybe (Nothing) (Just <<< Left) (pred a)
  pred (Right b) = maybe (Just $ Left top) (Just <<< Right) (pred b)

instance enumTuple :: (Enum a, BoundedEnum b) => Enum (Tuple a b) where
  succ (Tuple a b) = maybe (flip Tuple bottom <$> succ a) (Just <<< Tuple a) (succ b)
  pred (Tuple a b) = maybe (flip Tuple top <$> pred a) (Just <<< Tuple a) (pred b)

-- | ```defaultSucc toEnum fromEnum = succ```
defaultSucc :: forall a. (Int -> Maybe a) -> (a -> Int) -> a -> Maybe a
defaultSucc toEnum' fromEnum' a = toEnum' (fromEnum' a + 1)

-- | ```defaultPred toEnum fromEnum = pred```
defaultPred :: forall a. (Int -> Maybe a) -> (a -> Int) -> a -> Maybe a
defaultPred toEnum' fromEnum' a = toEnum' (fromEnum' a - 1)

-- | Returns a successive sequence of elements from the lower bound to
-- | the upper bound (inclusive).
enumFromTo :: forall a u. Enum a => Unfoldable u => a -> a -> u a
enumFromTo from to = unfoldr go (Just from)
  where
    go mx = do
      x <- mx
      guard (x <= to)
      pure $ Tuple x (succ x)

-- | `[a,b..c]`
enumFromThenTo :: forall a. BoundedEnum a => a -> a -> a -> Array a
enumFromThenTo = unsafePartial \a b c ->
  let a' = fromEnum a
      b' = fromEnum b
      c' = fromEnum c
  in (toEnum >>> fromJust) <$> intStepFromTo (b' - a') a' c'

-- | Property: ```forall e in intStepFromTo step a b: a <= e <= b```
intStepFromTo :: Int -> Int -> Int -> Array Int
intStepFromTo step from to =
  unfoldr (\e ->
            if e <= to
            then Just $ Tuple e (e + step)  -- Output the value e, set the next state to (e + step)
            else Nothing                    -- End of the collection.
          ) from

diag :: forall a. a -> Tuple a a
diag a = Tuple a a

-- | Results in all successors from given Enum in some unfoldable.
-- | Note that given Enum is not included in the result.
upFrom :: forall a u. Enum a => Unfoldable u => a -> u a
upFrom = unfoldr (map diag <<< succ)

-- | Results in all successors of given Enum (including itself)
-- | in some NonEmpty unfoldable.
upFromIncluding :: ∀ a u. Enum a => Unfoldable u => a -> NonEmpty u a
upFromIncluding x = x :| upFrom x

downFrom :: forall a u. Enum a => Unfoldable u => a -> u a
downFrom = unfoldr (map diag <<< pred)

-- | Type class for finite enumerations.
-- |
-- | This should not be considered a part of a numeric hierarchy, as in Haskell.
-- | Rather, this is a type class for small, ordered sum types with
-- | statically-determined cardinality and the ability to easily compute
-- | successor and predecessor elements, e.g. `DayOfWeek`.
-- |
-- | Laws:
-- |
-- | - ```succ bottom >>= succ >>= succ ... succ [cardinality - 1 times] == top```
-- | - ```pred top    >>= pred >>= pred ... pred [cardinality - 1 times] == bottom```
-- | - ```forall a > bottom: pred a >>= succ == Just a```
-- | - ```forall a < top:  succ a >>= pred == Just a```
-- | - ```forall a > bottom: fromEnum <$> pred a = Just (fromEnum a - 1)```
-- | - ```forall a < top:  fromEnum <$> succ a = Just (fromEnum a + 1)```
-- | - ```e1 `compare` e2 == fromEnum e1 `compare` fromEnum e2```
-- | - ```toEnum (fromEnum a) = Just a```
class (Bounded a, Enum a) <= BoundedEnum a where
  cardinality :: Cardinality a
  toEnum :: Int -> Maybe a
  fromEnum :: a -> Int

instance boundedEnumBoolean :: BoundedEnum Boolean where
  cardinality = Cardinality 2
  toEnum 0 = Just false
  toEnum 1 = Just true
  toEnum _ = Nothing
  fromEnum false = 0
  fromEnum true = 1

instance boundedEnumChar :: BoundedEnum Char where
  cardinality = Cardinality (toCharCode top - toCharCode bottom)
  toEnum = charToEnum
  fromEnum = toCharCode

instance boundedEnumUnit :: BoundedEnum Unit where
  cardinality = Cardinality 1
  toEnum 0 = Just unit
  toEnum _ = Nothing
  fromEnum = const 0

instance boundedEnumOrdering :: BoundedEnum Ordering where
  cardinality = Cardinality 3
  toEnum 0 = Just LT
  toEnum 1 = Just EQ
  toEnum 2 = Just GT
  toEnum _ = Nothing
  fromEnum LT = 0
  fromEnum EQ = 1
  fromEnum GT = 2

instance boundedEnumMaybe :: BoundedEnum a => BoundedEnum (Maybe a) where
  cardinality = Cardinality $ unwrap (cardinality :: Cardinality a) + 1
  toEnum 0 = pure Nothing
  toEnum n = Just <$> toEnum (n - 1)
  fromEnum Nothing = 0
  fromEnum (Just e) = fromEnum e + 1

instance boundedEnumEither :: (BoundedEnum a, BoundedEnum b) => BoundedEnum (Either a b) where
  cardinality =
    Cardinality
      $ unwrap (cardinality :: Cardinality a)
      + unwrap (cardinality :: Cardinality b)
  toEnum n = to cardinality
    where
    to :: Cardinality a -> Maybe (Either a b)
    to (Cardinality ca)
      | n >= 0 && n < ca = Left <$> toEnum n
      | otherwise = Right <$> toEnum (n - ca)
  fromEnum (Left a) = fromEnum a
  fromEnum (Right b) = fromEnum b + unwrap (cardinality :: Cardinality a)

instance boundedEnumTuple :: (BoundedEnum a, BoundedEnum b) => BoundedEnum (Tuple a b) where
  cardinality =
    Cardinality
      $ unwrap (cardinality :: Cardinality a)
      * unwrap (cardinality :: Cardinality b)
  toEnum = to cardinality
    where
    to :: Cardinality b -> Int -> Maybe (Tuple a b)
    to (Cardinality cb) n = Tuple <$> toEnum (n / cb) <*> toEnum (n `mod` cb)
  fromEnum = from cardinality
    where
    from :: Cardinality b -> Tuple a b -> Int
    from (Cardinality cb) (Tuple a b) = fromEnum a * cb + fromEnum b

-- | Runs in `O(n)` where `n` is `fromEnum top`
defaultCardinality :: forall a. Bounded a => Enum a => Cardinality a
defaultCardinality = Cardinality $ defaultCardinality' 1 (bottom :: a) where
  defaultCardinality' i = maybe i (defaultCardinality' $ i + 1) <<< succ

  -- | Runs in `O(n)` where `n` is `fromEnum a`
defaultToEnum :: forall a. Bounded a => Enum a => Int -> Maybe a
defaultToEnum n
  | n < 0 = Nothing
  | n == 0 = Just bottom
  | otherwise = defaultToEnum (n - 1) >>= succ

  -- | Runs in `O(n)` where `n` is `fromEnum a`
defaultFromEnum :: forall a. Enum a => a -> Int
defaultFromEnum = maybe 0 (\prd -> defaultFromEnum prd + 1) <<< pred

-- | Like `toEnum` but returns the first argument if `x` is less than
-- | `fromEnum bottom` and the second argument if `x` is greater than
-- | `fromEnum top`.
-- |
-- | ``` purescript
-- | toEnumWithDefaults False True (-1) -- False
-- | toEnumWithDefaults False True 0    -- False
-- | toEnumWithDefaults False True 1    -- True
-- | toEnumWithDefaults False True 2    -- True
-- | ```
toEnumWithDefaults :: forall a. BoundedEnum a => a -> a -> Int -> a
toEnumWithDefaults b t x = case toEnum x of
  Just enum -> enum
  Nothing -> if x < fromEnum (bottom :: a) then b else t


raw: Add Ord/Eq instances for Value

raw: Add Ord/Eq instances for Value

module Mesos.Raw where

import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (catchError, throwError)
import Data.Either (Either(..))
import Data.Foreign (F, Foreign, ForeignError(..), toForeign, writeObject, isNull, isUndefined, unsafeFromForeign)
import Data.Foreign.Class (class AsForeign, class IsForeign, read, write, readProp, (.=))
import Data.Foreign.Index (prop)
import Data.Foreign.NullOrUndefined (readNullOrUndefined, unNullOrUndefined)
import Data.Foreign.Undefined (Undefined(..))
import Data.Identity (Identity)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Monoid (class Monoid, mempty)
import Data.Newtype (class Newtype, unwrap, over2)
import Data.List.NonEmpty as NEL

readPropNU :: forall a. (IsForeign a) => String -> Foreign -> F (Maybe a)
readPropNU p v = unNullOrUndefined <$> (prop p v >>= readNullOrUndefined read)

readPropNUM :: forall a. (IsForeign a, Monoid a) => String -> Foreign -> F a
readPropNUM p v = fromMaybe mempty <$> readPropNU p v

isNullOrUndefined :: Foreign -> Boolean
isNullOrUndefined = isNull || isUndefined

-- | TODO: actually implement this
foreign import data HealthCheck :: *

instance healthCheckAsForeign :: AsForeign HealthCheck where
    write = toForeign

instance healthCheckIsForeign :: IsForeign HealthCheck where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data DiscoveryInfo :: *

instance discoveryInfoAsForeign :: AsForeign DiscoveryInfo where
    write = toForeign

instance discoveryInfoIsForeign :: IsForeign DiscoveryInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data LinuxInfo :: *

instance linuxInfoAsForeign :: AsForeign LinuxInfo where
    write = toForeign

instance linuxInfoIsForeign :: IsForeign LinuxInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data NetworkInfo :: *

instance networkInfoAsForeign :: AsForeign NetworkInfo where
    write = toForeign

instance networkInfoIsForeign :: IsForeign NetworkInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data TaskGroupInfo :: *

instance taskGroupInfoAsForeign :: AsForeign TaskGroupInfo where
    write = toForeign

instance taskGroupInfoIsForeign :: IsForeign TaskGroupInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data DockerInfo :: *

instance dockerInfoAsForeign :: AsForeign DockerInfo where
    write = toForeign

instance dockerInfoIsForeign :: IsForeign DockerInfo where
    read = pure <<< unsafeFromForeign

data VolumeMode = ReadWrite
                | ReadOnly

instance volumeModeAsForeign :: AsForeign VolumeMode where
    write ReadWrite = toForeign "RW"
    write ReadOnly = toForeign "RO"

instance volumeModeIsForeign :: IsForeign VolumeMode where
    read obj = do
        s <- read obj
        case s of
          "RW" -> pure ReadWrite
          "RO" -> pure ReadOnly
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown volume mode: " <> t

data Parameter = Parameter String String

instance parameterAsForeign :: AsForeign Parameter where
    write (Parameter name value) = toForeign { name: name, value: value }

instance parameterIsForeign :: IsForeign Parameter where
    read obj = do
        name <- readProp "name" obj
        value <- readProp "value" obj
        pure $ Parameter name value

newtype Parameters = Parameters (Array Parameter)

instance parametersAsForeign :: AsForeign Parameters where
    write (Parameters params) = toForeign { parameter: write params }

instance parametersIsForeign :: IsForeign Parameters where
    read obj = do
        params <- readProp "parameter" obj
        pure $ Parameters params

newtype VolumeSourceDocker = VolumeSourceDocker
    { driver :: Maybe String
    , name :: String
    , driverOptions :: Maybe Parameters
    }

instance volumeSourceDockerAsForeign :: AsForeign VolumeSourceDocker where
    write (VolumeSourceDocker obj) = writeObject props where
        props = [ "driver" .= (write $ Undefined obj.driver)
                , "name" .= write obj.name
                , "driver_options" .= (write $ Undefined obj.driverOptions)
                ]

instance volumeSourceDockerIsForeign :: IsForeign VolumeSourceDocker where
    read obj = do
        driver <- readPropNU "driver" obj
        name <- readProp "name" obj
        driverOptions <- readPropNU "driver_options" obj
        pure <<< VolumeSourceDocker $
            { driver: driver
            , name: name
            , driverOptions: driverOptions
            }

newtype VolumeSourceSandbox = VolumeSourceSandbox
    { type :: String
    , path :: String
    }

instance volumeSourceSandboxAsForeign :: AsForeign VolumeSourceSandbox where
    write (VolumeSourceSandbox obj) = toForeign obj

instance volumeSourceSandboxIsForeign :: IsForeign VolumeSourceSandbox where
    read obj = do
        t <- readProp "type" obj
        path <- readProp "path" obj
        pure <<< VolumeSourceSandbox $
            { type: t
            , path: path
            }

newtype Volume = Volume
    { mode :: VolumeMode
    , containerPath :: String
    , hostPath :: Maybe String
    , image :: Maybe Image
    , source :: Maybe (Either VolumeSourceDocker VolumeSourceSandbox)
    }

instance volumeAsForeign :: AsForeign Volume where
    write (Volume obj) = writeObject props where
        props = sourceProps <> [ "mode" .= write obj.mode
                               , "container_path" .= write obj.containerPath
                               , "host_path" .= (write $ Undefined obj.hostPath)
                               , "image" .= (write $ Undefined obj.image)
                               ]
        sourceProps =
            case obj.source of
              Nothing -> []
              Just (Left docker) -> [ "source" .= toForeign { type: "DOCKER_VOLUME", docker_volume: write docker } ]
              Just (Right sandbox) -> [ "source" .= toForeign { type: "SANDBOX_PATH", sandbox_path: write sandbox } ]

instance volumeIsForeign :: IsForeign Volume where
    read obj = do
        mode <- readProp "mode" obj
        containerPath <- readProp "container_path" obj
        hostPath <- readPropNU "host_path" obj
        image <- readPropNU "image" obj
        maybeSource <- catchError (Just <$> prop "source" obj) \_ -> pure Nothing
        let filteredForeignSource =
                do
                    foreignSource <- maybeSource
                    if isNullOrUndefined foreignSource
                       then Nothing
                       else Just foreignSource
        source <- case filteredForeignSource of
                    Nothing -> pure Nothing
                    Just f -> Just <$> readSource f
        pure <<< Volume $
            { mode: mode
            , containerPath: containerPath
            , hostPath: hostPath
            , image: image
            , source: source
            }
        where
            readSource :: Foreign -> F (Either VolumeSourceDocker VolumeSourceSandbox)
            readSource sObj = do
                ty <- readProp "type" sObj
                case ty of
                  "DOCKER_VOLUME" -> Left <$> readProp "docker_volume" sObj
                  "SANDBOX_PATH" -> Right <$> readProp "sandbox_path" sObj
                  t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown volume source type: " <> t

newtype Credential = Credential
    { principal :: String
    , secret :: Maybe String
    }

instance credentialAsForeign :: AsForeign Credential where
    write (Credential obj) = writeObject props where
        props = [ "principal" .= write obj.principal
                , "secret" .= (write $ Undefined obj.secret)
                ]

instance credentialIsForeign :: IsForeign Credential where
    read obj = do
        principal <- readProp "principal" obj
        secret <- readPropNU "secret" obj
        pure <<< Credential $
            { principal: principal
            , secret: secret
            }

newtype DockerImage = DockerImage
    { name :: String
    , credential :: Maybe Credential
    }

instance dockerImageAsForeign :: AsForeign DockerImage where
    write (DockerImage obj) = writeObject props where
        props = [ "name" .= write obj.name
                , "credential" .= (write $ Undefined obj.credential)
                ]

instance dockerImageIsForeign :: IsForeign DockerImage where
    read obj = do
        name <- readProp "name" obj
        credential <- readPropNU "credential" obj
        pure <<< DockerImage $
            { name: name
            , credential: credential
            }

-- | TODO: Actually implement this
foreign import data AppcImage :: *

instance appcImageAsForeign :: AsForeign AppcImage where
    write = toForeign

instance appcImageIsForeign :: IsForeign AppcImage where
    read = pure <<< unsafeFromForeign

newtype Image = Image
    { image :: Either AppcImage DockerImage
    , cached :: Boolean
    }

instance imageAsForeign :: AsForeign Image where
    write (Image obj) = writeObject props where
        props = imageProps <> [ "cached" .= write obj.cached
                              ]
        imageProps =
            case obj.image of
              Left appc -> [ "type" .= toForeign "APPC"
                           , "appc" .= write appc
                           ]
              Right docker -> [ "type" .= toForeign "DOCKER"
                              , "docker" .= write docker
                              ]

instance imageIsForeign :: IsForeign Image where
    read obj = do
        cached <- fromMaybe true <$> readPropNU "cached" obj
        image <- readProp "type" obj >>= readImage
        pure <<< Image $
            { image: image
            , cached: cached
            }
        where
            readImage :: String -> F (Either AppcImage DockerImage)
            readImage "APPC" = Left <$> readProp "appc" obj
            readImage "DOCKER" = Right <$> readProp "docker" obj
            readImage t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown image type: " <> t

data ExecutorInfoType = DefaultExecutor
                      | CustomExecutor

instance executorInfoTypeAsForeign :: AsForeign ExecutorInfoType where
    write (DefaultExecutor) = toForeign "DEFAULT"
    write (CustomExecutor) = toForeign "CUSTOM"

instance executorInfoTypeIsForeign :: IsForeign ExecutorInfoType where
    read obj = do
        s <- read obj
        case s of
          "DEFAULT" -> pure DefaultExecutor
          "CUSTOM" -> pure CustomExecutor
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown executor info type: " <> t

data EnvironmentVariable = EnvironmentVariable String String

instance environmentVariableAsForeign :: AsForeign EnvironmentVariable where
    write (EnvironmentVariable name value) = toForeign { name: name, value: value }

instance environmentVariableIsForeign :: IsForeign EnvironmentVariable where
    read obj = do
        name <- readProp "name" obj
        value <- readProp "value" obj
        pure $ EnvironmentVariable name value

newtype Environment = Environment (Array EnvironmentVariable)

instance environmentAsForeign :: AsForeign Environment where
    write (Environment vars) = toForeign $
        { variables: write vars
        }

instance environmentIsForeign :: IsForeign Environment where
    read obj = do
        variables <- readPropNUM "variables" obj
        pure $ Environment variables

newtype CommandInfoURI = CommandInfoURI
    { value :: String
    , executable :: Maybe Boolean
    , extract :: Maybe Boolean
    , cache :: Maybe Boolean
    , outputFile :: Maybe String
    }

instance commandInfoURIAsForeign :: AsForeign CommandInfoURI where
    write (CommandInfoURI obj) = writeObject props where
        props = [ "value" .= write obj.value
                , "executable" .= (write $ Undefined obj.executable)
                , "extract" .= (write $ Undefined obj.extract)
                , "cache" .= (write $ Undefined obj.cache)
                , "output_file" .= (write $ Undefined obj.outputFile)
                ]

instance commandInfoURIIsForeign :: IsForeign CommandInfoURI where
    read obj = do
        value <- readProp "value" obj
        executable <- readPropNU "executable" obj
        extract <- readPropNU "extract" obj
        cache <- readPropNU "cache" obj
        outputFile <- readPropNU "output_file" obj
        pure <<< CommandInfoURI $
            { value: value
            , executable: executable
            , extract: extract <|> Just true
            , cache: cache
            , outputFile: outputFile
            }

newtype CommandInfo = CommandInfo
    { uris :: Array CommandInfoURI
    , environment :: Maybe Environment
    , shell :: Boolean
    , value :: Maybe String
    , arguments :: Array String
    , user :: Maybe String
    }

instance commandInfoAsForeign :: AsForeign CommandInfo where
    write (CommandInfo obj) = writeObject props where
        props = [ "uris" .= write obj.uris
                , "environment" .= (write $ Undefined obj.environment)
                , "shell" .= write obj.shell
                , "value" .= (write $ Undefined obj.value)
                , "arguments" .= write obj.arguments
                , "user" .= (write $ Undefined obj.user)
                ]

instance commandInfoIsForeign :: IsForeign CommandInfo where
    read obj = do
        uris <- readPropNUM "uris" obj
        environment <- readPropNU "environment" obj
        shell <- readPropNU "shell" obj
        value <- readPropNU "value" obj
        arguments <- readPropNUM "arguments" obj
        user <- readPropNU "user" obj
        pure <<< CommandInfo $
            { uris: uris
            , environment: environment
            , shell: fromMaybe true shell
            , value: value
            , arguments: arguments
            , user: user
            }

newtype DurationInfo = DurationInfo Int

instance durationInfoAsForeign :: AsForeign DurationInfo where
    write (DurationInfo nanoseconds) = toForeign { nanoseconds: nanoseconds }

instance durationInfoIsForeign :: IsForeign DurationInfo where
    read obj = do
        nanoseconds <- readProp "nanoseconds" obj
        pure $ DurationInfo nanoseconds

data ContainerType = DockerContainer
                   | MesosContainer

instance containerTypeAsForeign :: AsForeign ContainerType where
    write DockerContainer = toForeign "DOCKER"
    write MesosContainer = toForeign "MESOS"

instance containerTypeIsForeign :: IsForeign ContainerType where
    read obj = do
        s <- read obj
        case s of
          "DOCKER" -> pure DockerContainer
          "MESOS" -> pure MesosContainer
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown container info type: " <> t

newtype MesosInfo = MesosInfo
    { image :: Image
    }

instance mesosInfoAsForeign :: AsForeign MesosInfo where
    write (MesosInfo obj) = toForeign $ { image: write obj.image }

instance mesosInfoIsForeign :: IsForeign MesosInfo where
    read obj = do
        image <- readProp "image" obj
        pure <<< MesosInfo $
            { image: image
            }

newtype ContainerInfo = ContainerInfo
    { type :: ContainerType
    , volumes :: Array Volume
    , hostname :: Maybe String
    , docker :: Maybe DockerInfo
    , mesos :: Maybe MesosInfo
    , networkInfos :: Array NetworkInfo
    , linuxInfo :: Maybe LinuxInfo
    }

instance containerInfoAsForeign :: AsForeign ContainerInfo where
    write (ContainerInfo obj) = writeObject props where
        props = [ "type" .= write obj.type
                , "volumes" .= write obj.volumes
                , "hostname" .= (write $ Undefined obj.hostname)
                , "docker" .= (write $ Undefined obj.docker)
                , "mesos" .= (write $ Undefined obj.mesos)
                , "network_infos" .= write obj.networkInfos
                , "linux_info" .= (write <<< Undefined) obj.linuxInfo
                ]

instance containerInfoIsForeign :: IsForeign ContainerInfo where
    read obj = do
        t <- readProp "type" obj
        volumes <- readPropNUM "volumes" obj
        hostname <- readPropNU "hostname" obj
        docker <- readPropNU "docker" obj
        mesos <- readPropNU "mesos" obj
        networkInfos <- readPropNUM "network_infos" obj
        linuxInfo <- readPropNU "linux_info" obj
        pure <<< ContainerInfo $
            { type: t
            , volumes: volumes
            , hostname: hostname
            , docker: docker
            , mesos: mesos
            , networkInfos: networkInfos
            , linuxInfo: linuxInfo
            }

newtype ExecutorInfo = ExecutorInfo
    { type :: ExecutorInfoType
    , executorId :: ExecutorID
    , frameworkId :: FrameworkID
    , command :: Maybe CommandInfo
    , container :: Maybe ContainerInfo
    , resources :: Array Resource
    , name :: Maybe String
    , data :: Maybe String
    , discovery :: Maybe DiscoveryInfo
    , shutdownGracePeriod :: Maybe DurationInfo
    , labels :: Maybe Labels
    }

instance executorInfoAsForeign :: AsForeign ExecutorInfo where
    write (ExecutorInfo obj) = writeObject props where
        props = [ "type" .= write obj.type
                , "executor_id" .= write obj.executorId
                , "framework_id" .= write obj.frameworkId
                , "command" .= (write $ Undefined obj.command)
                , "container" .= (write $ Undefined obj.container)
                , "resources" .= write obj.resources
                , "name" .= (write $ Undefined obj.name)
                , "data" .= (write $ Undefined obj.data)
                , "discovery" .= (write $ Undefined obj.discovery)
                , "shutdown_grace_period" .= (write $ Undefined obj.shutdownGracePeriod)
                , "labels" .= (write $ Undefined obj.labels)
                ]

instance executorInfoIsForeign :: IsForeign ExecutorInfo where
    read obj = do
        t <- readProp "type" obj
        executorId <- readProp "executor_id" obj
        frameworkId <- readProp "framework_id" obj
        command <- readPropNU "command" obj
        container <- readPropNU "container" obj
        resources <- readPropNUM "resources" obj
        name <- readPropNU "name" obj
        d <- readPropNU "data" obj
        discovery <- readPropNU "discovery" obj
        shutdownGracePeriod <- readPropNU "shutdown_grace_period" obj
        labels <- readPropNU "labels" obj
        pure <<< ExecutorInfo $
            { type: t
            , executorId: executorId
            , frameworkId: frameworkId
            , command: command
            , container: container
            , resources: resources
            , name: name
            , data: d
            , discovery: discovery
            , shutdownGracePeriod: shutdownGracePeriod
            , labels: labels
            }

newtype KillPolicy = KillPolicy
    { gracePeriod :: Maybe DurationInfo
    }

instance killPolicyAsForeign :: AsForeign KillPolicy where
    write (KillPolicy obj) = writeObject props where
        props = [ "grace_period" .= (write $ Undefined obj.gracePeriod)
                ]

instance killPolicyIsForeign :: IsForeign KillPolicy where
    read obj = do
        gracePeriod <- readPropNU "grace_period" obj
        pure <<< KillPolicy $
            { gracePeriod: gracePeriod
            }

newtype TaskInfo = TaskInfo
    { name :: String
    , taskId :: TaskID
    , slaveId :: SlaveID
    , resources :: Array Resource
    , executor :: Maybe ExecutorInfo
    , command :: Maybe CommandInfo
    , container :: Maybe ContainerInfo
    , healthCheck :: Maybe HealthCheck
    , killPolicy :: Maybe KillPolicy
    , data :: Maybe String
    , labels :: Maybe Labels
    , discoveryInfo :: Maybe DiscoveryInfo
    }

instance taskInfoAsForeign :: AsForeign TaskInfo where
    write (TaskInfo obj) = writeObject props where
        props = [ "name" .= write obj.name
                , "task_id" .= write obj.taskId
                , "agent_id" .= write obj.slaveId
                , "resources" .= write obj.resources
                , "executor" .= (write $ Undefined obj.executor)
                , "command" .= (write $ Undefined obj.command)
                , "container" .= (write $ Undefined obj.container)
                , "health_check" .= (write $ Undefined obj.healthCheck)
                , "kill_policy" .= (write $ Undefined obj.killPolicy)
                , "data" .= (write $ Undefined obj.data)
                , "labels" .= (write $ Undefined obj.labels)
                , "discovery_info" .= (write $ Undefined obj.discoveryInfo)
                ]

instance taskInfoIsForeign :: IsForeign TaskInfo where
    read obj = do
        name <- readProp "name" obj
        taskId <- readProp "task_id" obj
        slaveId <- readProp "slave_id" obj <|> readProp "agent_id" obj
        resources <- readPropNUM "resources" obj
        executor <- readPropNU "executor" obj
        command <- readPropNU "command" obj
        container <- readPropNU "container" obj
        healthCheck <- readPropNU "health_check" obj
        killPolicy <- readPropNU "kill_policy" obj
        d <- readPropNU "data" obj
        labels <- readPropNU "labels" obj
        discoveryInfo <- readPropNU "discovery_info" obj
        pure <<< TaskInfo $
            { name: name
            , taskId: taskId
            , slaveId: slaveId
            , resources: resources
            , executor: executor
            , command: command
            , container: container
            , healthCheck: healthCheck
            , killPolicy: killPolicy
            , data: d
            , labels: labels
            , discoveryInfo: discoveryInfo
            }

derive instance taskInfoNewtype :: Newtype TaskInfo _

newtype Filters = Filters
    { refuseSeconds :: Maybe Number
    }

instance filtersAsForeign :: AsForeign Filters where
    write (Filters obj) = toForeign $ { refuse_seconds: write $ Undefined obj.refuseSeconds }

instance filtersIsForeign :: IsForeign Filters where
    read obj = do
        refuseSeconds <- readPropNU "refuse_seconds" obj
        pure $ Filters { refuseSeconds: refuseSeconds
                       }

-- | TODO: actually implement this
foreign import data ContainerStatus :: *

instance containerStatusAsForeign :: AsForeign ContainerStatus where
    write = toForeign

instance containerStatusIsForeign :: IsForeign ContainerStatus where
    read = pure <<< unsafeFromForeign

newtype TaskStatus = TaskStatus
    { taskId :: TaskID
    , state :: String
    , message :: Maybe String
    , source :: Maybe String
    , reason :: Maybe String
    , data :: Maybe String
    , slaveId :: Maybe SlaveID
    , executorId :: Maybe ExecutorID
    , timestamp :: Maybe Number
    , uuid :: Maybe String
    , healthy :: Maybe Boolean
    , labels :: Maybe Labels
    , containerStatus :: Maybe ContainerStatus
    }

instance taskStatusAsForeign :: AsForeign TaskStatus where
    write (TaskStatus obj) = writeObject props where
        props = [ "task_id" .= write obj.taskId
                , "state" .= write obj.state
                , "message" .= (write $ Undefined obj.message)
                , "source" .= (write $ Undefined obj.source)
                , "reason" .= (write $ Undefined obj.reason)
                , "data" .= (write $ Undefined obj.data)
                , "agent_id" .= (write $ Undefined obj.slaveId)
                , "executor_id" .= (write $ Undefined obj.executorId)
                , "timestamp" .= (write $ Undefined obj.timestamp)
                , "uuid" .= (write $ Undefined obj.uuid)
                , "healthy" .= (write $ Undefined obj.healthy)
                , "labels" .= (write $ Undefined obj.labels)
                , "container_status" .= (write $ Undefined obj.containerStatus)
                ]

instance taskStatusIsForeign :: IsForeign TaskStatus where
    read obj = do
        taskId <- readProp "task_id" obj
        state <- readProp "state" obj
        message <- readPropNU "message" obj
        source <- readPropNU "source" obj
        reason <- readPropNU "reason" obj
        d <- readPropNU "data" obj
        slaveId <- catchError (readPropNU "slave_id" obj) \_ -> readPropNU "agent_id" obj
        executorId <- readPropNU "executor_id" obj
        timestamp <- readPropNU "timestamp" obj
        uuid <- readPropNU "uuid" obj
        healthy <- readPropNU "healthy" obj
        labels <- readPropNU "labels" obj
        containerStatus <- readPropNU "conainer_status" obj
        pure <<< TaskStatus $
            { taskId: taskId
            , state: state
            , message: message
            , source: source
            , reason: reason
            , data: d
            , slaveId: slaveId
            , executorId: executorId
            , timestamp: timestamp
            , uuid: uuid
            , healthy: healthy
            , labels: labels
            , containerStatus: containerStatus
            }

newtype Range = Range
    { begin :: Int
    , end :: Int
    }

instance rangeAsForeign :: AsForeign Range where
    write (Range obj) = toForeign obj

instance rangeIsForeign :: IsForeign Range where
    read obj = do
        begin <- readProp "begin" obj
        end <- readProp "end" obj
        pure $ Range { begin: begin
                     , end: end
                     }

newtype Ranges = Ranges (Array Range)

instance rangesAsForeign :: AsForeign Ranges where
    write (Ranges ranges) = toForeign $ { range: write ranges }

instance rangesIsForeign :: IsForeign Ranges where
    read obj = Ranges <$> readProp "range" obj

newtype Set = Set (Array String)

instance setAsForeign :: AsForeign Set where
    write (Set items) = toForeign $ { item: items }

instance setIsForeign :: IsForeign Set where
    read obj = Set <$> readProp "item" obj

data AttributeValue = ScalarAttribute Scalar
                    | RangesAttribute Ranges
                    | SetAttribute Set
                    | TextAttribute Text

data Attribute = Attribute String AttributeValue

instance attributeAsForeign :: AsForeign Attribute where
    write (Attribute name (ScalarAttribute s)) = toForeign $
        { name: name
        , type: "SCALAR"
        , scalar: write s
        }
    write (Attribute name (RangesAttribute ranges)) = toForeign $
        { name: name
        , type: "RANGES"
        , ranges: write ranges
        }
    write (Attribute name (SetAttribute set)) = toForeign $
        { name: name
        , type: "SET"
        , set: write set
        }
    write (Attribute name (TextAttribute text)) = toForeign $
        { name: name
        , type: "TEXT"
        , text: write text
        }

instance attributeIsForeign :: IsForeign Attribute where
    read obj = do
        name <- readProp "name" obj
        v <- readProp "type" obj >>= readAttributeType
        pure $ Attribute name v
        where
            readAttributeType "SCALAR" = ScalarAttribute <$> readProp "scalar" obj
            readAttributeType "RANGES" = RangesAttribute <$> readProp "ranges" obj
            readAttributeType "SET" = SetAttribute <$> readProp "set" obj
            readAttributeType "TEXT" = TextAttribute <$> readProp "text" obj
            readAttributeType t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown attribute type " <> show t

data ResourceValue = ScalarResource Scalar
                   | RangesResource Ranges
                   | SetResource Set

data Resource = Resource String ResourceValue

instance resourceAsForeign :: AsForeign Resource where
    write (Resource name (ScalarResource s)) = toForeign $
        { name: name
        , type: "SCALAR"
        , scalar: write s
        }
    write (Resource name (RangesResource ranges)) = toForeign $
        { name: name
        , type: "RANGES"
        , ranges: write ranges
        }
    write (Resource name (SetResource set)) = toForeign $
        { name: name
        , type: "SET"
        , set: write set
        }

instance resourceIsForiegn :: IsForeign Resource where
    read obj = do
        name <- readProp "name" obj
        v <- readProp "type" obj >>= readResourceType
        pure $ Resource name v
        where
            readResourceType "SCALAR" = ScalarResource <$> readProp "scalar" obj
            readResourceType "RANGES" = RangesResource <$> readProp "ranges" obj
            readResourceType "SET" = SetResource <$> readProp "set" obj
            readResourceType t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown resource type " <> show t

newtype Address = Address
    { hostname :: Maybe String
    , ip :: Maybe String
    , port :: Int
    }

instance addressAsForeign :: AsForeign Address where
    write (Address obj) = writeObject props where
        props = [ "hostname" .= (write $ Undefined obj.hostname)
                , "ip" .= (write $ Undefined obj.ip)
                , "port" .= write obj.port
                ]

instance addressIsForeign :: IsForeign Address where
    read obj = do
        hostname <- readPropNU "hostname" obj
        ip <- readPropNU "ip" obj
        port <- readProp "port" obj
        pure $ Address { hostname: hostname
                       , ip: ip
                       , port: port
                       }

newtype URL = URL
    { scheme :: String
    , address :: Address
    , path :: Maybe String
    , query :: Array Parameter
    , fragment :: Maybe String
    }

instance urlAsForeign :: AsForeign URL where
    write (URL obj) = writeObject props where
        props = [ "scheme" .= write obj.scheme
                , "address" .= write obj.address
                , "path" .= (write $ Undefined obj.path)
                , "query" .= write obj.query
                , "fragment" .= (write $ Undefined obj.fragment)
                ]

instance urlIsForeign :: IsForeign URL where
    read obj = do
        scheme <- readProp "scheme" obj
        address <- readProp "address" obj
        path <- readPropNU "path" obj
        query <- readPropNUM "query" obj
        fragment <- readPropNU "fragment" obj
        pure <<< URL $
            { scheme: scheme
            , address: address
            , path: path
            , query: query
            , fragment: fragment
            }

newtype Offer = Offer
    { id :: OfferID
    , frameworkId :: FrameworkID
    , slaveId :: SlaveID
    , hostname :: String
    , url :: Maybe URL
    , resources :: Array Resource
    , attributes :: Array Attribute
    , executorIds :: Array ExecutorID
    }

instance offerAsForeign :: AsForeign Offer where
    write (Offer obj) = writeObject props where
        props = [ "id" .= write obj.id
                , "framework_id" .= write obj.frameworkId
                , "agent_id" .= write obj.slaveId
                , "hostname" .= write obj.hostname
                , "url" .= (write $ Undefined obj.url)
                , "resources" .= write obj.resources
                , "attributes" .= write obj.attributes
                , "executor_ids" .= write obj.executorIds
                ]

instance offerIsForeign :: IsForeign Offer where
    read obj = do
        id <- readProp "id" obj
        frameworkId <- readProp "framework_id" obj
        slaveId <- catchError (readProp "slave_id" obj) \_ -> readProp "agent_id" obj
        hostname <- readProp "hostname" obj
        url <- readPropNU "url" obj
        resources <- readPropNUM "resources" obj
        attributes <- readPropNUM "attributes" obj
        executorIds <- readPropNUM "executor_ids" obj
        pure <<< Offer $
            { id: id
            , frameworkId: frameworkId
            , slaveId: slaveId
            , hostname: hostname
            , url: url
            , resources: resources
            , attributes: attributes
            , executorIds: executorIds
            }

newtype Value a = Value a

instance valueAsForeign :: (AsForeign a) => AsForeign (Value a) where
    write (Value v) = toForeign $ { value: write v }

instance valueIsForeign :: (IsForeign a) => IsForeign (Value a) where
    read obj = do
        v <- readProp "value" obj
        pure $ Value v

derive instance valueNewtype :: Newtype (Value a) _

instance valueEq :: (Eq a) => Eq (Value a) where
    eq a b = unwrap ((over2 Value eq a b) :: Identity Boolean)

instance valueOrd :: (Ord a) => Ord (Value a) where
    compare a b =  unwrap ((over2 Value compare a b) :: Identity Ordering)

type OfferID = Value String
type FrameworkID = Value String
type ExecutorID = Value String
type SlaveID = Value String
type AgentID = SlaveID
type TaskID = Value String
type Text = Value String
type Scalar = Value Number

newtype FrameworkInfoCapability = FrameworkInfoCapability { ty :: Int
                                                          }

instance frameworkInfoCapabilityAsForeign :: AsForeign FrameworkInfoCapability where
    write (FrameworkInfoCapability obj) = writeObject props where
        props = [ "type" .= write obj.ty
                ]

instance frameworkInfoCapabilityIsForeign :: IsForeign FrameworkInfoCapability where
    read obj = do
        ty <- readProp "type" obj
        pure $ FrameworkInfoCapability { ty: ty
                                       }

newtype Label = Label { key :: String
                      , value :: Maybe String
                      }

instance labelAsForeign :: AsForeign Label where
    write (Label obj) = writeObject props where
        props = [ "key" .= write obj.key
                , "value" .= (write $ Undefined obj.value)
                ]

instance labelIsForeign :: IsForeign Label where
    read obj = do
        key <- readProp "key" obj
        value <- readPropNU "value" obj
        pure $ Label { key: key
                     , value: value
                     }

newtype Labels = Labels { labels :: Array Label
                        }

instance labelsAsForeign :: AsForeign Labels where
    write (Labels obj) =
        toForeign { labels: write obj.labels
                  }

instance labelsIsForeign :: IsForeign Labels where
    read obj = do
        labels <- readProp "labels" obj
        pure $ Labels { labels: labels
                      }

newtype FrameworkInfo = FrameworkInfo { user :: String
                                      , name :: String
                                      , id :: Maybe FrameworkID
                                      , failoverTimeout :: Maybe Number
                                      , checkpoint :: Maybe Boolean
                                      , role :: Maybe String
                                      , hostname :: Maybe String
                                      , principal :: Maybe String
                                      , webuiUrl :: Maybe String
                                      , capabilities :: Array FrameworkInfoCapability
                                      , labels :: Maybe Labels
                                      }

instance frameworkInfoAsForeign :: AsForeign FrameworkInfo where
    write (FrameworkInfo obj) = writeObject props where
        props = [ "user" .= write obj.user
                , "name" .= write obj.name
                , "id" .= (write $ Undefined obj.id)
                , "failover_timeout" .= (write $ Undefined obj.failoverTimeout)
                , "checkpoint" .= (write $ Undefined obj.checkpoint)
                , "role" .= (write $ Undefined obj.role)
                , "hostname" .= (write $ Undefined obj.hostname)
                , "principal" .= (write $ Undefined obj.principal)
                , "webui_url" .= (write $ Undefined obj.webuiUrl)
                , "capabilities" .= write obj.capabilities
                , "labels" .= (write $ Undefined obj.labels)
                ]

module Mesos.Raw where

import Prelude
import Control.Alt ((<|>))
import Control.Monad.Error.Class (catchError, throwError)
import Data.Either (Either(..))
import Data.Foreign (F, Foreign, ForeignError(..), toForeign, writeObject, isNull, isUndefined, unsafeFromForeign)
import Data.Foreign.Class (class AsForeign, class IsForeign, read, write, readProp, (.=))
import Data.Foreign.Index (prop)
import Data.Foreign.NullOrUndefined (readNullOrUndefined, unNullOrUndefined)
import Data.Foreign.Undefined (Undefined(..))
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Monoid (class Monoid, mempty)
import Data.Newtype (class Newtype)
import Data.List.NonEmpty as NEL

readPropNU :: forall a. (IsForeign a) => String -> Foreign -> F (Maybe a)
readPropNU p v = unNullOrUndefined <$> (prop p v >>= readNullOrUndefined read)

readPropNUM :: forall a. (IsForeign a, Monoid a) => String -> Foreign -> F a
readPropNUM p v = fromMaybe mempty <$> readPropNU p v

isNullOrUndefined :: Foreign -> Boolean
isNullOrUndefined = isNull || isUndefined

-- | TODO: actually implement this
foreign import data HealthCheck :: *

instance healthCheckAsForeign :: AsForeign HealthCheck where
    write = toForeign

instance healthCheckIsForeign :: IsForeign HealthCheck where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data DiscoveryInfo :: *

instance discoveryInfoAsForeign :: AsForeign DiscoveryInfo where
    write = toForeign

instance discoveryInfoIsForeign :: IsForeign DiscoveryInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data LinuxInfo :: *

instance linuxInfoAsForeign :: AsForeign LinuxInfo where
    write = toForeign

instance linuxInfoIsForeign :: IsForeign LinuxInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data NetworkInfo :: *

instance networkInfoAsForeign :: AsForeign NetworkInfo where
    write = toForeign

instance networkInfoIsForeign :: IsForeign NetworkInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data TaskGroupInfo :: *

instance taskGroupInfoAsForeign :: AsForeign TaskGroupInfo where
    write = toForeign

instance taskGroupInfoIsForeign :: IsForeign TaskGroupInfo where
    read = pure <<< unsafeFromForeign

-- | TODO: actually implement this
foreign import data DockerInfo :: *

instance dockerInfoAsForeign :: AsForeign DockerInfo where
    write = toForeign

instance dockerInfoIsForeign :: IsForeign DockerInfo where
    read = pure <<< unsafeFromForeign

data VolumeMode = ReadWrite
                | ReadOnly

instance volumeModeAsForeign :: AsForeign VolumeMode where
    write ReadWrite = toForeign "RW"
    write ReadOnly = toForeign "RO"

instance volumeModeIsForeign :: IsForeign VolumeMode where
    read obj = do
        s <- read obj
        case s of
          "RW" -> pure ReadWrite
          "RO" -> pure ReadOnly
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown volume mode: " <> t

data Parameter = Parameter String String

instance parameterAsForeign :: AsForeign Parameter where
    write (Parameter name value) = toForeign { name: name, value: value }

instance parameterIsForeign :: IsForeign Parameter where
    read obj = do
        name <- readProp "name" obj
        value <- readProp "value" obj
        pure $ Parameter name value

newtype Parameters = Parameters (Array Parameter)

instance parametersAsForeign :: AsForeign Parameters where
    write (Parameters params) = toForeign { parameter: write params }

instance parametersIsForeign :: IsForeign Parameters where
    read obj = do
        params <- readProp "parameter" obj
        pure $ Parameters params

newtype VolumeSourceDocker = VolumeSourceDocker
    { driver :: Maybe String
    , name :: String
    , driverOptions :: Maybe Parameters
    }

instance volumeSourceDockerAsForeign :: AsForeign VolumeSourceDocker where
    write (VolumeSourceDocker obj) = writeObject props where
        props = [ "driver" .= (write $ Undefined obj.driver)
                , "name" .= write obj.name
                , "driver_options" .= (write $ Undefined obj.driverOptions)
                ]

instance volumeSourceDockerIsForeign :: IsForeign VolumeSourceDocker where
    read obj = do
        driver <- readPropNU "driver" obj
        name <- readProp "name" obj
        driverOptions <- readPropNU "driver_options" obj
        pure <<< VolumeSourceDocker $
            { driver: driver
            , name: name
            , driverOptions: driverOptions
            }

newtype VolumeSourceSandbox = VolumeSourceSandbox
    { type :: String
    , path :: String
    }

instance volumeSourceSandboxAsForeign :: AsForeign VolumeSourceSandbox where
    write (VolumeSourceSandbox obj) = toForeign obj

instance volumeSourceSandboxIsForeign :: IsForeign VolumeSourceSandbox where
    read obj = do
        t <- readProp "type" obj
        path <- readProp "path" obj
        pure <<< VolumeSourceSandbox $
            { type: t
            , path: path
            }

newtype Volume = Volume
    { mode :: VolumeMode
    , containerPath :: String
    , hostPath :: Maybe String
    , image :: Maybe Image
    , source :: Maybe (Either VolumeSourceDocker VolumeSourceSandbox)
    }

instance volumeAsForeign :: AsForeign Volume where
    write (Volume obj) = writeObject props where
        props = sourceProps <> [ "mode" .= write obj.mode
                               , "container_path" .= write obj.containerPath
                               , "host_path" .= (write $ Undefined obj.hostPath)
                               , "image" .= (write $ Undefined obj.image)
                               ]
        sourceProps =
            case obj.source of
              Nothing -> []
              Just (Left docker) -> [ "source" .= toForeign { type: "DOCKER_VOLUME", docker_volume: write docker } ]
              Just (Right sandbox) -> [ "source" .= toForeign { type: "SANDBOX_PATH", sandbox_path: write sandbox } ]

instance volumeIsForeign :: IsForeign Volume where
    read obj = do
        mode <- readProp "mode" obj
        containerPath <- readProp "container_path" obj
        hostPath <- readPropNU "host_path" obj
        image <- readPropNU "image" obj
        maybeSource <- catchError (Just <$> prop "source" obj) \_ -> pure Nothing
        let filteredForeignSource =
                do
                    foreignSource <- maybeSource
                    if isNullOrUndefined foreignSource
                       then Nothing
                       else Just foreignSource
        source <- case filteredForeignSource of
                    Nothing -> pure Nothing
                    Just f -> Just <$> readSource f
        pure <<< Volume $
            { mode: mode
            , containerPath: containerPath
            , hostPath: hostPath
            , image: image
            , source: source
            }
        where
            readSource :: Foreign -> F (Either VolumeSourceDocker VolumeSourceSandbox)
            readSource sObj = do
                ty <- readProp "type" sObj
                case ty of
                  "DOCKER_VOLUME" -> Left <$> readProp "docker_volume" sObj
                  "SANDBOX_PATH" -> Right <$> readProp "sandbox_path" sObj
                  t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown volume source type: " <> t

newtype Credential = Credential
    { principal :: String
    , secret :: Maybe String
    }

instance credentialAsForeign :: AsForeign Credential where
    write (Credential obj) = writeObject props where
        props = [ "principal" .= write obj.principal
                , "secret" .= (write $ Undefined obj.secret)
                ]

instance credentialIsForeign :: IsForeign Credential where
    read obj = do
        principal <- readProp "principal" obj
        secret <- readPropNU "secret" obj
        pure <<< Credential $
            { principal: principal
            , secret: secret
            }

newtype DockerImage = DockerImage
    { name :: String
    , credential :: Maybe Credential
    }

instance dockerImageAsForeign :: AsForeign DockerImage where
    write (DockerImage obj) = writeObject props where
        props = [ "name" .= write obj.name
                , "credential" .= (write $ Undefined obj.credential)
                ]

instance dockerImageIsForeign :: IsForeign DockerImage where
    read obj = do
        name <- readProp "name" obj
        credential <- readPropNU "credential" obj
        pure <<< DockerImage $
            { name: name
            , credential: credential
            }

-- | TODO: Actually implement this
foreign import data AppcImage :: *

instance appcImageAsForeign :: AsForeign AppcImage where
    write = toForeign

instance appcImageIsForeign :: IsForeign AppcImage where
    read = pure <<< unsafeFromForeign

newtype Image = Image
    { image :: Either AppcImage DockerImage
    , cached :: Boolean
    }

instance imageAsForeign :: AsForeign Image where
    write (Image obj) = writeObject props where
        props = imageProps <> [ "cached" .= write obj.cached
                              ]
        imageProps =
            case obj.image of
              Left appc -> [ "type" .= toForeign "APPC"
                           , "appc" .= write appc
                           ]
              Right docker -> [ "type" .= toForeign "DOCKER"
                              , "docker" .= write docker
                              ]

instance imageIsForeign :: IsForeign Image where
    read obj = do
        cached <- fromMaybe true <$> readPropNU "cached" obj
        image <- readProp "type" obj >>= readImage
        pure <<< Image $
            { image: image
            , cached: cached
            }
        where
            readImage :: String -> F (Either AppcImage DockerImage)
            readImage "APPC" = Left <$> readProp "appc" obj
            readImage "DOCKER" = Right <$> readProp "docker" obj
            readImage t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown image type: " <> t

data ExecutorInfoType = DefaultExecutor
                      | CustomExecutor

instance executorInfoTypeAsForeign :: AsForeign ExecutorInfoType where
    write (DefaultExecutor) = toForeign "DEFAULT"
    write (CustomExecutor) = toForeign "CUSTOM"

instance executorInfoTypeIsForeign :: IsForeign ExecutorInfoType where
    read obj = do
        s <- read obj
        case s of
          "DEFAULT" -> pure DefaultExecutor
          "CUSTOM" -> pure CustomExecutor
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown executor info type: " <> t

data EnvironmentVariable = EnvironmentVariable String String

instance environmentVariableAsForeign :: AsForeign EnvironmentVariable where
    write (EnvironmentVariable name value) = toForeign { name: name, value: value }

instance environmentVariableIsForeign :: IsForeign EnvironmentVariable where
    read obj = do
        name <- readProp "name" obj
        value <- readProp "value" obj
        pure $ EnvironmentVariable name value

newtype Environment = Environment (Array EnvironmentVariable)

instance environmentAsForeign :: AsForeign Environment where
    write (Environment vars) = toForeign $
        { variables: write vars
        }

instance environmentIsForeign :: IsForeign Environment where
    read obj = do
        variables <- readPropNUM "variables" obj
        pure $ Environment variables

newtype CommandInfoURI = CommandInfoURI
    { value :: String
    , executable :: Maybe Boolean
    , extract :: Maybe Boolean
    , cache :: Maybe Boolean
    , outputFile :: Maybe String
    }

instance commandInfoURIAsForeign :: AsForeign CommandInfoURI where
    write (CommandInfoURI obj) = writeObject props where
        props = [ "value" .= write obj.value
                , "executable" .= (write $ Undefined obj.executable)
                , "extract" .= (write $ Undefined obj.extract)
                , "cache" .= (write $ Undefined obj.cache)
                , "output_file" .= (write $ Undefined obj.outputFile)
                ]

instance commandInfoURIIsForeign :: IsForeign CommandInfoURI where
    read obj = do
        value <- readProp "value" obj
        executable <- readPropNU "executable" obj
        extract <- readPropNU "extract" obj
        cache <- readPropNU "cache" obj
        outputFile <- readPropNU "output_file" obj
        pure <<< CommandInfoURI $
            { value: value
            , executable: executable
            , extract: extract <|> Just true
            , cache: cache
            , outputFile: outputFile
            }

newtype CommandInfo = CommandInfo
    { uris :: Array CommandInfoURI
    , environment :: Maybe Environment
    , shell :: Boolean
    , value :: Maybe String
    , arguments :: Array String
    , user :: Maybe String
    }

instance commandInfoAsForeign :: AsForeign CommandInfo where
    write (CommandInfo obj) = writeObject props where
        props = [ "uris" .= write obj.uris
                , "environment" .= (write $ Undefined obj.environment)
                , "shell" .= write obj.shell
                , "value" .= (write $ Undefined obj.value)
                , "arguments" .= write obj.arguments
                , "user" .= (write $ Undefined obj.user)
                ]

instance commandInfoIsForeign :: IsForeign CommandInfo where
    read obj = do
        uris <- readPropNUM "uris" obj
        environment <- readPropNU "environment" obj
        shell <- readPropNU "shell" obj
        value <- readPropNU "value" obj
        arguments <- readPropNUM "arguments" obj
        user <- readPropNU "user" obj
        pure <<< CommandInfo $
            { uris: uris
            , environment: environment
            , shell: fromMaybe true shell
            , value: value
            , arguments: arguments
            , user: user
            }

newtype DurationInfo = DurationInfo Int

instance durationInfoAsForeign :: AsForeign DurationInfo where
    write (DurationInfo nanoseconds) = toForeign { nanoseconds: nanoseconds }

instance durationInfoIsForeign :: IsForeign DurationInfo where
    read obj = do
        nanoseconds <- readProp "nanoseconds" obj
        pure $ DurationInfo nanoseconds

data ContainerType = DockerContainer
                   | MesosContainer

instance containerTypeAsForeign :: AsForeign ContainerType where
    write DockerContainer = toForeign "DOCKER"
    write MesosContainer = toForeign "MESOS"

instance containerTypeIsForeign :: IsForeign ContainerType where
    read obj = do
        s <- read obj
        case s of
          "DOCKER" -> pure DockerContainer
          "MESOS" -> pure MesosContainer
          t -> throwError <<< NEL.singleton <<< ForeignError $ "Unknown container info type: " <> t

newtype MesosInfo = MesosInfo
    { image :: Image
    }

instance mesosInfoAsForeign :: AsForeign MesosInfo where
    write (MesosInfo obj) = toForeign $ { image: write obj.image }

instance mesosInfoIsForeign :: IsForeign MesosInfo where
    read obj = do
        image <- readProp "image" obj
        pure <<< MesosInfo $
            { image: image
            }

newtype ContainerInfo = ContainerInfo
    { type :: ContainerType
    , volumes :: Array Volume
    , hostname :: Maybe String
    , docker :: Maybe DockerInfo
    , mesos :: Maybe MesosInfo
    , networkInfos :: Array NetworkInfo
    , linuxInfo :: Maybe LinuxInfo
    }

instance containerInfoAsForeign :: AsForeign ContainerInfo where
    write (ContainerInfo obj) = writeObject props where
        props = [ "type" .= write obj.type
                , "volumes" .= write obj.volumes
                , "hostname" .= (write $ Undefined obj.hostname)
                , "docker" .= (write $ Undefined obj.docker)
                , "mesos" .= (write $ Undefined obj.mesos)
                , "network_infos" .= write obj.networkInfos
                , "linux_info" .= (write <<< Undefined) obj.linuxInfo
                ]

instance containerInfoIsForeign :: IsForeign ContainerInfo where
    read obj = do
        t <- readProp "type" obj
        volumes <- readPropNUM "volumes" obj
        hostname <- readPropNU "hostname" obj
        docker <- readPropNU "docker" obj
        mesos <- readPropNU "mesos" obj
        networkInfos <- readPropNUM "network_infos" obj
        linuxInfo <- readPropNU "linux_info" obj
        pure <<< ContainerInfo $
            { type: t
            , volumes: volumes
            , hostname: hostname
            , docker: docker
            , mesos: mesos
            , networkInfos: networkInfos
            , linuxInfo: linuxInfo
            }

newtype ExecutorInfo = ExecutorInfo
    { type :: ExecutorInfoType
    , executorId :: ExecutorID
    , frameworkId :: FrameworkID
    , command :: Maybe CommandInfo
    , container :: Maybe ContainerInfo
    , resources :: Array Resource
    , name :: Maybe String
    , data :: Maybe String
    , discovery :: Maybe DiscoveryInfo
    , shutdownGracePeriod :: Maybe DurationInfo
    , labels :: Maybe Labels
    }

instance executorInfoAsForeign :: AsForeign ExecutorInfo where
    write (ExecutorInfo obj) = writeObject props where
        props = [ "type" .= write obj.type
                , "executor_id" .= write obj.executorId
                , "framework_id" .= write obj.frameworkId
                , "command" .= (write $ Undefined obj.command)
                , "container" .= (write $ Undefined obj.container)
                , "resources" .= write obj.resources
                , "name" .= (write $ Undefined obj.name)
                , "data" .= (write $ Undefined obj.data)
                , "discovery" .= (write $ Undefined obj.discovery)
                , "shutdown_grace_period" .= (write $ Undefined obj.shutdownGracePeriod)
                , "labels" .= (write $ Undefined obj.labels)
                ]

instance executorInfoIsForeign :: IsForeign ExecutorInfo where
    read obj = do
        t <- readProp "type" obj
        executorId <- readProp "executor_id" obj
        frameworkId <- readProp "framework_id" obj
        command <- readPropNU "command" obj
        container <- readPropNU "container" obj
        resources <- readPropNUM "resources" obj
        name <- readPropNU "name" obj
        d <- readPropNU "data" obj
        discovery <- readPropNU "discovery" obj
        shutdownGracePeriod <- readPropNU "shutdown_grace_period" obj
        labels <- readPropNU "labels" obj
        pure <<< ExecutorInfo $
            { type: t
            , executorId: executorId
            , frameworkId: frameworkId
            , command: command
            , container: container
            , resources: resources
            , name: name
            , data: d
            , discovery: discovery
            , shutdownGracePeriod: shutdownGracePeriod
            , labels: labels
            }

newtype KillPolicy = KillPolicy
    { gracePeriod :: Maybe DurationInfo
    }

instance killPolicyAsForeign :: AsForeign KillPolicy where
    write (KillPolicy obj) = writeObject props where
        props = [ "grace_period" .= (write $ Undefined obj.gracePeriod)
                ]

instance killPolicyIsForeign :: IsForeign KillPolicy where
    read obj = do
        gracePeriod <- readPropNU "grace_period" obj
        pure <<< KillPolicy $
            { gracePeriod: gracePeriod
            }

newtype TaskInfo = TaskInfo
    { name :: String
    , taskId :: TaskID
    , slaveId :: SlaveID
    , resources :: Array Resource
    , executor :: Maybe ExecutorInfo
    , command :: Maybe CommandInfo
    , container :: Maybe ContainerInfo
    , healthCheck :: Maybe HealthCheck
    , killPolicy :: Maybe KillPolicy
    , data :: Maybe String
    , labels :: Maybe Labels
    , discoveryInfo :: Maybe DiscoveryInfo
    }

instance taskInfoAsForeign :: AsForeign TaskInfo where
    write (TaskInfo obj) = writeObject props where
        props = [ "name" .= write obj.name
                , "task_id" .= write obj.taskId
                , "agent_id" .= write obj.slaveId
                , "resources" .= write obj.resources
                , "executor" .= (write $ Undefined obj.executor)
                , "command" .= (write $ Undefined obj.command)
                , "container" .= (write $ Undefined obj.container)
                , "health_check" .= (write $ Undefined obj.healthCheck)
                , "kill_policy" .= (write $ Undefined obj.killPolicy)
                , "data" .= (write $ Undefined obj.data)
                , "labels" .= (write $ Undefined obj.labels)
                , "discovery_info" .= (write $ Undefined obj.discoveryInfo)
                ]

instance taskInfoIsForeign :: IsForeign TaskInfo where
    read obj = do
        name <- readProp "name" obj
        taskId <- readProp "task_id" obj
        slaveId <- readProp "slave_id" obj <|> readProp "agent_id" obj
        resources <- readPropNUM "resources" obj
        executor <- readPropNU "executor" obj
        command <- readPropNU "command" obj
        container <- readPropNU "container" obj
        healthCheck <- readPropNU "health_check" obj
        killPolicy <- readPropNU "kill_policy" obj
        d <- readPropNU "data" obj
        labels <- readPropNU "labels" obj
        discoveryInfo <- readPropNU "discovery_info" obj
        pure <<< TaskInfo $
            { name: name
            , taskId: taskId
            , slaveId: slaveId
            , resources: resources
            , executor: executor
            , command: command
            , container: container
            , healthCheck: healthCheck
            , killPolicy: killPolicy
            , data: d
            , labels: labels
            , discoveryInfo: discoveryInfo
            }

derive instance taskInfoNewtype :: Newtype TaskInfo _

newtype Filters = Filters
    { refuseSeconds :: Maybe Number
    }

instance filtersAsForeign :: AsForeign Filters where
    write (Filters obj) = toForeign $ { refuse_seconds: write $ Undefined obj.refuseSeconds }

instance filtersIsForeign :: IsForeign Filters where
    read obj = do
        refuseSeconds <- readPropNU "refuse_seconds" obj
        pure $ Filters { refuseSeconds: refuseSeconds
                       }

-- | TODO: actually implement this
foreign import data ContainerStatus :: *

instance containerStatusAsForeign :: AsForeign ContainerStatus where
    write = toForeign

instance containerStatusIsForeign :: IsForeign ContainerStatus where
    read = pure <<< unsafeFromForeign

newtype TaskStatus = TaskStatus
    { taskId :: TaskID
    , state :: String
    , message :: Maybe String
    , source :: Maybe String
    , reason :: Maybe String
    , data :: Maybe String
    , slaveId :: Maybe SlaveID
    , executorId :: Maybe ExecutorID
    , timestamp :: Maybe Number
    , uuid :: Maybe String
    , healthy :: Maybe Boolean
    , labels :: Maybe Labels
    , containerStatus :: Maybe ContainerStatus
    }

instance taskStatusAsForeign :: AsForeign TaskStatus where
    write (TaskStatus obj) = writeObject props where
        props = [ "task_id" .= write obj.taskId
                , "state" .= write obj.state
                , "message" .= (write $ Undefined obj.message)
                , "source" .= (write $ Undefined obj.source)
                , "reason" .= (write $ Undefined obj.reason)
                , "data" .= (write $ Undefined obj.data)
                , "agent_id" .= (write $ Undefined obj.slaveId)
                , "executor_id" .= (write $ Undefined obj.executorId)
                , "timestamp" .= (write $ Undefined obj.timestamp)
                , "uuid" .= (write $ Undefined obj.uuid)
                , "healthy" .= (write $ Undefined obj.healthy)
                , "labels" .= (write $ Undefined obj.labels)
                , "container_status" .= (write $ Undefined obj.containerStatus)
                ]

instance taskStatusIsForeign :: IsForeign TaskStatus where
    read obj = do
        taskId <- readProp "task_id" obj
        state <- readProp "state" obj
        message <- readPropNU "message" obj
        source <- readPropNU "source" obj
        reason <- readPropNU "reason" obj
        d <- readPropNU "data" obj
        slaveId <- catchError (readPropNU "slave_id" obj) \_ -> readPropNU "agent_id" obj
        executorId <- readPropNU "executor_id" obj
        timestamp <- readPropNU "timestamp" obj
        uuid <- readPropNU "uuid" obj
        healthy <- readPropNU "healthy" obj
        labels <- readPropNU "labels" obj
        containerStatus <- readPropNU "conainer_status" obj
        pure <<< TaskStatus $
            { taskId: taskId
            , state: state
            , message: message
            , source: source
            , reason: reason
            , data: d
            , slaveId: slaveId
            , executorId: executorId
            , timestamp: timestamp
            , uuid: uuid
            , healthy: healthy
            , labels: labels
            , containerStatus: containerStatus
            }

newtype Range = Range
    { begin :: Int
    , end :: Int
    }

instance rangeAsForeign :: AsForeign Range where
    write (Range obj) = toForeign obj

instance rangeIsForeign :: IsForeign Range where
    read obj = do
        begin <- readProp "begin" obj
        end <- readProp "end" obj
        pure $ Range { begin: begin
                     , end: end
                     }

newtype Ranges = Ranges (Array Range)

instance rangesAsForeign :: AsForeign Ranges where
    write (Ranges ranges) = toForeign $ { range: write ranges }

instance rangesIsForeign :: IsForeign Ranges where
    read obj = Ranges <$> readProp "range" obj

newtype Set = Set (Array String)

instance setAsForeign :: AsForeign Set where
    write (Set items) = toForeign $ { item: items }

instance setIsForeign :: IsForeign Set where
    read obj = Set <$> readProp "item" obj

data AttributeValue = ScalarAttribute Scalar
                    | RangesAttribute Ranges
                    | SetAttribute Set
                    | TextAttribute Text

data Attribute = Attribute String AttributeValue

instance attributeAsForeign :: AsForeign Attribute where
    write (Attribute name (ScalarAttribute s)) = toForeign $
        { name: name
        , type: "SCALAR"
        , scalar: write s
        }
    write (Attribute name (RangesAttribute ranges)) = toForeign $
        { name: name
        , type: "RANGES"
        , ranges: write ranges
        }
    write (Attribute name (SetAttribute set)) = toForeign $
        { name: name
        , type: "SET"
        , set: write set
        }
    write (Attribute name (TextAttribute text)) = toForeign $
        { name: name
        , type: "TEXT"
        , text: write text
        }

instance attributeIsForeign :: IsForeign Attribute where
    read obj = do
        name <- readProp "name" obj
        v <- readProp "type" obj >>= readAttributeType
        pure $ Attribute name v
        where
            readAttributeType "SCALAR" = ScalarAttribute <$> readProp "scalar" obj
            readAttributeType "RANGES" = RangesAttribute <$> readProp "ranges" obj
            readAttributeType "SET" = SetAttribute <$> readProp "set" obj
            readAttributeType "TEXT" = TextAttribute <$> readProp "text" obj
            readAttributeType t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown attribute type " <> show t

data ResourceValue = ScalarResource Scalar
                   | RangesResource Ranges
                   | SetResource Set

data Resource = Resource String ResourceValue

instance resourceAsForeign :: AsForeign Resource where
    write (Resource name (ScalarResource s)) = toForeign $
        { name: name
        , type: "SCALAR"
        , scalar: write s
        }
    write (Resource name (RangesResource ranges)) = toForeign $
        { name: name
        , type: "RANGES"
        , ranges: write ranges
        }
    write (Resource name (SetResource set)) = toForeign $
        { name: name
        , type: "SET"
        , set: write set
        }

instance resourceIsForiegn :: IsForeign Resource where
    read obj = do
        name <- readProp "name" obj
        v <- readProp "type" obj >>= readResourceType
        pure $ Resource name v
        where
            readResourceType "SCALAR" = ScalarResource <$> readProp "scalar" obj
            readResourceType "RANGES" = RangesResource <$> readProp "ranges" obj
            readResourceType "SET" = SetResource <$> readProp "set" obj
            readResourceType t = throwError <<< NEL.singleton <<< ForeignError $ "Unknown resource type " <> show t

newtype Address = Address
    { hostname :: Maybe String
    , ip :: Maybe String
    , port :: Int
    }

instance addressAsForeign :: AsForeign Address where
    write (Address obj) = writeObject props where
        props = [ "hostname" .= (write $ Undefined obj.hostname)
                , "ip" .= (write $ Undefined obj.ip)
                , "port" .= write obj.port
                ]

instance addressIsForeign :: IsForeign Address where
    read obj = do
        hostname <- readPropNU "hostname" obj
        ip <- readPropNU "ip" obj
        port <- readProp "port" obj
        pure $ Address { hostname: hostname
                       , ip: ip
                       , port: port
                       }

newtype URL = URL
    { scheme :: String
    , address :: Address
    , path :: Maybe String
    , query :: Array Parameter
    , fragment :: Maybe String
    }

instance urlAsForeign :: AsForeign URL where
    write (URL obj) = writeObject props where
        props = [ "scheme" .= write obj.scheme
                , "address" .= write obj.address
                , "path" .= (write $ Undefined obj.path)
                , "query" .= write obj.query
                , "fragment" .= (write $ Undefined obj.fragment)
                ]

instance urlIsForeign :: IsForeign URL where
    read obj = do
        scheme <- readProp "scheme" obj
        address <- readProp "address" obj
        path <- readPropNU "path" obj
        query <- readPropNUM "query" obj
        fragment <- readPropNU "fragment" obj
        pure <<< URL $
            { scheme: scheme
            , address: address
            , path: path
            , query: query
            , fragment: fragment
            }

newtype Offer = Offer
    { id :: OfferID
    , frameworkId :: FrameworkID
    , slaveId :: SlaveID
    , hostname :: String
    , url :: Maybe URL
    , resources :: Array Resource
    , attributes :: Array Attribute
    , executorIds :: Array ExecutorID
    }

instance offerAsForeign :: AsForeign Offer where
    write (Offer obj) = writeObject props where
        props = [ "id" .= write obj.id
                , "framework_id" .= write obj.frameworkId
                , "agent_id" .= write obj.slaveId
                , "hostname" .= write obj.hostname
                , "url" .= (write $ Undefined obj.url)
                , "resources" .= write obj.resources
                , "attributes" .= write obj.attributes
                , "executor_ids" .= write obj.executorIds
                ]

instance offerIsForeign :: IsForeign Offer where
    read obj = do
        id <- readProp "id" obj
        frameworkId <- readProp "framework_id" obj
        slaveId <- catchError (readProp "slave_id" obj) \_ -> readProp "agent_id" obj
        hostname <- readProp "hostname" obj
        url <- readPropNU "url" obj
        resources <- readPropNUM "resources" obj
        attributes <- readPropNUM "attributes" obj
        executorIds <- readPropNUM "executor_ids" obj
        pure <<< Offer $
            { id: id
            , frameworkId: frameworkId
            , slaveId: slaveId
            , hostname: hostname
            , url: url
            , resources: resources
            , attributes: attributes
            , executorIds: executorIds
            }

newtype Value a = Value a

instance valueAsForeign :: (AsForeign a) => AsForeign (Value a) where
    write (Value v) = toForeign $ { value: write v }

instance valueIsForeign :: (IsForeign a) => IsForeign (Value a) where
    read obj = do
        v <- readProp "value" obj
        pure $ Value v

derive instance valueNewtype :: Newtype (Value a) _

type OfferID = Value String
type FrameworkID = Value String
type ExecutorID = Value String
type SlaveID = Value String
type AgentID = SlaveID
type TaskID = Value String
type Text = Value String
type Scalar = Value Number

newtype FrameworkInfoCapability = FrameworkInfoCapability { ty :: Int
                                                          }

instance frameworkInfoCapabilityAsForeign :: AsForeign FrameworkInfoCapability where
    write (FrameworkInfoCapability obj) = writeObject props where
        props = [ "type" .= write obj.ty
                ]

instance frameworkInfoCapabilityIsForeign :: IsForeign FrameworkInfoCapability where
    read obj = do
        ty <- readProp "type" obj
        pure $ FrameworkInfoCapability { ty: ty
                                       }

newtype Label = Label { key :: String
                      , value :: Maybe String
                      }

instance labelAsForeign :: AsForeign Label where
    write (Label obj) = writeObject props where
        props = [ "key" .= write obj.key
                , "value" .= (write $ Undefined obj.value)
                ]

instance labelIsForeign :: IsForeign Label where
    read obj = do
        key <- readProp "key" obj
        value <- readPropNU "value" obj
        pure $ Label { key: key
                     , value: value
                     }

newtype Labels = Labels { labels :: Array Label
                        }

instance labelsAsForeign :: AsForeign Labels where
    write (Labels obj) =
        toForeign { labels: write obj.labels
                  }

instance labelsIsForeign :: IsForeign Labels where
    read obj = do
        labels <- readProp "labels" obj
        pure $ Labels { labels: labels
                      }

newtype FrameworkInfo = FrameworkInfo { user :: String
                                      , name :: String
                                      , id :: Maybe FrameworkID
                                      , failoverTimeout :: Maybe Number
                                      , checkpoint :: Maybe Boolean
                                      , role :: Maybe String
                                      , hostname :: Maybe String
                                      , principal :: Maybe String
                                      , webuiUrl :: Maybe String
                                      , capabilities :: Array FrameworkInfoCapability
                                      , labels :: Maybe Labels
                                      }

instance frameworkInfoAsForeign :: AsForeign FrameworkInfo where
    write (FrameworkInfo obj) = writeObject props where
        props = [ "user" .= write obj.user
                , "name" .= write obj.name
                , "id" .= (write $ Undefined obj.id)
                , "failover_timeout" .= (write $ Undefined obj.failoverTimeout)
                , "checkpoint" .= (write $ Undefined obj.checkpoint)
                , "role" .= (write $ Undefined obj.role)
                , "hostname" .= (write $ Undefined obj.hostname)
                , "principal" .= (write $ Undefined obj.principal)
                , "webui_url" .= (write $ Undefined obj.webuiUrl)
                , "capabilities" .= write obj.capabilities
                , "labels" .= (write $ Undefined obj.labels)
                ]