{-
nix-eval: Dependency-injecting Haskell evaluator

Copyright (C) 2015 Chris Warburton

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
-}

module Language.Eval.Internal where

import Data.Char
import Data.List
import Data.String
import System.Exit
import System.IO
import System.Process

-- We're stringly typed, with a veneer of safety

newtype Pkg  = Pkg String deriving (Eq, Ord, Show)
newtype Mod  = Mod String deriving (Eq, Ord, Show)
newtype Expr = Expr ([Pkg], [Mod], String) deriving (Show)

-- Allow permutations of package and module lists
instance Eq Expr where
  (Expr (p1, m1, e1)) == (Expr (p2, m2, e2)) = e1 == e2   &&
                                               perm p1 p2 &&
                                               perm m1 m2
    where perm  xs ys = allIn xs ys && allIn ys xs
          allIn xs ys = all (`elem` ys) xs

-- For convenience

instance IsString Expr where
  fromString = raw

instance IsString Pkg where
  fromString = Pkg

instance IsString Mod where
  fromString = Mod

-- Evaluation via `nix-shell`

-- | Evaluate an `Expr`; this is where the magic happens! If successful, returns
--   `Just` a `String`, which you can do what you like with.
eval :: Expr -> IO (Maybe String)
eval x@(Expr (pkgs, mods, expr)) = do
    (code, out, err) <- let (cmd, args) = mkCmd x
                         in readProcessWithExitCode cmd args (mkHs x)
    hPutStr stderr err
    return $ case code of
      ExitSuccess   -> Just (trim out)
      ExitFailure _ -> Nothing

mkCmd :: Expr -> (String, [String])
mkCmd (Expr (ps, _, _)) = ("nix-shell", ["--run", "runhaskell",
                                         "-p", mkGhcPkg ps])

-- The prefix "h." is arbitrary, as long as it matches the argument "h:"
mkGhcPkg ps = let pkgs = map (\(Pkg p) -> "(h." ++ p ++ ")") ps
               in concat ["haskellPackages.ghcWithPackages ",
                          "(h: [", unwords pkgs, "])"]

mkHs :: Expr -> String
mkHs (Expr (_, ms, e)) = unlines (imports ++ [main])
  where imports = map (\(Mod m) -> "import " ++ m) ms
        main    = "main = putStr (" ++ e ++ ")"

trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace

-- User-facing combinators

-- | A raw String of Haskell code, with no packages or modules. You can use
--   OverloadedStrings to call this automatically.
raw :: String -> Expr
raw s = Expr ([], [], s)

-- | Apply the first Expr to the second, eg. `f $$ x` ==> `f x`
($$) :: Expr -> Expr -> Expr
(Expr (p1, m1, e1)) $$ (Expr (p2, m2, e2)) = Expr
  (nub (p1 ++ p2),
   nub (m1 ++ m2),
   concat ["((", e1, ") (", e2, "))"])

-- | Convert the argument to a String, then send to `raw`
asString :: (Show a) => a -> Expr
asString = raw . show

qualified :: Mod -> String -> Expr
qualified (Mod m) e = Expr ([], [Mod m], m ++ "." ++ e)

withMods :: [Mod] -> Expr -> Expr
withMods ms (Expr (ps, ms', e)) = Expr (ps, ms' ++ ms, e)

withPkgs :: [Pkg] -> Expr -> Expr
withPkgs ps (Expr (ps', ms, e)) = Expr (ps' ++ ps, ms, e)