#!/usr/bin/env stack
{-
  stack --resolver lts-17.4 script
    --package sbv
-}
{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances  #-}
{-# LANGUAGE NamedFieldPuns     #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell    #-}
{-# OPTIONS_GHC -Wall -Werror   #-}

{-|
  A estrutura deste módulo foi "fortemente inspirada" por
  [este exemplo do SBV](https://hackage.haskell.org/package/sbv-8.10/docs/Documentation-SBV-Examples-Puzzles-Murder.html).

  O desafio em si foi tirado [deste site](https://www.geniol.com.br/logica/desafios/casais-de-ferias/).
-}
module Desafio2 where

import Data.Foldable (traverse_)
import Data.Functor.Identity (Identity(..))
import Data.List (transpose)
import Data.SBV
import Data.SBV.Control


data Hotel = California
           | FlorestaNativa
           | PonteGrande
           | VolteSempre

data Diferencial = ArCondicionado
                 | BelaVista
                 | Janta
                 | VagaGaragem

mkSymbolicEnumeration ''Hotel
mkSymbolicEnumeration ''Diferencial

-- | Parametrizado sobre um funtor para permitir usar este tipo nos
-- contextos simbólico e concreto.
data Casal f = MkCasal { nome :: String
                       , hotel :: f Hotel
                       , diferencial :: f Diferencial
                       , diaria :: f Word16
                       }

instance Show (Casal Identity) where
  show MkCasal{nome, hotel, diferencial, diaria} =
    unwords [ nome
            , show (runIdentity hotel)
            , show (runIdentity diferencial)
            , show (runIdentity diaria)
            ]

newCasal :: String -> Symbolic (Casal SBV)
newCasal n = do
  a <- MkCasal n <$> free_ <*> free_ <*> free_
  constrain $ diaria a `sElem` [100, 200, 300, 400]
  pure a

getCasal :: Casal SBV -> Query (Casal Identity)
getCasal MkCasal{nome, hotel, diferencial, diaria} =
  MkCasal nome <$> (Identity <$> getValue hotel)
               <*> (Identity <$> getValue diferencial)
               <*> (Identity <$> getValue diaria)

puzzle :: IO [Casal Identity]
puzzle = runSMT $ do
  giseleRafael <- newCasal "Gisele e Rafael"
  jenyVitor <- newCasal "Jeny e Vitor"
  julianaLuis <- newCasal "Juliana e Luís"
  luciaEder <- newCasal "Lúcia e Éder"

  let casais = [ giseleRafael
               , jenyVitor
               , julianaLuis
               , luciaEder
               ]
      -- some f = sOr (map f casais)
      every f = sAnd (map f casais)
      ofHotel f a = hotel a .== literal f
      ofDiferencial o a = diferencial a .== literal o

  constrain $ sAnd
    [ distinct (map hotel casais)
    , distinct (map diferencial casais)
    , distinct (map diaria casais)
    ]

  -- dica 1. O casal que ficou no hotel com vaga na garagem pagou R$
  -- 100 a mais na diária do que o casal Juliana e Luis.
  constrain . every $ \g -> ofDiferencial VagaGaragem g .=>
    diaria g .== diaria julianaLuis + 100

  -- dica 2. O casal que se hospedou no hotel Volte Sempre pagou uma
  -- diária mais cara do que o casal Jeny e Victor.
  constrain . every $ \vs -> ofHotel VolteSempre vs .=>
    diaria vs .> diaria jenyVitor

  -- dica 3. O hotel California não tem a diária mais barata.
  constrain . every $ \c -> ofHotel California c .=>
    every (\o -> literal (nome o /= nome c) .=> diaria o .< diaria c)

  -- dica 4. A diária do hotel Floresta Nativa é mais cara do que a
  -- diária do hotel Volte Sempre.
  constrain . every $ \fn -> ofHotel FlorestaNativa fn .=>
    every (\vs -> ofHotel VolteSempre vs .=>
      diaria fn .> diaria vs)

  -- dica 5. O hotel que tem ar condicionado é o Floresta Nativa ou
  -- o hotel com a diária de R$ 100.
  constrain . every $ \ac -> ofDiferencial ArCondicionado ac .=>
    sOr [ hotel ac .== literal FlorestaNativa
        , diaria ac .== 100
        ]

  -- dica 6. Sobre o casal Lucia e Eder e o casal que se hospedou no
  -- hotel Floresta Nativa, um ficou no quarto com bela vista e o
  -- outro pagou R$ 200 de diária, não necessariamente nessa ordem.
  constrain $ hotel luciaEder ./= literal FlorestaNativa
  constrain . every $ \fn -> ofHotel FlorestaNativa fn .=>
    sOr [ diferencial fn .== literal BelaVista
        , diaria fn .== 200
        ]
  constrain $
    sOr [ diferencial luciaEder .== literal BelaVista
        , diaria luciaEder .== 200
        ]

  query $ do
    cs <- checkSat
    case cs of
      Sat ->
        traverse getCasal casais
      _ ->
        pure []

resolver :: IO ()
resolver = do
  casais <- puzzle
  let toRow MkCasal{nome, hotel, diferencial, diaria} =
        nome : [ show . runIdentity $ hotel
               , show . runIdentity $ diferencial
               , show . runIdentity $ diaria
               ]
      rows = map toRow casais
      widths = map ((+ 2) . maximum . map length) (transpose rows)
      align = concat . zipWith (\i x -> take i (' ' : x ++ repeat ' ')) widths
  traverse_ (putStrLn . align) rows