module Language.Sifflet.Export.ToScheme
    (SExpr(..)
    , Atom(..)
    , Indent
    , Exporter
    , SchemeOptions(..)
    , defaultSchemeOptions
    , exprToSExpr
    , functionNameToSchemeName
    , valueToSExpr
    , exprToSchemeRepr
    , exprToSchemePretty
    , exprToScheme
    , inl
    , sepLines2
    , functionsToSExprs
    , functionsToPrettyScheme
    , defToSExpr
    , exportScheme
    )

where

import Paths_sifflet_lib -- generated by Cabal

import Data.Number.Sifflet
import Language.Sifflet.Export.Exporter
import Language.Sifflet.Expr
import Text.Sifflet.Repr
import Text.Sifflet.Pretty
import Language.Sifflet.Util

-- Scheme S-exprs
-- --------------

-- Names beginning with S are generally Scheme things,
-- and of course, SExpr also stands for Symbolic Expression.

data SExpr = SAtom Atom | SList [SExpr]
           deriving (Eq, Show)

data Atom = SFloat Double
          | SInt Integer
          | SSymbol String
          | SString String
          | SChar Char
          | SBool Bool
          | SFunction Function
          deriving (Eq, Show)

type Indent = Int

data SchemeOptions = 
    SchemeOptions { defineWithLambda :: Bool 
                    -- ^ use explicit lambda in function definitions,
                    -- (define f (lambda (a b) ...)
                  }
    deriving (Eq, Show)

defaultSchemeOptions :: SchemeOptions
defaultSchemeOptions = SchemeOptions {defineWithLambda = False}

-- | An SExpr is "flattish" if it is an atom
-- or is a list containing only atoms, 
-- where the empty list () is an atom.

flattish :: [SExpr] -> Bool
flattish sexprs =
    case sexprs of
      [] -> True
      x:xs -> atom x && flattish xs

atom :: SExpr -> Bool
atom sexpr =
    case sexpr of
      SAtom _ -> True
      SList [] -> True
      _ -> False

-- Converting Sifflet Exprs to SExprs

exprToSExpr :: Expr -> SExpr
exprToSExpr expr =
    case expr of
      EUndefined -> 
          SAtom (SSymbol "*sifflet-undefined*")
      ESymbol (Symbol str) -> 
          SAtom (SSymbol (functionNameToSchemeName str))

      EBool b -> valueToSExpr (VBool b)
      EChar c -> valueToSExpr (VChar c)
      ENumber n -> valueToSExpr (VNumber n)
      EString s -> valueToSExpr (VString s)

      EIf cond action altAction ->
          SList [SAtom (SSymbol "if"), exprToSExpr cond,
                 exprToSExpr action, exprToSExpr altAction]
      EList exprs -> 
          -- This case is not likely to be used,
          -- but if it is, the exprs might need to be evaluated,
          -- so we have to use list instead of quote
          SList (SAtom (SSymbol "list") : (map exprToSExpr exprs))
      ELambda (Symbol x) body ->
          SList [SAtom (SSymbol "lambda"), SList [SAtom (SSymbol x)], 
                 exprToSExpr body] 
      ECall fsym args -> 
          SList (exprToSExpr (ESymbol fsym) :  map exprToSExpr args) 
      _ -> errcats ["exprToSExpr: extended expr:", show expr]

-- Convert Sifflet function names to corresponding Scheme function names.
-- There are a few special cases; otherwise, the names are the same.
-- In particular, all of these have the same names in Sifflet
-- as in standard (R5RS) Scheme:
--     +, -, *, 
-- All of these are defined in the library sifflet.scm, 
-- with the prefix "sifflet-" (e.g., sifflet-div):
--     div, add1, sub1, /, not-equal?
-- Notes: 1+ and 1- are commonly found in Scheme implementations,
-- but not standard.

functionNameToSchemeName :: String -> String
functionNameToSchemeName name =
    case name of
      "mod" -> "modulo"
      "add1" -> "sifflet-add1"
      "sub1" -> "sifflet-sub1"
      "==" -> "equal?"
      "/=" -> "sifflet-not-equal?"
      "null" -> "null?"
      "head" -> "car"
      "tail" -> "cdr"
      ":" -> "cons"
      _ -> name

-- Converting Sifflet Values to SExprs

valueToSExpr :: Value -> SExpr
valueToSExpr value =
    case value of
      VList vs -> 
          SList [SAtom (SSymbol "quote"), SList (map valueToSExpr vs)]
      _ ->
          SAtom (case value of
                   VBool b -> SBool b
                   VChar c -> SChar c
                   VNumber (Exact i) -> SInt i
                   VNumber (Inexact x) -> SFloat x
                   VString s -> SString s
                   VFun f -> SFunction f
                   VList _ -> 
                       error ("valueToSExpr: Impossible!  " ++
                              "We can't have VList here!")
                )

-- Converting Exprs to Strings of Scheme code

exprToSchemeRepr :: Expr -> String
exprToSchemeRepr = repr . exprToSExpr

exprToSchemePretty :: Expr -> String
exprToSchemePretty = pretty . exprToSExpr

exprToScheme :: Expr -> String
exprToScheme = exprToSchemePretty

-- Converting SExprs to Strings of Scheme code

instance Repr SExpr where
    repr sexpr =
        case sexpr of
          SAtom satom ->
              case satom of
                SFloat x -> show x
                SInt i -> show i
                SSymbol name -> name -- without ""
                SString str -> show str -- with ""
                SChar char -> show char
                SBool False -> "#f"
                SBool True -> "#t"
                -- SFunction: is this case really needed?
                -- Isn't the conversion to Scheme function names
                -- done in exprToSExpr?  
                SFunction (Function mname _ _ _) ->
                    case mname of
                      Nothing -> 
                          error "SExpr/repr: cannot repr unnamed function"
                      Just name -> 
                          functionNameToSchemeName name
          SList exprs ->
              "(" ++ unwords (map repr exprs) ++ ")"

instance Pretty SExpr where
    pretty = prettyLoop 0

prettyLoop :: Indent -> SExpr -> String
prettyLoop ind sexpr =
    case sexpr of
      SAtom _ -> repr sexpr
      SList xs ->
          if flattish xs
          then repr sexpr
          else
              case xs of
                [] -> error "prettyLoop: non-flattish xs cannot be []."
                [SAtom (SSymbol "if"), _, _, _] ->
                    displayList2 ind (ind + 4) xs
                [SAtom (SSymbol "define"), _, _] ->
                    displayList2 ind (ind + 4) xs
                [SAtom (SSymbol "lambda"), _, _] ->
                    displayList2 ind (ind + 4) xs
                SAtom (SSymbol name) : args ->
                    -- If it starts as a symbol, it's probably a function call,
                    -- so put function name and first argument on one line,
                    -- and indent everything following to first argument.
                    case args of
                      [] ->
                          -- no argument
                          displayList1 ind (ind + length name + 2) xs
                      _ ->
                          -- at least one argument, so at least two elements
                          -- in the list
                          displayList2 ind (ind + length name + 2) xs
                _ -> displayList1 ind (ind + 1) xs


-- | Newline and indent
inl :: Int -> String
inl ind = "\n" ++ replicate ind ' '

-- displayList1 "shows" the first list element on the first line
-- and then the rest on succeeding lines, so it must have at
-- least one element
displayList1 :: Indent -> Indent -> [SExpr] -> String
displayList1 ind ind' xs =
    case xs of
      [] -> error "displayList1: empty list"
      x:xs' -> "(" ++ 
               prettyLoop ind x ++ 
               displayTail ind' xs'

-- Like displayList1 but "shows" the first *two* list elements
-- on the first line.
displayList2 :: Indent -> Indent -> [SExpr] -> String
displayList2 ind ind' xs =
    case xs of
      x0:x1:xs' -> "(" ++ prettyLoop ind x0 ++
                   " " ++ prettyLoop ind' x1 ++
                   displayTail ind' xs'
      _ -> error "displayList2: list is too short"

-- displayTail "shows" the tail of an SExpr which is a list,
-- it assumes the initial "(" and first element have already
-- been "shown"
displayTail :: Indent -> [SExpr] -> String
displayTail ind xs =
    case xs of
       [] -> ")"
       -- to prevent final ")" from being on a line by itself
       x:[] -> inl ind ++ prettyLoop ind x ++ ")"
       x:xs' -> inl ind ++ prettyLoop ind x ++ displayTail ind xs'



-- Converting Sifflet definitions to Scheme definitions.

-- | Convert Sifflet Functions to Scheme SExprs
functionsToSExprs :: SchemeOptions -> Functions -> [SExpr]
functionsToSExprs options (Functions fs) =
    map (defToSExpr options . functionToDef) fs

-- | Convert Sifflet Functions to pretty Scheme
functionsToPrettyScheme :: SchemeOptions -> Functions -> String
functionsToPrettyScheme options = 
    sepLines2 . map pretty . functionsToSExprs options

-- | Convert a FunctionDefTuple to a Scheme SExpr.
-- Use the form (define (name . args) body)
-- except when there are zero arguments, which becomes a
-- Scheme constant rather than a function,
-- use (define name expr).

defToSExpr :: SchemeOptions -> FunctionDefTuple -> SExpr
defToSExpr options (name, args, _atypes, _rtype, body) =
    let asym = SAtom . SSymbol
        sdefine = asym "define"
        sname = asym name
        sbody = exprToSExpr body
    in case args of
         [] -> SList [sdefine, sname, sbody]
         _:_ ->
             let argAtoms = map asym args
             in if defineWithLambda options
                then let slambda = asym "lambda"
                         sargs = SList argAtoms
                         slambdaArgsBody = SList [slambda, sargs, sbody]
                     in SList [sdefine, sname, slambdaArgsBody]
                else let snameArgs = SList (sname : argAtoms)
                     in SList [sdefine, snameArgs, sbody]

-- | Export functions to a Scheme file.

-- This, too, could use an extra "explicit lambda" argument,
-- like defToSExpr.

exportScheme :: SchemeOptions -> Exporter
exportScheme options functions path = do
  {
    let header = ";;; File: " ++ path ++
                 "\n;;; Generated by the Sifflet->Scheme exporter."
  ; lib <- schemeLibSiffletPath >>= readFile
  ; writeFile path 
              (sepLines2 [header,
                              functionsToPrettyScheme options functions,
                              lib])
  }

-- | The path to the "sifflet.scm" file.
schemeLibSiffletPath :: IO FilePath

-- getDataFileName is provided by Cabal
schemeLibSiffletPath = getDataFileName "sifflet.scm"