{-# LANGUAGE TemplateHaskell #-}

{-| Declaration of the datatypes and functions dealing with cluster metrics
    generated by template haskell.

-}

{-

Copyright (C) 2015 Google Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-}


module Ganeti.HTools.Cluster.MetricsTH
  ( MetricComponent(..)
  , declareStatistics
  ) where

import Data.List (partition)
import Data.Maybe (mapMaybe)
import Language.Haskell.TH
import Text.Printf (printf)

import qualified Ganeti.HTools.Node as Node
import Ganeti.Utils (printTable)
import Ganeti.Utils.Statistics

-- | Data type describing the metric component. The information provided by
-- this data type is used to generate statistics data types and functions
-- dealing with them
data MetricComponent = MetricComponent
  { name           :: String -- ^ The component name
  , weight         :: Q Exp  -- ^ The component weight in the statistics sum
  , fromNode       :: Q Exp  -- ^ Quasi quoted function obtaining spread value
                             -- from a node given (Node.Node -> fromNodeType)
  , fromNodeType   :: Q Type -- ^ Quasi quoted spread value type
  , statisticsType :: Q Type -- ^ Quasi quoted statistics data type. Stat
                             -- instance for fromNodeType and statisticsType
                             -- should be defined
  , forOnlineNodes :: Bool   -- ^ Whether this component should be calculated
                             -- for online or offline nodes
  , optimalValue   :: Maybe ExpQ  -- ^ Maybe quasi quoted function obtaining
                                  -- optimal value of such component
                                  -- (Node.List -> Double)
  }

-- | Declares all functions and data types implemented in template haskell
declareStatistics :: [MetricComponent] -> Q [Dec]
declareStatistics components = do
  nodeValues              <- nodeValuesDecl components
  getNodeValues           <- getNodeValuesDecl components
  clusterStatistics       <- clusterStatisticsDecl components
  compClusterStatistics   <- compClusterStatisticsDecl components
  updateClusterStatistics <- updateClusterStatisticsDecl components
  compCVfromStats         <- compCVfromStatsDecl components
  showClusterStatistics   <- showClusterStatisticsDecl components
  optimalCVScore          <- optimalCVScoreDecl components
  return $ nodeValues ++ getNodeValues ++ clusterStatistics ++
           compClusterStatistics ++ updateClusterStatistics ++
           compCVfromStats ++ showClusterStatistics ++
           optimalCVScore

-- | Helper function constructing VarStringTypeQ
getVarStrictTypeQ :: (String, Q Type) -> VarStrictTypeQ
getVarStrictTypeQ (n, t) = do
  t' <- t
  return (mkName n, NotStrict, t')

-- | Function constructs NodeValues data type for metric components given.
-- The data type is used to store all spread values of one Node.
nodeValuesDecl :: [MetricComponent] -> Q [Dec]
nodeValuesDecl components = do
  let names = map (("nv_" ++ ) . name ) components
      types = map fromNodeType components
  strict_types <- mapM getVarStrictTypeQ $ zip names types
  return [DataD [] (mkName "NodeValues") []
         [RecC (mkName "NodeValues") strict_types] []]

-- | Function constructs ClusterStatistics data type for metric components
-- given. The data type is used to store all Statistics constructed from the
-- [NodeValues].
clusterStatisticsDecl :: [MetricComponent] -> Q [Dec]
clusterStatisticsDecl components = do
  let names = map (("cs_" ++ ) . name ) components
      types = map statisticsType components
  strict_types <- mapM getVarStrictTypeQ $ zip names types
  return [DataD [] (mkName "ClusterStatistics") []
         [RecC (mkName "ClusterStatistics") strict_types] []]

-- | Generates (getNodeValues :: Node.Node -> NodeValues) declaration for
-- metric components given. The function constructs NodeValues by calling
-- fromNode function for each metrics component.
getNodeValuesDecl :: [MetricComponent] -> Q [Dec]
getNodeValuesDecl components = do
  extract_functions <- mapM fromNode components
  x <- newName "node"
  node_t <- [t| Node.Node |]
  let names = map (mkName . ("nv_" ++) . name) components
      values = map (\f -> AppE f (VarE x)) extract_functions
      body_exp = RecConE (mkName "NodeValues") $ zip names values
      fname = mkName "getNodeValues"
      nv_t = ConT $ mkName "NodeValues"
      sig_d = SigD fname (ArrowT `AppT` node_t `AppT` nv_t)
      fun_d = FunD fname [Clause [VarP x] (NormalB body_exp) []]
  return [sig_d, fun_d]

-- | Helper function passing two arguments to a function
appTwice :: Q Exp -> Q Exp -> Q Exp -> Q Exp
appTwice fun arg1 = appE $ appE fun arg1

-- | Helper function constructing Q (Name, Exp)
getQNameExp :: String -> Q Exp -> Q (Name, Exp)
getQNameExp n e = do
  e' <- e
  return (mkName n, e')

-- | Generates (compClusterStatisticsHelper :: [Node.Node] ->
-- ClusterStatistics) declaration for metric components given. The function
-- constructs ClusterStatistics by calling calculate function for each spread
-- values list. Spread values lists are obtained by getNodeValues call.
compClusterStatisticsDecl :: [MetricComponent] -> Q [Dec]
compClusterStatisticsDecl components = do
  nl_i <- newName "nl"
  let splitted = appTwice [| partition |] [| Node.offline |] (varE nl_i)
      (nl_off, nl_on) = (appE [| fst |] splitted, appE [| snd |] splitted)
      (online, offline) = partition forOnlineNodes components
      nv_f nm = varE . mkName $ "nv_" ++ nm
      nvl_f = appTwice [| map |] (varE (mkName "getNodeValues"))
      nv_field nm = appTwice [| map |] $ nv_f nm
      cs_field nm nvl = appE [| calculate |] $ nv_field nm nvl
      (online_names, offline_names)  = (map name online, map name offline)
      offline_f = map (\nm -> getQNameExp ("cs_" ++ nm) .
                              cs_field nm $ nvl_f nl_off) offline_names
      online_f  = map (\nm -> getQNameExp ("cs_" ++ nm) .
                              cs_field nm $ nvl_f nl_on ) online_names
      body = recConE (mkName "ClusterStatistics") $ offline_f ++ online_f
      cls_stat_t = conT $ mkName "ClusterStatistics"
      fname = mkName "compClusterStatistics"
  sig_d <- sigD fname ((arrowT `appT` [t| [Node.Node] |]) `appT` cls_stat_t)
  fun_d <- funD fname [clause [varP nl_i] (normalB body) []]
  return [sig_d, fun_d]

-- | Generates (updateClusterStatistics :: ClusterStatistics ->
-- (Node.Node, Node.Node) -> ClusterStatistics) declaration for metric
-- components given. The function calls update for each ClusterStatistics
-- field if the node is online or preserves the old ClusterStatistics
-- otherwise. This action replaces contribution of the first node by the
-- contribution of the second node.
updateClusterStatisticsDecl :: [MetricComponent] -> Q [Dec]
updateClusterStatisticsDecl components = do
  old_s <- newName "old_s"
  n  <- newName "n"
  n' <- newName "n'"
  let (online, offline) = partition forOnlineNodes components
      pattern = [varP old_s, tupP [varP n, varP n']]
      is_node_online = appE [| not . Node.offline |] $ varE n
      get_nv nd = appE (varE $ mkName "getNodeValues") $ varE nd
      nv_get_field nm nd = appE (varE . mkName $ "nv_" ++ nm) $ get_nv nd
      cs_cur_field nm = appE (varE . mkName $ "cs_" ++ nm) $ varE old_s
      update_field nm = appTwice (appE [| update |] $ cs_cur_field nm)
                                  (nv_get_field nm n) (nv_get_field nm n')
      (online_names, offline_names) = (map name online, map name offline)
      offline_f = map (\nm -> getQNameExp ("cs_" ++ nm) $
                                          cs_cur_field nm) offline_names
      online_f  = map (\nm -> getQNameExp ("cs_" ++ nm) $
                                          update_field nm) online_names
      body = condE is_node_online
             (recConE (mkName "ClusterStatistics") $ offline_f ++ online_f)
             (varE old_s)
      fname = mkName "updateClusterStatistics"
      cs_t = conT $ mkName "ClusterStatistics"
  sig_d <- sigD fname ((arrowT `appT` cs_t) `appT`
                       ((arrowT `appT` [t| (Node.Node, Node.Node) |]) `appT`
                         cs_t))
  fun_d <- funD fname [clause pattern (normalB body) []]
  return [sig_d, fun_d]

-- | Generates (compCVFromStats :: ClusterStatistics -> Double) declaration
-- for metric components given. The function computes the cluster score from
-- the ClusterStatistics.
compCVfromStatsDecl :: [MetricComponent] -> Q [Dec]
compCVfromStatsDecl components = do
  cs <- newName "cs"
  let get_comp c = appE (varE . mkName $ "cs_" ++ name c) $ varE cs
      get_val c = appE [| getValue |] $ get_comp c
      term c = appTwice [| (*) :: Double -> Double -> Double |]
                         (get_val c) (weight c)
      stat = appE [| sum :: [Double] -> Double |] . listE $ map term components
      fname = mkName "compCVfromStats"
      cs_t = conT $ mkName "ClusterStatistics"
  sig_d <- sigD fname ((arrowT `appT` cs_t) `appT` [t| Double |])
  fun_d <- funD fname [clause [varP cs] (normalB stat) []]
  return [sig_d, fun_d]

-- | Generates (showClusterStatistics :: ClusterStatistics -> String)
-- declaration for metric components given. The function converts
-- ClusterStatistics to a string containing a table obtained by printTable.
showClusterStatisticsDecl :: [MetricComponent] -> Q [Dec]
showClusterStatisticsDecl components = do
  lp <- newName "lp"
  cs <- newName "cs"
  let get_comp c = appE (varE . mkName $ "cs_" ++ name c) $ varE cs
      get_val c = appE [| getValue |] $ get_comp c
      format w h val = listE [ h
                             , appE [| printf "%.8f" |] val
                             , appE [| printf "x%.2f"|] w
                             ]
      print_line c = format (weight c) (litE . StringL $ name c) (get_val c)
      header = [| [ "Field", "Value", "Weight" ] |]
      printed = listE $ map print_line components
      result = appTwice (appTwice [| printTable |] (varE lp) header)
                         printed [| False:repeat True |]
      fname = mkName "showClusterStatistics"
      cs_t = conT $ mkName "ClusterStatistics"
  sig_d <- sigD fname ((arrowT `appT` [t| String |]) `appT`
                       ((arrowT `appT` cs_t) `appT` [t| String |]))
  fun_d <- funD fname [clause [varP lp, varP cs] (normalB result) []]
  return [sig_d, fun_d]


-- | Generates (optimalCVScore :: Node.List -> Double) declaration for metric
-- components given. The function computes the lower bound of the cluster
-- score, i.e., the sum of the minimal values for all cluster score values that
-- are not 0 on a perfectly balanced cluster. Components which optimal values
-- are not 0 have Nothing as optimaLValue component
optimalCVScoreDecl :: [MetricComponent] -> Q [Dec]
optimalCVScoreDecl components = do
  nl <- newName "nl"
  let stat =
        foldl (addVal nl) [| 0 :: Double |] $ mapMaybe optimalValue components
      fname = mkName "optimalCVScore"
  sig_d <- sigD fname ((arrowT `appT` [t| Node.List |]) `appT` [t| Double |])
  fun_d <- funD fname [clause [varP nl] (normalB stat) []]
  return [sig_d, fun_d]
  where
    addVal :: Name -> ExpQ -> ExpQ -> ExpQ
    addVal nl cur f = appTwice [| (+) :: Double -> Double -> Double |]
                               cur . appE f $ varE nl