{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE OverloadedStrings #-}
-- 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.
--
-- | Variable DBs.
module Command.VariableDB
    ( VariableDB(..)
    , InputDef(..)
    , Connection(..)
    , TopicDef(..)
    , TypeDef(..)
    , emptyVariableDB
    , findInput
    , findConnection
    , findTopic
    , findType
    , findTypeByType
    , mergeVariableDB
    )
  where

-- External imports
import Control.Monad.Except (ExceptT, throwError)
import Data.Aeson           (FromJSON (..), Value (Object), (.:), withObject)
import Data.Aeson.KeyMap    (filterWithKey)
import Data.Aeson.TH        (defaultOptions, deriveFromJSON, fieldLabelModifier)
import Data.Char            (toLower)
import Data.List            (find)
import Data.Maybe           (isNothing)
import GHC.Generics         (Generic)

-- External imports: auxiliary
import Data.List.Extra (toHead)
import Data.Location (Location(..))

-- Internal imports
import Command.Errors (ErrorTriplet(..), ErrorCode)

-- * Variable Databases

-- | A variable database.
data VariableDB = VariableDB
    { inputs  :: [InputDef]
    , topics  :: [TopicDef]
    , types   :: [TypeDef]
    }
  deriving (Generic, Show)

-- | Definition of an input variable.
data InputDef = InputDef
    { inputName        :: String
    , inputType        :: Maybe String
    , inputActive      :: Bool
    , inputConnections :: [ Connection ]
    }
  deriving (Eq, Show)

-- | Definition of a connection to a topic.
data Connection = Connection
    { connectionScope :: String
    , connectionTopic :: String
    , connectionField :: Maybe String
    }
  deriving (Eq, Show)

-- | Definition of a topic.
data TopicDef = TopicDef
    { topicScope :: String
    , topicTopic :: String
    , topicType  :: String
    , topicExtra :: Value  -- ^ Additional extra info.
    }
  deriving (Eq, Show)

-- | Definition of a type or type mapping.
data TypeDef = TypeDef
    { typeFromScope :: String
    , typeFromType  :: String
    , typeFromField :: Maybe String
    , typeToScope   :: String
    , typeToType    :: String
    }
  deriving (Eq, Show)

-- | A variable database with no entries.
emptyVariableDB :: VariableDB
emptyVariableDB = VariableDB [] [] []

-- | Find an input with a given name.
findInput :: VariableDB -> String -> Maybe InputDef
findInput varDB name =
  find (\x -> inputName x == name) (inputs varDB)

-- | Find a connection a given scope.
findConnection :: InputDef -> String -> Maybe Connection
findConnection inputDef scope =
  find (\x -> connectionScope x == scope) (inputConnections inputDef)

-- | Find a topic a given scope and name.
findTopic :: VariableDB -> String -> String -> Maybe TopicDef
findTopic varDB scope name =
  find (\x -> topicScope x == scope && topicTopic x == name) (topics varDB)

-- | Find a type with a given input name, scope, and destination system.
findType :: VariableDB -> String -> String -> String -> Maybe TypeDef
findType varDB name scope destConn = do
  inputDef <- findInput varDB name
  let connectionDef :: Maybe Connection
      connectionDef = findConnection inputDef scope

      field :: Maybe String
      field = connectionField =<< connectionDef

      topic :: Maybe String
      topic = connectionTopic <$> connectionDef

      topicDef :: Maybe TopicDef
      topicDef = findTopic varDB scope =<< topic

      ty :: Maybe String
      ty = topicType <$> topicDef

  let match :: TypeDef -> Bool
      match typeDef =
        case (inputType inputDef, ty) of
          (Just ty1, Nothing) ->
                  typeFromScope typeDef == scope
               && typeFromField typeDef == field
               && typeToScope typeDef   == destConn
               && typeToType typeDef    == ty1
          (Just ty1, Just ty2) ->
                  typeFromScope typeDef == scope
               && typeFromType typeDef  == ty2
               && typeFromField typeDef == field
               && typeToScope typeDef   == destConn
               && typeToType typeDef    == ty1
          (_ ,      Just ty2) ->
                  typeFromScope typeDef == scope
               && typeFromType typeDef  == ty2
               && typeFromField typeDef == field
               && typeToScope typeDef   == destConn
          (Nothing, Nothing) -> False

  find match (types varDB)

-- | Find a type definition for a given scope, and destination system, and
-- destination type.
findTypeByType :: VariableDB -> String -> String -> String -> Maybe TypeDef
findTypeByType varDB fromScope toScope toType = do

  let match :: TypeDef -> Bool
      match typeDef =
           typeFromScope typeDef == fromScope
        && typeToScope typeDef   == toScope
        && typeToType typeDef    == toType

  find match (types varDB)

-- ** Merging of variable DBs

-- | Merge two variable DBs, so long as they do not contain contradictory
-- information.
mergeVariableDB :: Monad m
                => VariableDB -> VariableDB -> ExceptT ErrorTriplet m VariableDB
mergeVariableDB varDB1 varDB2 = do
  inputs' <- mergeInputs (inputs varDB1) (inputs varDB2)
  topics' <- mergeTopics (topics varDB1) (topics varDB2)
  types'  <- mergeTypes  (types varDB1)  (types varDB2)
  return $ VariableDB inputs' topics' types'

-- | Merge two lists of input definitions, so long as they do not contain
-- contradictory information.
mergeInputs :: Monad m
            => [InputDef] -> [InputDef] -> ExceptT ErrorTriplet m [InputDef]
mergeInputs is1 []     = return is1
mergeInputs is1 (i2:is2) = do
  is1' <- mergeInput is1 i2
  mergeInputs is1' is2

-- | Merge an input definition into a list of input definitions, so long as it
-- does not contain contradictory information.
mergeInput :: Monad m
           => [InputDef] -> InputDef -> ExceptT ErrorTriplet m [InputDef]
mergeInput []       i2 = return [i2]
mergeInput (i1:is1) i2
  | inputName i1 == inputName i2
    && (  isNothing (inputType i1)
       || isNothing (inputType i2)
       || inputType i1 == inputType i2
       )
  = do cs <- mergeConnections (inputConnections i1) (inputConnections i2)
       let i1' = i1 { inputType =
                        mergeMaybe (inputType i1) (inputType i2)
                    , inputConnections =
                        cs
                    }
       return (i1' : is1)

  | otherwise
  = do is1' <- mergeInput is1 i2
       return $ i1 : is1'

-- | Merge two lists of connections, so long as they do not contain
-- contradictory information.
mergeConnections :: Monad m
                 => [Connection] -> [Connection] -> ExceptT ErrorTriplet m [Connection]
mergeConnections cs1 []       = return cs1
mergeConnections cs1 (c2:cs2) = do
  cs1' <- mergeConnection cs1 c2
  mergeConnections cs1' cs2

-- | Merge a connection into a list of connections, so long as it does not
-- contain contradictory information.
mergeConnection ::  Monad m
                => [Connection] -> Connection -> ExceptT ErrorTriplet m [Connection]
mergeConnection []       c2 = return [c2]
mergeConnection (c1:cs1) c2
  | c1 == c2
  = return $ c1 : cs1

  | connectionScope c1 == connectionScope c2
  = throwError $
      cannotMergeVariableDBs "connections with the same scopes"

  | otherwise
  = do cs1' <- mergeConnection cs1 c2
       return (c1 : cs1')

-- | Merge two lists of topics, so long as they do not contain contradictory
-- information.
mergeTopics :: Monad m
            => [TopicDef] -> [TopicDef] -> ExceptT ErrorTriplet m [TopicDef]
mergeTopics ts1 [] = return ts1
mergeTopics ts1 (t2:ts2) = do
  ts1' <- mergeTopic ts1 t2
  mergeTopics ts1' ts2

-- | Merge a topic into a list of topics, so long as it does not contain
-- contradictory information.
mergeTopic :: Monad m
           => [TopicDef] -> TopicDef -> ExceptT ErrorTriplet m [TopicDef]
mergeTopic []       t2 = return [t2]
mergeTopic (t1:ts1) t2
  | t1 == t2
  = return $ t1 : ts1

  | topicScope t1 == topicScope t2 && topicTopic t1 == topicTopic t2
  = throwError $
      cannotMergeVariableDBs "topics with the same scopes and different types"

  | otherwise
  = do ts1' <- mergeTopic ts1 t2
       return (t1 : ts1')

-- | Merge two lists of type definitions, so long as they do not contain
-- contradictory information.
mergeTypes :: Monad m
           => [TypeDef] -> [TypeDef] -> ExceptT ErrorTriplet m [TypeDef]
mergeTypes ts1 []       = return ts1
mergeTypes ts1 (t2:ts2) = do
  ts1' <- mergeType ts1 t2
  mergeTypes ts1' ts2

-- | Merge a type definition into a list of type definitions, so long as it
-- does not contain contradictory information.
mergeType :: Monad m
           => [TypeDef] -> TypeDef -> ExceptT ErrorTriplet m [TypeDef]
mergeType []       t2 = return [t2]
mergeType (t1:ts1) t2
  | t1 == t2
  = return $ t1 : ts1

  |    typeFromScope t1 == typeFromScope t2
    && typeFromType t1  == typeFromType t2
    && typeToScope t1   == typeToScope t2
  = throwError $
      cannotMergeVariableDBs
        "types with the same scopes and from types but otherwise different"

  | otherwise
  = do ts1' <- mergeType ts1 t2
       return (t1 : ts1')

-- | Exception handler to deal with the case of variable DB files that cannot
-- be merged due to having incompatible information.
cannotMergeVariableDBs :: String -> ErrorTriplet
cannotMergeVariableDBs element =
    ErrorTriplet ecCannotMergeVariableDB msg LocationNothing
  where
    msg =
      "Reading variable DBs has failed due to them having incompatible"
      ++ " information for " ++ element ++ "."

-- | Error: one of the variable DBs provided cannot be merged.
ecCannotMergeVariableDB :: ErrorCode
ecCannotMergeVariableDB = 1

-- | Merge two @Maybe@ values, prefering the left one if two @Just@s are
-- provided.
mergeMaybe :: Maybe a -> Maybe a -> Maybe a
mergeMaybe Nothing x       = x
mergeMaybe x       Nothing = x
mergeMaybe x       _       = x

-- | Implement instances of parser to read variable DB from JSON, dropping the
-- prefix in each field name.
deriveFromJSON
  defaultOptions {fieldLabelModifier = toHead toLower . drop 4 }
  ''TypeDef

instance FromJSON TopicDef where
  parseJSON = withObject "TopicDef" $ \obj -> do
    scope <- obj .: "scope"
    topic <- obj .: "topic"
    typeV <- obj .: "type"

    let extra = Object $
          filterWithKey (\k _ -> k `notElem` [ "scope", "topic", "type" ]) obj

    pure TopicDef
      { topicScope = scope
      , topicTopic = topic
      , topicType  = typeV
      , topicExtra = extra
      }

deriveFromJSON
  defaultOptions {fieldLabelModifier = toHead toLower . drop 10 }
  ''Connection

deriveFromJSON
  defaultOptions {fieldLabelModifier = toHead toLower . drop 5 }
  ''InputDef

instance FromJSON VariableDB
