{-# LANGUAGE GeneralizedNewtypeDeriving, MultiParamTypeClasses #-}

{- Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

module Cost (
	module Cost,
	module Types.Cost
) where

import Types.Cost
import Data.List
import Data.Maybe
import Text.Read

-- | Cost in seconds, with the type of hardware needed.
totalCost :: Cost op -> (Seconds, [UsingHardware])
totalCost (CPUCost s _) = (s, [UsingCPU])

raiseCostPower :: Cost c -> Entropy e -> Cost c
raiseCostPower c (Entropy e) = mapCost (* 2^e) c

mapCost :: (Rational-> Rational) -> Cost op -> Cost op
mapCost f (CPUCost (Seconds n) d) = CPUCost (Seconds (f n)) d

type NumCores = Integer

showCostMinutes :: NumCores -> Cost op -> String
showCostMinutes numcores (CPUCost (Seconds n) (Divisibility d))
	| n' < 61 = "1 minute"
	| otherwise = show (n' / 60) ++ " minutes"
  where
	n' :: Double
	n' = fromRational n / fromIntegral (min numcores d)

-- If an operation took n seconds on a number of cores,
-- multiply to get the CPUCost, which is for a single core.
coreCost :: NumCores -> Seconds -> Divisibility -> Cost op
coreCost cores (Seconds n) d = CPUCost (Seconds (fromIntegral cores * n)) d

castCost :: Cost a -> Cost b
castCost (CPUCost s d) = CPUCost s d

-- | CostCalc for a brute force linear search through an entropy space
-- in which each step entails paying a cost.
--
-- On average, the solution will be found half way through.
-- This is equivilant to one bit less of entropy.
bruteForceLinearSearch :: Cost step -> CostCalc BruteForceOp t
bruteForceLinearSearch stepcost e = 
	castCost stepcost `raiseCostPower` reduceEntropy e 1

-- | Estimate of cost of a brute force attack.
estimateBruteforceOf :: Bruteforceable t a => t -> Entropy a -> Cost BruteForceOp
estimateBruteforceOf t e = getBruteCostCalc t e

data DataCenterPrice = DataCenterPrice
	{ instanceCpuCores :: Integer
	, instanceCpuCoreMultiplier :: Integer
	-- ^ If the cores are twice as fast as the commodity hardware
	-- that keysafe's cost estimates are based on, use 2 to indicate
	-- this, etc.
	, instanceCostPerHour :: Cents
	}

-- August 2016 spot pricing: 36 CPU core c4.8xlarge at 33c/hour
spotAWS :: DataCenterPrice
spotAWS = DataCenterPrice
	{ instanceCpuCores = 36
	, instanceCpuCoreMultiplier = 2
	, instanceCostPerHour = Cents 33
	}

-- | Estimate of cost of brute force attack using a datacenter.
--
-- Note that this assumes that CPU cores and GPU cores are of equal number,
-- which is unlikely to be the case; typically there will be many more
-- cores than GPUs. So, this underestimates the price to brute force
-- operations which run faster on GPUs.
estimateAttackCost :: DataCenterPrice -> Cost BruteForceOp -> Dollars
estimateAttackCost dc opcost = centsToDollars $ costcents
  where
	(Seconds cpuseconds) = fst (totalCost opcost)
	cpuyears = cpuseconds / (60*60*24*365)
	costpercpuyear = Cents $
		fromIntegral (instanceCostPerHour dc) * 24 * 365
			`div` (instanceCpuCores dc * instanceCpuCoreMultiplier dc)
	costcents = Cents (ceiling cpuyears) * costpercpuyear

newtype Cents = Cents Integer
	deriving (Num, Integral, Enum, Real, Ord, Eq, Show)

-- | USD
newtype Dollars = Dollars Integer
	deriving (Num, Integral, Enum, Real, Ord, Eq)

instance Show Dollars where
	show (Dollars n) = go 
		[ (1000000000000, "trillion")
		, (1000000000, "billion")
		, (1000000, "million")
		, (1000, "thousand")
		]
	  where
		go [] = fmt (show n)
		go ((d, u):us)
			| n >= d = 
				let n' = n `div` d
				in fmt (show n' ++ " " ++ u)
			| otherwise = go us
		fmt d = "$" ++ d ++ " (USD)"

centsToDollars :: Cents -> Dollars
centsToDollars (Cents c) = Dollars (c `div` 100)

type Year = Integer

-- | Apply Moore's law to show how a cost might vary over time.
costOverTime :: Dollars -> Year -> [(Dollars, Year)]
costOverTime (Dollars currcost) thisyear = 
	(Dollars currcost, thisyear) : map calc otheryears
  where
	otheryears = [thisyear+1, thisyear+5, thisyear+10]
	calc y = 
		let monthdelta = (fromIntegral ((y * 12) - (thisyear * 12))) :: Double
		    cost = floor $ fromIntegral currcost / 2 ** (monthdelta / 18)
		in (Dollars cost, y)

costOverTimeTable :: Dollars -> Year -> [String]
costOverTimeTable cost thisyear = go [] thisyear $ costOverTime cost thisyear
  where
	go t _ [] = reverse t
	go t yprev ((c, y):ys) =
		let s = "  in " ++ show y ++ ":  " ++ show c
		in if yprev < y - 1
			then go (s:"  ...":t) y ys
			else go (s:t) y ys

-- Number of physical cores. This is not the same as
-- getNumProcessors, which includes hyper-threading.
getNumCores :: IO (Maybe NumCores)
getNumCores = getmax . mapMaybe parse . lines <$> readFile "/proc/cpuinfo"
  where
	getmax [] = Nothing
	getmax l = Just $
		maximum l + 1 -- add 1 because /proc/cpuinfo counts from 0
	parse l
		| "core id" `isPrefixOf` l =
			readMaybe $ drop 1 $ dropWhile (/= ':') l
		| otherwise = Nothing
