{-# LANGUAGE DeriveDataTypeable #-} 

-- |
-- Module      : Scripting.Lua.ConfigFile
-- Copyright   : (c) Benjamin Geer 2011
--
-- License     : BSD3-style
--
-- Maintainer  : benjamin.geer@gmail.com
-- Stability   : alpha
-- Portability : portable, ffi
--
-- Reads configuration files written in Lua.  See @http:\/\/www.lua.org\/@
-- for more details.
module Scripting.Lua.ConfigFile
       (
         Config,
         openConfig,
         closeConfig,
         getBool,
         getString,
         getInt,
         getDouble,
         getList,
         getNestedLists,
         getAssocList,
         getListOfAssocLists,
         getNestedAssocLists,
         ConfigFileException
       ) where

import qualified Scripting.Lua as Lua
import System.IO (FilePath)
import Control.Exception (Exception, throwIO)
import Control.Monad (forM, forM_)
import Data.Typeable (Typeable)

-- | Represents an open configuration file.
data Config = Config Lua.LuaState

-- | Thrown when an error occurs in reading a configuration file.
data ConfigFileException = ConfigFileException String
                         deriving (Show, Typeable)
instance Exception ConfigFileException

-- | Opens a config file and returns an opaque reference to the file.
-- You must close this reference using @close@ when you're done reading
-- the file.
openConfig :: FilePath -> IO Config
openConfig path = do
  l <- Lua.newstate
  loadResult <- Lua.loadfile l path
  callResult <- Lua.call l 0 0
  if loadResult /= 0 || callResult /= 0 then
    do
      errMsg <- Lua.tostring l (-1)
      throwIO $ ConfigFileException $ "cannot run config file: " ++ errMsg
    else return (Config l)

-- | Closes a configuration file.
closeConfig :: Config -> IO ()
closeConfig (Config l) =
  -- putStrLn "closing Lua"
  Lua.close l

-- | Returns a boolean value from a configuration file.  Returns @False@
-- if the value is not defined in the file.  Example:
-- 
-- > someVal = true
getBool :: Config -> String -> IO Bool
getBool (Config l) name = do
  (val, valType) <- getGlobalVal l name
  case (val, valType) of
    (Just v, Lua.TBOOLEAN) -> return v
    (Nothing, Lua.TNIL) -> return False
    (_, _) -> throwIO $ ConfigFileException $
              "expected boolean value: " ++ name

-- | Returns a string value from a configuration file.  Returns the
-- empty string if the value is not defined in the file.  Example:
-- 
-- > someVal = "foo"
getString :: Config -> String -> IO String
getString (Config l) name = do
  (val, valType) <- getGlobalVal l name
  case (val, valType) of
    (Just v, Lua.TSTRING) -> return v
    (Nothing, Lua.TNIL) -> return ""
    (_, _) -> throwIO $ ConfigFileException $
              "expected string value: " ++ name

-- | Returns an integer value from a configuration file.  Example:
-- 
-- > someVal = 2
getInt :: Config -> String -> IO (Maybe Int)
getInt (Config l) name = do
  (val, valType) <- getGlobalVal l name
  case (val, valType) of
    (Just v, Lua.TNUMBER) -> return (Just v)
    (Nothing, Lua.TNIL) -> return Nothing
    (_, _) -> throwIO $ ConfigFileException $
              "expected numeric value: " ++ name

-- | Returns a double value from a configuration file.  Example:
-- 
-- > someVal = 3.1415926
getDouble :: Config -> String -> IO (Maybe Double)
getDouble (Config l) name = do
  (val, valType) <- getGlobalVal l name
  case (val, valType) of
    (Just v, Lua.TNUMBER) -> return (Just v)
    (Nothing, Lua.TNIL) -> return Nothing
    (_, _) -> throwIO $ ConfigFileException $
              "expected numeric value: " ++ name

-- | Returns a list of strings (i.e. a Lua table in which the keys
-- are integers and the values are strings) from a configuration file.
-- Example:
-- 
-- > someVal = { "foo", "bar", "baz" }
getList :: Config -> String -> IO [String]
getList (Config l) name =
  getTable l name getListOfStrings

-- | Returns a list of lists, i.e. a Lua table of tables.  In the outer
-- table, the keys are integers and the values are tables, and in the inner
-- tables, the keys are integers and the values are strings.  Example:
-- 
-- > someVal = {
-- >    { "foo one", "foo two", "foo three" },
-- >    { "bar one", "bar two", "bar three" }
-- > }
getNestedLists :: Config -> String -> IO [[String]]
getNestedLists (Config l) name =
  getTable l name (\l name -> getOuterList l name getListOfStrings)

-- | Returns an association list, i.e. a Lua table in which the keys
-- and values are strings.  Example:
-- 
-- > someVal = {
-- >    one = "foo",
-- >    two = "bar",
-- >    three = "baz"
-- > }
getAssocList :: Config -> String -> IO [(String, String)]
getAssocList (Config l) name =
  getTable l name getColumns

-- | Returns a list of association lists, i.e. a Lua table of tables.
-- In the outer table, the keys are integers and the values are tables,
-- and in the inner tables, the keys and values are strings.  Example:
-- 
-- > someVal = {
-- >    {
-- >       foo = "aaa",
-- >       bar = "bbb",
-- >       baz = "ccc"
-- >    },
-- >    {
-- >       foo = "ddd",
-- >       bar = "eee",
-- >       baz = "fff"
-- >    }
-- > }
getListOfAssocLists :: Config -> String -> IO [[(String, String)]]
getListOfAssocLists (Config l) name =
  getTable l name (\l name -> getOuterList l name getColumns)

-- | Returns an association list of association lists, i.e. a Lua table
-- of tables.  In the outer table, the keys are strings and the values
-- are tables, and in the inner tables, the keys and values are strings.
-- Example:
-- 
-- > someVal = {
-- >    something = {
-- >       foo = "aaa",
-- >       bar = "bbb",
-- >       baz = "ccc"
-- >    },
-- >    somethingElse = {
-- >       foo = "ddd",
-- >       bar = "eee",
-- >       baz = "fff"
-- >    }
-- > }
getNestedAssocLists :: Config -> String -> IO [(String, [(String, String)])]
getNestedAssocLists (Config l) name =
  getTable l name getRows

-- Private functions

{-

Gets a Lua global and pops it off the Lua stack.

-}
getGlobalVal l name = do  
  Lua.getglobal l name
  val <- Lua.peek l (-1)
  valType <- Lua.ltype l (-1)
  Lua.pop l 1
  return (val, valType)
  
{-

Checks whether a value can be converted to a string.

-}
canBeString valType =
  valType `elem` [Lua.TSTRING, Lua.TNUMBER]

{-

Gets a Lua table, performs some action on it and returns the result
as a list.

-}
getTable :: Lua.LuaState ->
            String ->
            (Lua.LuaState -> String -> IO [a]) ->
            IO [a]
getTable l name f = do
  Lua.getglobal l name
  valType <- Lua.ltype l (-1)
  case valType of
    Lua.TTABLE -> do items <- f l name
                     Lua.pop l 1
                     return items
    Lua.TNIL -> return []
    _ -> throwIO $ ConfigFileException $ "expected table: " ++ name
  

{-

Iterates over the elements of a Lua table whose keys are integers,  
performs some action on each element, and returns the results as a
list.

-}
forList :: Lua.LuaState ->
           IO a ->
           IO [a]
forList l f = do
  tableSize <- Lua.objlen l (-1)
  forM [1..tableSize] $ \i -> do
    Lua.push l i
    Lua.gettable l (-2)
    f

{-

Gets all elements from a Lua table representing a list.  Keys are
integers and values are strings.

-}
getListOfStrings :: Lua.LuaState ->
                    String ->
                    IO [String]
getListOfStrings l name =
  forList l $ do
    valType <- Lua.ltype l (-1)
    if canBeString valType then
      do
        valStr <- Lua.tostring l (-1)
        Lua.pop l 1
        return valStr
      else throwIO $ ConfigFileException $
           "expected table of strings: " ++ name

{-

Gets all elements from a Lua table of tables.  In the outer table,
keys are integers and values are tables.  The function passed as an
argument knows the structure of the inner tables.

-}
getOuterList :: Lua.LuaState ->
                String ->
                (Lua.LuaState -> String -> IO a) ->
                IO [a]
getOuterList l name f =
  forList l $ do
    valType <- Lua.ltype l (-1)
    case valType of
      Lua.TTABLE -> do innerItems <- f l name
                       Lua.pop l 1
                       return innerItems
      _ -> throwIO $ ConfigFileException $ "expected table: " ++ name

{-

Gets all elements from a Lua table of tables.  In the outer table,
each key is a string, and each value is a table.  In the inner tables,
keys and values are strings.

-}
getRows :: Lua.LuaState -> String -> IO [(String, [(String, String)])]
getRows l name = do
  -- putStrLn $ "entering getRows"
  -- stackDump l
  Lua.pushnil l
  getRemainingRows l name

{-

Recursively gets the remaining elements from a Lua table of tables.
In the outer table, each key is a string, and each value is a table.
In the inner tables, keys and values are strings.

-}
getRemainingRows :: Lua.LuaState -> String -> IO [(String, [(String, String)])]
getRemainingRows l name = do
  -- putStrLn $ "entering getRemainingRows"
  -- stackDump l
  hasNext <- Lua.next l (-2)
  if hasNext then
     do -- putStrLn $ "getRemainingRows: hasNext"
        keyType <- Lua.ltype l (-2)
        valType <- Lua.ltype l (-1)
        case (keyType, valType) of
          (Lua.TSTRING, Lua.TTABLE) ->
            do keyStr <- Lua.tostring l (-2)
               columns <- getColumns l name
               Lua.pop l 1
               rest <- getRemainingRows l name
               return ((keyStr, columns) : rest)
          (_, _) -> throwIO $ ConfigFileException $
                       "expected string keys and table values: " ++ name
    else return []

{-

Gets all elements from a Lua table and returns them as a list of
key-value pairs, where keys and values are strings.

-}
getColumns :: Lua.LuaState -> String -> IO [(String, String)]
getColumns l name = do
  -- putStrLn $ "entering getColumns"
  -- stackDump l
  Lua.pushnil l
  getRemainingColumns l name

{-

Recursively gets the remaining elements from a Lua table and returns
them as a list of key-value pairs, where keys and values are strings.

-}
getRemainingColumns :: Lua.LuaState -> String -> IO [(String, String)]
getRemainingColumns l name = do
  -- putStrLn $ "entering getRemainingColumns"
  -- stackDump l
  hasNext <- Lua.next l (-2)
  if hasNext then
     do -- putStrLn $ "getRemainingColumns: hasNext"
        -- stackDump l
        keyType <- Lua.ltype l (-2)
        valType <- Lua.ltype l (-1)
        if keyType == Lua.TSTRING && canBeString valType then
          do
            keyStr <- Lua.tostring l (-2)
            valStr <- Lua.tostring l (-1)
            Lua.pop l 1
            rest <- getRemainingColumns l name
            return ((keyStr, valStr) : rest)
          else throwIO $ ConfigFileException $
               "expected string keys and string values: " ++ name
    else return []

{-

Dumps the Lua stack for debugging purposes.

-}
stackDump l = do
  stackSize <- Lua.gettop l
  -- putStrLn $ "Stack dump:"
  forM_ (reverse [1..stackSize]) $ \i -> do
    let relativeIndex = stackSize - i + 1
    putStr $ "Index[" ++ show i ++ " / -" ++ show relativeIndex ++ "] = "
    itemType <- Lua.ltype l i
    case itemType of
      Lua.TNONE -> putStr "TNONE"
      Lua.TNIL -> putStr "TNIL"
      Lua.TBOOLEAN -> do boolVal <- Lua.toboolean l i
                         putStr $ "TBOOLEAN " ++ show boolVal
      Lua.TLIGHTUSERDATA -> putStr "TLIGHTUSERDATA"
      Lua.TNUMBER -> do iVal <- Lua.tointeger l i
                        putStr $ "TNUMBER " ++ show iVal
      Lua.TSTRING -> do sVal <- Lua.tostring l i
                        putStr $ "TSTRING " ++ sVal
      Lua.TTABLE -> putStr "TTABLE"
      Lua.TFUNCTION -> putStr "TFUNCTION"
      Lua.TTHREAD -> putStr "TTHREAD"
    putStr "\n"
  putStr "\n"