{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveGeneric             #-}
{-# LANGUAGE MultiWayIf                #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE ScopedTypeVariables       #-}
-- Copyright 2022 United States Government as represented by the Administrator
-- of the National Aeronautics and Space Administration. All Rights Reserved.
--
-- Disclaimers
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may
-- not use this file except in compliance with the License. You may obtain a
-- copy of the License at
--
--      https://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-- License for the specific language governing permissions and limitations
-- under the License.
--
-- | Create <https://github.com/nasa/fprime FPrime> components that subscribe
-- to obtain data and call Copilot when new values arrive.

{- HLINT ignore "Functor law" -}
module Command.FPrimeApp
    ( command
    , CommandOptions(..)
    , ErrorCode
    )
  where

-- External imports
import           Control.Applicative    ( liftA2, (<|>) )
import qualified Control.Exception      as E
import           Control.Monad.Except   ( ExceptT(..), liftEither )
import           Data.Aeson             ( ToJSON, toJSON )
import           Data.Char              ( toUpper )
import           Data.Maybe             ( fromMaybe, mapMaybe, maybeToList )
import           GHC.Generics           ( Generic )

-- External imports: auxiliary
import System.Directory.Extra ( copyTemplate )

import qualified Command.Standalone

-- Internal imports: auxiliary
import Command.Result (Result (..))

-- Internal imports
import Command.Common
import Command.Errors     (ErrorCode, ErrorTriplet (..))
import Command.VariableDB (InputDef (..), TypeDef (..), VariableDB, findInput,
                           findType, findTypeByType)

-- | Generate a new FPrime component connected to Copilot.
command :: CommandOptions -- ^ Options to the ROS backend.
        -> IO (Result ErrorCode)
command :: CommandOptions -> IO (Result ErrorCode)
command CommandOptions
options = ExceptT ErrorTriplet IO () -> IO (Result ErrorCode)
forall (m :: * -> *) a.
Monad m =>
ExceptT ErrorTriplet m a -> m (Result ErrorCode)
processResult (ExceptT ErrorTriplet IO () -> IO (Result ErrorCode))
-> ExceptT ErrorTriplet IO () -> IO (Result ErrorCode)
forall a b. (a -> b) -> a -> b
$ do
    -- Obtain template dir
    templateDir <- Maybe FilePath -> FilePath -> ExceptT ErrorTriplet IO FilePath
forall e. Maybe FilePath -> FilePath -> ExceptT e IO FilePath
locateTemplateDir Maybe FilePath
mTemplateDir FilePath
"fprime"

    templateVars <- parseTemplateVarsFile templateVarsF

    appData <- command' options functions

    let subst = Value -> Value -> Value
mergeObjects (AppData -> Value
forall a. ToJSON a => a -> Value
toJSON AppData
appData) Value
templateVars

    -- Expand template
    ExceptT $ fmap (makeLeftE cannotCopyTemplate) $ E.try $
      copyTemplate templateDir subst targetDir

  where

    targetDir :: FilePath
targetDir     = CommandOptions -> FilePath
commandTargetDir CommandOptions
options
    mTemplateDir :: Maybe FilePath
mTemplateDir  = CommandOptions -> Maybe FilePath
commandTemplateDir CommandOptions
options
    functions :: ExprPair
functions     = FilePath -> ExprPair
exprPair (CommandOptions -> FilePath
commandPropFormat CommandOptions
options)
    templateVarsF :: Maybe FilePath
templateVarsF = CommandOptions -> Maybe FilePath
commandExtraVars CommandOptions
options

command' :: CommandOptions
         -> ExprPair
         -> ExceptT ErrorTriplet IO AppData
command' :: CommandOptions -> ExprPair -> ExceptT ErrorTriplet IO AppData
command' CommandOptions
options (ExprPair ExprPairT a
exprT) = do
    -- Open files needed to fill in details in the template.
    vs    <- Maybe FilePath -> ExceptT ErrorTriplet IO (Maybe [FilePath])
parseVariablesFile Maybe FilePath
varNameFile
    rs    <- parseRequirementsListFile handlersFile
    varDB <- openVarDBFilesWithDefault varDBFile

    specT <- maybe (return Nothing) (\FilePath
e -> Spec a -> Maybe (Spec a)
forall a. a -> Maybe a
Just (Spec a -> Maybe (Spec a))
-> ExceptT ErrorTriplet IO (Spec a)
-> ExceptT ErrorTriplet IO (Maybe (Spec a))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> ExceptT ErrorTriplet IO (Spec a)
parseInputExpr' FilePath
e) cExpr
    specF <- maybe (return Nothing) (\FilePath
f -> Spec a -> Maybe (Spec a)
forall a. a -> Maybe a
Just (Spec a -> Maybe (Spec a))
-> ExceptT ErrorTriplet IO (Spec a)
-> ExceptT ErrorTriplet IO (Maybe (Spec a))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> FilePath -> ExceptT ErrorTriplet IO (Spec a)
parseInputFile' FilePath
f) fp

    let spec = Maybe (Spec a)
specT Maybe (Spec a) -> Maybe (Spec a) -> Maybe (Spec a)
forall a. Maybe a -> Maybe a -> Maybe a
forall (f :: * -> *) a. Alternative f => f a -> f a -> f a
<|> Maybe (Spec a)
specF

    liftEither $ checkArguments spec vs rs

    copilotM <- sequenceA $ (\Spec a
spec' -> Spec a
-> Maybe FilePath
-> Maybe FilePath
-> ExceptT ErrorTriplet IO AppData
processSpec Spec a
spec' Maybe FilePath
fp Maybe FilePath
cExpr) <$> spec

    let varNames = [FilePath] -> Maybe [FilePath] -> [FilePath]
forall a. a -> Maybe a -> a
fromMaybe (Maybe (Spec a) -> [FilePath]
forall a. Maybe (Spec a) -> [FilePath]
specExtractExternalVariables Maybe (Spec a)
spec) Maybe [FilePath]
vs
        monitors = [(FilePath, Maybe FilePath)]
-> ([FilePath] -> [(FilePath, Maybe FilePath)])
-> Maybe [FilePath]
-> [(FilePath, Maybe FilePath)]
forall b a. b -> (a -> b) -> Maybe a -> b
maybe
                     (Maybe (Spec a) -> [(FilePath, Maybe FilePath)]
forall a. Maybe (Spec a) -> [(FilePath, Maybe FilePath)]
specExtractHandlers Maybe (Spec a)
spec)
                     ((FilePath -> (FilePath, Maybe FilePath))
-> [FilePath] -> [(FilePath, Maybe FilePath)]
forall a b. (a -> b) -> [a] -> [b]
map (\FilePath
x -> (FilePath
x, Maybe FilePath
forall a. Maybe a
Nothing)))
                     Maybe [FilePath]
rs

    let appData   = [VarDecl] -> [Monitor] -> Maybe AppData -> AppData
AppData [VarDecl]
variables [Monitor]
monitors' Maybe AppData
copilotM
        variables = (FilePath -> Maybe VarDecl) -> [FilePath] -> [VarDecl]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (VariableDB -> FilePath -> Maybe VarDecl
variableMap VariableDB
varDB) [FilePath]
varNames
        monitors' = ((FilePath, Maybe FilePath) -> Maybe Monitor)
-> [(FilePath, Maybe FilePath)] -> [Monitor]
forall a b. (a -> Maybe b) -> [a] -> [b]
mapMaybe (VariableDB -> (FilePath, Maybe FilePath) -> Maybe Monitor
monitorMap VariableDB
varDB) [(FilePath, Maybe FilePath)]
monitors

    return appData

  where

    cExpr :: Maybe FilePath
cExpr          = CommandOptions -> Maybe FilePath
commandConditionExpr CommandOptions
options
    fp :: Maybe FilePath
fp             = CommandOptions -> Maybe FilePath
commandInputFile CommandOptions
options
    varNameFile :: Maybe FilePath
varNameFile    = CommandOptions -> Maybe FilePath
commandVariables CommandOptions
options
    varDBFile :: [FilePath]
varDBFile      = Maybe FilePath -> [FilePath]
forall a. Maybe a -> [a]
maybeToList (Maybe FilePath -> [FilePath]) -> Maybe FilePath -> [FilePath]
forall a b. (a -> b) -> a -> b
$ CommandOptions -> Maybe FilePath
commandVariableDB CommandOptions
options
    handlersFile :: Maybe FilePath
handlersFile   = CommandOptions -> Maybe FilePath
commandHandlers CommandOptions
options
    formatName :: FilePath
formatName     = CommandOptions -> FilePath
commandFormat CommandOptions
options
    propFormatName :: FilePath
propFormatName = CommandOptions -> FilePath
commandPropFormat CommandOptions
options
    propVia :: Maybe FilePath
propVia        = CommandOptions -> Maybe FilePath
commandPropVia CommandOptions
options

    parseInputExpr' :: FilePath -> ExceptT ErrorTriplet IO (Spec a)
parseInputExpr' FilePath
e =
      FilePath
-> FilePath
-> Maybe FilePath
-> ExprPairT a
-> ExceptT ErrorTriplet IO (Spec a)
forall a.
FilePath
-> FilePath
-> Maybe FilePath
-> ExprPairT a
-> ExceptT ErrorTriplet IO (Spec a)
parseInputExpr FilePath
e FilePath
propFormatName Maybe FilePath
propVia ExprPairT a
exprT

    parseInputFile' :: FilePath -> ExceptT ErrorTriplet IO (Spec a)
parseInputFile' FilePath
f =
      FilePath
-> FilePath
-> FilePath
-> Maybe FilePath
-> ExprPairT a
-> ExceptT ErrorTriplet IO (Spec a)
forall a.
FilePath
-> FilePath
-> FilePath
-> Maybe FilePath
-> ExprPairT a
-> ExceptT ErrorTriplet IO (Spec a)
parseInputFile FilePath
f FilePath
formatName FilePath
propFormatName Maybe FilePath
propVia ExprPairT a
exprT

    processSpec :: Spec a
-> Maybe FilePath
-> Maybe FilePath
-> ExceptT ErrorTriplet IO AppData
processSpec Spec a
spec' Maybe FilePath
expr' Maybe FilePath
fp' =
      Maybe FilePath
-> Maybe FilePath
-> FilePath
-> [(FilePath, FilePath)]
-> ExprPairT a
-> Spec a
-> ExceptT ErrorTriplet IO AppData
forall a.
Maybe FilePath
-> Maybe FilePath
-> FilePath
-> [(FilePath, FilePath)]
-> ExprPairT a
-> Spec a
-> ExceptT ErrorTriplet IO AppData
Command.Standalone.commandLogic Maybe FilePath
expr' Maybe FilePath
fp' FilePath
"copilot" [] ExprPairT a
exprT Spec a
spec'

-- ** Argument processing

-- | Options used to customize the conversion of specifications to F'
-- applications.
data CommandOptions = CommandOptions
  { CommandOptions -> Maybe FilePath
commandConditionExpr :: Maybe String   -- ^ Trigger condition.
  , CommandOptions -> Maybe FilePath
commandInputFile   :: Maybe FilePath -- ^ Input specification file.
  , CommandOptions -> FilePath
commandTargetDir   :: FilePath       -- ^ Target directory where the
                                         -- component should be created.
  , CommandOptions -> Maybe FilePath
commandTemplateDir :: Maybe FilePath -- ^ Directory where the template is
                                         -- to be found.
  , CommandOptions -> Maybe FilePath
commandVariables   :: Maybe FilePath -- ^ File containing a list of
                                         -- variables to make available to
                                         -- Copilot.
  , CommandOptions -> Maybe FilePath
commandVariableDB  :: Maybe FilePath -- ^ File containing a list of known
                                         -- variables with their types and the
                                         -- message IDs they can be obtained
                                         -- from.
  , CommandOptions -> Maybe FilePath
commandHandlers    :: Maybe FilePath -- ^ File containing a list of
                                         -- handlers used in the Copilot
                                         -- specification. The handlers are
                                         -- assumed to receive no arguments.
  , CommandOptions -> FilePath
commandFormat      :: String         -- ^ Format of the input file.
  , CommandOptions -> FilePath
commandPropFormat  :: String         -- ^ Format used for input properties.
  , CommandOptions -> Maybe FilePath
commandPropVia     :: Maybe String   -- ^ Use external command to
                                         -- pre-process system properties.
  , CommandOptions -> Maybe FilePath
commandExtraVars   :: Maybe FilePath -- ^ File containing additional
                                         -- variables to make available to the
                                         -- template.
  }

-- | Return the variable information needed to generate declarations
-- and subscriptions for a given variable name and variable database.
variableMap :: VariableDB
            -> String
            -> Maybe VarDecl
variableMap :: VariableDB -> FilePath -> Maybe VarDecl
variableMap VariableDB
varDB FilePath
varName = do
  inputDef     <- VariableDB -> FilePath -> Maybe InputDef
findInput VariableDB
varDB FilePath
varName
  inputDefType <- inputType inputDef
  let typeDef = VariableDB -> FilePath -> FilePath -> FilePath -> Maybe TypeDef
findType VariableDB
varDB FilePath
varName FilePath
"fprime/port" FilePath
"C"

  portType <- maybe (inputType inputDef) (Just . typeFromType) typeDef

  return $ VarDecl varName inputDefType portType

-- | Return the monitor information needed to generate declarations and
-- publishers for the given monitor info, and variable database.
monitorMap :: VariableDB
           -> (String, Maybe String)
           -> Maybe Monitor
monitorMap :: VariableDB -> (FilePath, Maybe FilePath) -> Maybe Monitor
monitorMap VariableDB
varDB (FilePath
monitorName, Maybe FilePath
Nothing) =
  Monitor -> Maybe Monitor
forall a. a -> Maybe a
Just (Monitor -> Maybe Monitor) -> Monitor -> Maybe Monitor
forall a b. (a -> b) -> a -> b
$ FilePath -> FilePath -> Maybe FilePath -> Maybe FilePath -> Monitor
Monitor FilePath
monitorName ((Char -> Char) -> FilePath -> FilePath
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toUpper FilePath
monitorName) Maybe FilePath
forall a. Maybe a
Nothing Maybe FilePath
forall a. Maybe a
Nothing
monitorMap VariableDB
varDB (FilePath
monitorName, Just FilePath
ty) = do
  let tyPort :: FilePath
tyPort = FilePath -> (TypeDef -> FilePath) -> Maybe TypeDef -> FilePath
forall b a. b -> (a -> b) -> Maybe a -> b
maybe FilePath
ty TypeDef -> FilePath
typeFromType (Maybe TypeDef -> FilePath) -> Maybe TypeDef -> FilePath
forall a b. (a -> b) -> a -> b
$ VariableDB -> FilePath -> FilePath -> FilePath -> Maybe TypeDef
findTypeByType VariableDB
varDB FilePath
"fprime/port" FilePath
"C" FilePath
ty
  Monitor -> Maybe Monitor
forall a. a -> Maybe a
forall (m :: * -> *) a. Monad m => a -> m a
return (Monitor -> Maybe Monitor) -> Monitor -> Maybe Monitor
forall a b. (a -> b) -> a -> b
$ FilePath -> FilePath -> Maybe FilePath -> Maybe FilePath -> Monitor
Monitor FilePath
monitorName ((Char -> Char) -> FilePath -> FilePath
forall a b. (a -> b) -> [a] -> [b]
map Char -> Char
toUpper FilePath
monitorName) (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
ty) (FilePath -> Maybe FilePath
forall a. a -> Maybe a
Just FilePath
tyPort)

-- | The declaration of a variable in C, with a given type and name.
data VarDecl = VarDecl
    { VarDecl -> FilePath
varDeclName       :: String
    , VarDecl -> FilePath
varDeclType       :: String
    , VarDecl -> FilePath
varDeclFPrimeType :: String
    }
  deriving (forall x. VarDecl -> Rep VarDecl x)
-> (forall x. Rep VarDecl x -> VarDecl) -> Generic VarDecl
forall x. Rep VarDecl x -> VarDecl
forall x. VarDecl -> Rep VarDecl x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. VarDecl -> Rep VarDecl x
from :: forall x. VarDecl -> Rep VarDecl x
$cto :: forall x. Rep VarDecl x -> VarDecl
to :: forall x. Rep VarDecl x -> VarDecl
Generic

instance ToJSON VarDecl

data Monitor = Monitor
    { Monitor -> FilePath
monitorName     :: String
    , Monitor -> FilePath
monitorUC       :: String
    , Monitor -> Maybe FilePath
monitorType     :: Maybe String
    , Monitor -> Maybe FilePath
monitorPortType :: Maybe String
    }
  deriving (forall x. Monitor -> Rep Monitor x)
-> (forall x. Rep Monitor x -> Monitor) -> Generic Monitor
forall x. Rep Monitor x -> Monitor
forall x. Monitor -> Rep Monitor x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. Monitor -> Rep Monitor x
from :: forall x. Monitor -> Rep Monitor x
$cto :: forall x. Rep Monitor x -> Monitor
to :: forall x. Rep Monitor x -> Monitor
Generic

instance ToJSON Monitor

-- | Data that may be relevant to generate a ROS application.
data AppData = AppData
  { AppData -> [VarDecl]
variables :: [VarDecl]
  , AppData -> [Monitor]
monitors  :: [Monitor]
  , AppData -> Maybe AppData
copilot   :: Maybe Command.Standalone.AppData
  }
  deriving ((forall x. AppData -> Rep AppData x)
-> (forall x. Rep AppData x -> AppData) -> Generic AppData
forall x. Rep AppData x -> AppData
forall x. AppData -> Rep AppData x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. AppData -> Rep AppData x
from :: forall x. AppData -> Rep AppData x
$cto :: forall x. Rep AppData x -> AppData
to :: forall x. Rep AppData x -> AppData
Generic)

instance ToJSON AppData