-- |This is the expansion module.  It provides an interface for a monad
-- in which expansions can happen, and then defines the expansions.

module Language.Sh.Expansion ( ExpansionFunctions(..),
                               noGlobExpansion,
                               expand, expandWord,
                               expandPattern ) where

import Control.Monad ( forM_, forM )
import Control.Monad.Reader ( ReaderT, runReaderT, asks )
import Control.Monad.Trans ( lift )
import Data.Char ( isAlphaNum )
import Data.List ( takeWhile, dropWhile, groupBy, intersperse )
import Data.Maybe ( fromMaybe )
import Data.Monoid ( Monoid, mappend, mempty )

import Language.Sh.Compat ( on )
import Language.Sh.Glob ( removePrefix, removeSuffix )
import Language.Sh.Syntax ( Command, Word, Lexeme(..),
                            Expansion(..) ) -- , Glob, GlobChar(..) )

import Language.Sh.Arithmetic ( runMathParser )

data ExpansionFunctions m = ExpansionFunctions {
      getAllEnv :: m [(String,String)],
      setEnv :: String -> String -> m (),
      homeDir :: String -> m (Maybe String), -- default: return . Just
      expandGlob :: Word -> m [FilePath],
      commandSub :: [Command] -> m String,
      positionals :: m [String] -- maybe we want to just have getEnv...?
    }

-- |This is a private monad we use to pass around the functions...
type Exp m = ReaderT (ExpansionFunctions m) m

-- |And here's the easiest way to use them...
get' :: Monad m => Exp m [(String,String)]
get' = asks getAllEnv >>= lift
get :: Monad m => String -> Exp m (Maybe String)
get s = lookup s `fmap` get'
set :: Monad m => String -> String -> Exp m ()
set s v = use2 setEnv s v
home :: Monad m => String -> Exp m (Maybe String)
home u = use homeDir u
glob :: Monad m => Word -> Exp m [FilePath]
glob g = use expandGlob g
run :: Monad m => [Command] -> Exp m String
run cs = use commandSub cs
pos :: Monad m => Exp m [String]
pos = asks positionals >>= lift

-- |Helper functions to define these accessors
use :: Monad m => (ExpansionFunctions m -> a -> m b) -> a -> Exp m b
use f a = asks f >>= lift . ($a)
use2 :: Monad m => (ExpansionFunctions m -> a -> b -> m c) -> a -> b -> Exp m c
use2 f a b = asks f >>= lift . ($b) . ($a)


-- |This is a default function that basically treats globs as literals.
noGlobExpansion :: Monad m => Word -> m [String]
noGlobExpansion _ = return []
{-
noGlobExpansion :: (Monad m,Functor m) => Word -> m [String]
noGlobExpansion x = do s <- nge x
                       return [s]
    where nge [] = return []
          nge (Lit c:gs) = (c:) `fmap` nge gs
          nge (One:gs) = ('?':) `fmap` nge gs
          nge (Many:gs) = ('*':) `fmap` nge gs
          nge (OneOf cs:gs) = (\s->'[':cs++']':s) `fmap` nge gs
          nge (NoneOf cs:gs) = (\s->"[^"++cs++']':s) `fmap` nge gs
-}

-- |We have one main sticking point here... in the case of @A=*@, we want
-- to use expandWord, and do the glob expansion.  In the case of @>*@, we
-- want to /try/ the glob expansion and then given an error in the case
-- that we get multiple hits.  We could make one more expansion function?
-- (expandNoAmbiguousGlob?)
expand :: (Monad m,Functor m) => ExpansionFunctions m -> [Word] -> m [String]
expand fs ws = runReaderT (expandE ws) fs


-- |Test: A=1\ \ * --> A=1 ... -> so it's getting expand'ed/joined, and not
-- expandWord'ed.  For now, we'll leave globs out of this function, but it
-- seems like maybe the only use is in redirects, so then we can make this
-- the one that doesn't allow ambiguity.  Also, we know that glob expansion
-- comes after field splitting... (B=\ \ ; A=2$B*)
-- Tricky: A="3$B*"; echo $A --> looks silly, but echo "$A"...
expandWord :: (Monad m,Functor m) => ExpansionFunctions m -> Word -> m String
expandWord fs w = runReaderT (expandWordE w) fs

-- |This is a version of expandWord that doesn't deal with globs or remove
-- quotes!  It's currently only used in case statements.
expandPattern :: (Monad m,Functor m) => ExpansionFunctions m -> Word -> m Word
expandPattern fs w = runReaderT (expand' w) fs

--

expandE :: (Monad m,Functor m) => [Word] -> Exp m [String]
expandE ws = do sf <- splitFields =<< mapM expand' ws
                sfs <- forM sf $ \w -> do g <- glob w
                                          return $ if null g
                                                   then [w]
                                                   else map (map Literal) g
                return $ map removeQuotes $ concat sfs

expandWordE :: (Monad m,Functor m) => Word -> Exp m String
expandWordE w = fmap removeQuotes $ expand' w

expand' :: (Monad m,Functor m) => Word -> Exp m Word
expand' = expandParams <=< expandTilde

f <=< g = \a -> g a >>= f
infixr 1 <=<

-- |First step: tilde expansion.
expandTilde :: (Monad m,Functor m) => Word -> Exp m Word
expandTilde w = let (lit,rest) = span isLiteral w
                in case (fromLit lit) of
                     '~':s -> exp s rest
                     _     -> return w
    where exp s r | '/' `elem` s = do let (user,path) = break (=='/') s
                                      dir <- homedir user
                                      return $ map Literal (dir++"/"++path) ++ r
          exp s [] = do dir <- homedir s
                        return $ map Literal dir
          exp s r = return $ map Literal s ++ r
          isLiteral (Literal _) = True
          isLiteral _ = False
          fromLit [] = []
          fromLit (Literal c:xs) = c:fromLit xs -- don't need other case

homedir :: (Monad m,Functor m) => String -> Exp m String
homedir "" = fromMaybe ("~") `fmap` get "HOME"
homedir user = fromMaybe ("~"++user) `fmap` home user

quote :: Bool -> Word -> Word
quote True = map Quoted
quote False = id

quoteLiteral :: Bool -> String -> Word
quoteLiteral q = quote q . map Literal

-- |Parameter expansion
expandParams :: (Monad m,Functor m) => Word -> Exp m Word
expandParams = expandWith e
    where e q (SimpleExpansion n) = getEnvQ q n
          e q (LengthExpansion n) = do v <- getEnvQ q n
                                       return $ quoteLiteral q $
                                              show $ length v
          e q (ModifiedExpansion n o c w)
              = do v <- getEnvQC q c n
                   case o of
                     '-' -> return $ fromMaybe w v
                     '=' -> case v of
                              Nothing -> do setEnvW n w
                                            return w
                              Just v' -> return v'
                     '?' -> case v of -- if w then use that as message...
                              Nothing -> fail $ n++": undefined or null"
                              Just v' -> return v'
                     '+' -> return $ maybe mempty (const w) v
                     '#' -> do r <- expand' w -- expandPatternE
                               return $ fromStr q $ removePrefix c r $ toStr v
                     '%' -> do r <- expand' w -- expandPatternE
                               return $ fromStr q $ removeSuffix c r $ toStr v
          e q (CommandSub cs) = (quoteLiteral q . removeNewlines) `fmap` run cs
          e q (Arithmetic w) = fmap (quoteLiteral q) $
                                 arithExpand =<< expandWordE w
          --e _ x = fail $ "Expansion "++show x++" not yet implemented"
          removeNewlines = reverse . dropWhile (`elem`"\r\n") . reverse
          toStr = removeQuotes . fromMaybe [] --  ${@#...} should map over words
          fromStr = quoteLiteral  -- but it's technically undefined so no worry

-- crap - need to fully expand all letters...?

arithExpand :: Monad m => String -> Exp m String
arithExpand s = fmap show $ doMath s

-- This doesn't work with ++ and -- operators.....?
-- there's no postfix in parsec2... (but we could do it by hand in term parser)
-- this is a bit broken maybe...
-- plan: first clean up any unexpected tokens (\, #, etc) after
-- an initial expansion run.
-- maybe do real passes of group-words, expand, repeat...?
-- what to do with variables...?
-- dash has a much simpler arithexp than bash..  in particular,
-- a=5+10
-- echo $((++a))
-- echo $((a))   -- even this fails in dash...
-- echo $((2*$a*4)) -- 50 in both...  $-expansion comes first
-- echo $((2*a*4)) -- 120 in bash... so this expansion is LATER
-- b=c
-- c=10
-- echo $((++b))
-- ------> dash doesn't even support ++ at all...!

{-
expandLetters :: String -> Exp m String
expandLetters [] = return []
expandLetters cs | not $ null name = do e <- fromMaybe "" `fmap` getEnv name
                                        return $ expandLetters $
                                               name:expandLetters rest
                 | otherwise = do let (a,b) = break endTok cs
                                      (a',b') = span endTok cs
                                  rest <- expandLetters b'
                                  return $ a'++a''++rest
    where (name,rest) = spanName cs
          spanName (x:xs) | isAlpha x || x=='_' = let (c,rest)=span isANU xs
                                                  in (x:c,rest)
          spanName xs = ([],xs) -- not a name
          isANU x = isAlphaNum x || x=='_'
          endTok = (`elem` " \t\r\n()+-*/%^|&<>=!~?:") -- lots of operators...
-}

-- one possibility: perform all expansions by encasing first in parens?
-- BUT... a=\(; b=\); echo $(($a 5+10$b*2)) works in both shells...

-- |Helper functions...
setEnvW :: (Monad m,Functor m) => String -> Word -> Exp m () -- set a variable
setEnvW s w = do v <- expandWordE w
                 set s v

getEnvQC :: Monad m => Bool -> Bool -> String -> Exp m (Maybe Word)
getEnvQC q c n = do v <- getSpecial q n
                    case v of
                      Nothing -> return Nothing
                      Just [] -> if c then return Nothing
                                      else return $ Just []
                      Just v' -> return $ Just v'

getEnvQ :: Monad m => Bool -> String -> Exp m Word
getEnvQ q n = fromMaybe [] `fmap` getEnvQC q False n

getSpecial :: Monad m => Bool -> String -> Exp m (Maybe Word)
getSpecial q "@" = getAtStar q $ (++[SplitField]) . map Literal
getSpecial q "*" = getAtStar q $ quoteLiteral q
getSpecial q "#" = (Just . quoteLiteral q.show.length) `fmap` pos
getSpecial q n = fmap (quoteLiteral q) `fmap` get n

-- |Helper function for 'getSpecial'.
getAtStar :: Monad m => Bool -> (String -> Word) -> Exp m (Maybe Word)
getAtStar q c2l = do ps <- map (quoteLiteral q) `fmap` pos
                     fs <- (c2l . take 1) `fmap` getIFS
                     return $ if null ps
                              then Nothing
                              else Just $ concat $ intersperse fs ps

-- |Helper function for expansions...  The @Bool@ argument is for
-- whether or not we're quoted.
expandWith :: Monad m => (Bool -> Expansion -> Exp m Word)
           -> Word -> Exp m Word
expandWith f (Expand x:xs) = do x' <- f False x
                                xs' <- expandWith f xs
                                return $ x' ++ xs'
expandWith f (Quoted (Expand x):xs) = do x' <- f True x
                                         xs' <- expandWith f xs
                                         return $ x' ++ xs'
expandWith f (x:xs) = do fmap (x:) $ expandWith f xs
expandWith _ [] = return []

-- |Use @$IFS@ to split fields.
splitFields :: Monad m => [Word] -> Exp m [Word]
splitFields w = do ifs <- getIFS
                   let f SplitField  = True
                       f (Literal c) = c `elem` ifs
                       f _           = False
                       split = filter (any (not . f)) . (groupBy ((==) `on` f))
                   return $ concatMap split w

getIFS :: Monad m => Exp m String
getIFS = fmap (fromMaybe " \t\r\n") $ get "IFS"

-- |This always returns a LitWord.
removeQuotes :: Word -> String
removeQuotes [] = ""
removeQuotes (SplitField:xs) = removeQuotes xs -- IFS should already be here
removeQuotes (Quote _:xs) = removeQuotes xs
removeQuotes (Quoted x:xs) = removeQuotes $ x:xs
removeQuotes (Expand _:xs) = undefined -- shouldn't happen
removeQuotes (Literal c:xs) = c:removeQuotes xs

-- *Math-parsing
-- |We use a stateful parser, keeping track of all the current expansions,
-- as well as all the new assignments we need to make...
-- How can we do the ternary operator with parsec...? its slowness makes
-- it at least somewhat tractable...
doMath :: Monad m => String -> Exp m Int
doMath s = do subs <- get'
              case runMathParser subs s of
                Left err -> fail err
                Right (r,ss) -> do forM_ ss $ \(n,v) -> set n $ show v
                                   return r

---