module Diet where

import           Control.Monad
import           Foreign.C.Types
import           Foreign.Ptr
import           Test.Tasty
import           Test.Tasty.HUnit
import           Text.Printf

import           Math.Programming.Glpk.Header

test_diet :: TestTree
test_diet = testGroup "Diet problem"
  [ testCase "Basic diet problem" basicDiet
  ]

data Food = Corn | Milk | Bread
  deriving
    ( Eq
    , Ord
    , Read
    , Show
    )

data Nutrient = Calories | VitaminA
  deriving
    ( Eq
    , Ord
    , Read
    , Show
    )

basicDiet :: Assertion
basicDiet =
  let
    cost :: Food -> CDouble
    cost Corn  = 0.18
    cost Milk  = 0.23
    cost Bread = 0.05

    nutrition :: Nutrient -> Food -> CDouble
    nutrition Calories Corn  = 72
    nutrition VitaminA Corn  = 107
    nutrition Calories Milk  = 121
    nutrition VitaminA Milk  = 500
    nutrition Calories Bread = 65
    nutrition VitaminA Bread = 0

    foods :: [Food]
    foods = [Corn, Milk, Bread]

    nutrients :: [Nutrient]
    nutrients = [Calories, VitaminA]

    maxServings :: CDouble
    maxServings = 10

    nutrientBounds :: Nutrient -> (CDouble, CDouble)
    nutrientBounds Calories = (2000, 2250)
    nutrientBounds VitaminA = (5000, 50000)

    numAmountVariables :: CInt
    numAmountVariables = fromIntegral $ length foods

    numNutrientConstraints :: CInt
    numNutrientConstraints = fromIntegral (length nutrients)

    expected :: Food -> CDouble
    expected Corn  = 1.94
    expected Milk  = 10
    expected Bread = 10

    expectedCost :: CDouble
    expectedCost = 3.15
  in do
    _ <- glp_term_out glpkOff
    problem <- glp_create_prob

    -- Create the decision variables
    amountStart <- glp_add_cols problem numAmountVariables
    let amounts = zip foods [amountStart..]

    -- Create the nutrient constraints
    nutrientConStart <- glp_add_rows problem numNutrientConstraints
    let nutrientConstraints = zip nutrients [nutrientConStart..]

    -- Set the objective
    glp_set_obj_dir problem glpkMin
    forM_ amounts $ \(food, column) ->
      glp_set_obj_coef problem column (cost food)

    -- Set the right-hand side of the nutrient constraints
    forM_ nutrientConstraints $ \(nutrient, row) -> do
      let (lower, upper) = nutrientBounds nutrient
      glp_set_row_bnds problem row  glpkBounded lower upper

    -- Set the body of the nutrient constraints
    forM_ nutrientConstraints $ \(nutrient, row) ->
      allocaGlpkArray (map snd amounts) $ \indices ->
        allocaGlpkArray (map (nutrition nutrient . fst) amounts) $ \coefs ->
          glp_set_mat_row problem row numAmountVariables indices coefs

    -- Ensure we don't have too many servings of any one food
    forM_ amounts $ \(_, column) ->
      glp_set_col_bnds problem column glpkBounded 0 maxServings

    -- Check that we reached optimality
    status <- glp_simplex problem nullPtr
    status @?= glpkSimplexSuccess

    -- Check the variable values
    forM_ amounts $ \(food, column) -> do
      x <- glp_get_col_prim problem column

      let correct = expected food
          msg = printf
                "Amount of %s should be about %.2f, but is %.3f"
                (show food)
                (realToFrac correct :: Double)
                (realToFrac x :: Double)
      assertBool msg (abs (x - correct) <= 1e-1)

    -- Check the objective value
    objective <- glp_get_obj_val problem
    let msg = printf
              "Objective should be about %.2f, but is %.3f"
              (realToFrac expectedCost :: Double)
              (realToFrac objective :: Double)
    assertBool msg (abs (objective - expectedCost) < 1e-1)
