module EHaskell (
  parseAll
, getImportsSrcDst
, copyIfSrcExist

, haskellSffx
, getEhsDir

, processOptionEhs
, exitIfFail

, CodePos(..)		-- FOR TEST
, parseEHaskell		-- FOR TEST
, getSrcAndImports	-- FOR TEST
, notOverwrided		-- FOR TEST
) where

import System.Directory       (copyFile, doesFileExist)
import System.Directory.Tools (doesNotExistOrOldThan)
import System.Exit            (ExitCode(ExitSuccess), exitWith)
import System.FilePath        (takeDirectory)
import Control.Monad          (unless)
import Control.Monad.Tools    (whenM, skipRet)
import Control.Applicative    ((<$>))
import Text.ParserCombinators.MTLParse
                              (MonadPlus, Parse, runParse, evalParse,
			       spot, spotBack, token, tokenBack, tokens, parseNot, still,
			       optional, list, greedyList, neList, greedyNeList,
			       mplus, endOfInput)
import Data.Char              (isSpace, isUpper, isLower, isDigit)
import Data.Function.Tools    (applyUnless)
import Data.List              (isPrefixOf)
import Prelude hiding         (readFile, writeFile)
import Text.RegexPR           (getbrsRegexPR)

ehaskellDir, haskellSffx, ehsHandleStr, putStrStr :: String
ehaskellDir  = "_ehs/"
haskellSffx  = ".hs"
ehsHandleStr = "_ehs_handle"
putStrStr    = "(modify . flip (++))" -- "hPutStr " ++ ehsHandleStr

getEhsDir :: FilePath -> FilePath
getEhsDir inFl = let d = takeDirectory inFl
                  in applyUnless (null d) ((d ++ "/") ++) ehaskellDir

data CodePos = Import | Top | Definition | Inner deriving (Eq, Enum, Show)

exitIfFail :: IO ExitCode -> IO ()
exitIfFail act = do
  ec <- act
  unless (ec == ExitSuccess) $ exitWith ec
  
getImportsSrcDst :: FilePath -> String -> (FilePath, FilePath)
getImportsSrcDst dir src = head $ evalParse (copyImportsParse dir) ("", src)

copyIfSrcExist :: FilePath -> FilePath -> IO ()
copyIfSrcExist src dst
  = whenM (doesFileExist src) $ whenM (doesNotExistOrOldThan dst src) $
      copyFile src dst

copyImportsParse :: String -> Parse Char (FilePath, FilePath)
copyImportsParse dir = do
  tokens "import"
  neList $ spot $ isSpace
  mn <- neList (spot $ \c -> not (isSpace c) && notElem c "()") >>= skipRet (still (spot $ \c -> isSpace c || elem c "(%") `mplus` endOfInput ' ')
  list $ spot isSpace
  optional parseParenthesis
  list $ spot isSpace
  endOfInput ()
  let sfn = mn ++ ".hs"
      dfn = dir ++ sfn
  return (sfn, dfn)

processOptionEhs :: [String] -> ([String], Maybe String, String)
processOptionEhs args
  = let (eqs, args_)        = (takeOptionEq args, dropOptionEq args)
        (outfile, [infile]) = (takeOptionO args_, dropOptionO args_)
     in (eqs, outfile, infile)

takeOptionO :: [String] -> Maybe String
takeOptionO []         = Nothing
takeOptionO ("-o":f:_) = Just f
takeOptionO (_:as)     = takeOptionO as
dropOptionO :: [String] -> [String]
dropOptionO []          = []
dropOptionO ("-o":_:as) = as
dropOptionO (a:as)      = a : dropOptionO as

takeOptionEq :: [String] -> [String]
takeOptionEq []             = []
takeOptionEq (('-':_):as)   = takeOptionEq as
takeOptionEq (a:as)
  | elem '=' a            = a : takeOptionEq as
  | otherwise               =     takeOptionEq as
dropOptionEq :: [String] -> [String]
dropOptionEq []             = []
dropOptionEq (a@('-':_):as) = a : dropOptionEq as
dropOptionEq (a:as)
  | elem '=' a            =     dropOptionEq as
  | otherwise               = a : dropOptionEq as

parseAll                                 :: [String] -> Parse Char (String, [String])
parseInnerPlain, parseString, parseNotOnlyEq
                                         :: Parse Char String
parseParenthesis, parseInner, parseLet   :: Parse Char String
parseImport, parseDef, parseTop          :: Parse Char (CodePos, String)
parseEHaskell, parseApply                :: Parse Char [ (CodePos, String) ]
parseText, parseN, parseEq, parseEqEq, parseEqShow, parseEqEqShow
                                         :: Parse Char (CodePos, String)
parseApplyBegin, parseApplyContinue, parseApplyEnd, parseVarid, parseAssigned
                                         :: Parse Char String
mkOutputText, mkOutputTop, mkOutputHere, mkOutputLet,
  mkOutputCode, mkOutputShowCode, mkOutputReturnCode, mkOutputReturnShowCode
                                         :: String -> String
mkOutputImport                           :: String -> [String] -> String
mkOutputDef                              :: String -> String -> String
getHandleStr                             :: String

parseAll eqs
  = ( getSrcAndImports
		. (map (\eq -> (Definition, mkOutputTop eq)) eqs ++)
                . filter (notOverwrided eqs)
                . ((Inner, "main = do {\n"++getHandleStr):)
		. ((Inner, "  do {\n"):)
                . (++[(Inner, "  hClose " ++ ehsHandleStr ++ " }\n")])
                . (++ [(Inner, "  } `runStateT` \"\" >>= hPutStr " ++ ehsHandleStr ++ " . snd;\n")])
		. ((Import, "import Control.Monad.State (runStateT, modify, lift)\n"):)
	        . ((Import, "import System.IO (stdout, openFile, IOMode(WriteMode), hClose)\n"):)
	        . ((Import, "import System.IO.UTF8 (hPutStr)\n"):)
	        . ((Import, "import System.Environment (getArgs)\n"):) )
        <$> parseEHaskell >>= endOfInput

getHandleStr = "  " ++ ehsHandleStr ++
               " <- getArgs >>= (\\args -> " ++
	       "if null args then return stdout else openFile (head args) WriteMode);\n"

parseEHaskell = concat <$>
  ( greedyList $ (single parseText >>= \r -> still (parseNot r $ parseText))
             `mplus`
	     single parseN
             `mplus`
	     single parseEq
	     `mplus`
	     single parseEqEq
	     `mplus`
	     single parseEqShow
	     `mplus`
	     single parseEqEqShow
	     `mplus`
             single parseImport
	     `mplus`
             single parseDef
	     `mplus`
             single parseTop
             `mplus`
             parseApply )
  where single = ((:[]) <$>)

getSrcAndImports :: [ (CodePos, String) ] -> (String, [String])
getSrcAndImports lst = (myConcat lst, map snd $ filter ((==Import).fst) $ lst)

notOverwrided :: [ String ] -> (CodePos, String) -> Bool
notOverwrided eqs (Definition, def)
  = (fst $ head $ runParse parseAssigned $ ("", def)) `notElem`
    map (fst . head . runParse parseAssigned . (,) "") eqs
notOverwrided _ _ = True

parseAssigned = mplus parseVarid $ do
  token '('
  op <- neList $ spot isAscSymbol
  token ')'
  return $ '(' : op ++ ")"

myConcat :: (Eq e, Enum e) => [ (e, [a]) ] -> [a]
myConcat lst = mcc (toEnum 0) lst
  where
  mcc _ [] = []
  mcc e lt = concat       (map snd $ filter ((==e) . fst) lt) ++
             mcc (succ e) (          filter ((/=e) . fst) lt)

parseText = do
  cont <- greedyNeList $ do
    still $ parseNot () $ tokens "<%"
    spot $ const True
  return $ (Inner, mkOutputText cont)

parseN = do
  tokens "<%" >> still (parseNot () $ spot $ flip elem "-=%")
  code <- parseInner `mplus` parseLet
  still (parseNot () $ tokenBack '-')
  tokens "%>"
  if isPrefixOf "let " code
     then return $ (Inner, mkOutputLet code)
     else return $ (Inner, mkOutputHere code)

parseLet = do
  list $ spot isSpace
  tokens "let"
  list $ spot isSpace
  token '{'
  list $ spot isSpace
  assnd <- parseAssigned
  list $ spot isSpace
  token '='
  list $ spot isSpace
  innr <- parseInner
  still $ do
    list $ spotBack isSpace
    tokenBack '}'
  list $ spot isSpace
  return $ "let { " ++ assnd ++ " = " ++ innr

parseEq = do
  tokens "<%=" >> still (parseNot () $ spot $ flip elem "=$")
  code <- parseInner
  tokens "%>"
  return $ (Inner, mkOutputReturnCode code)

parseEqEq = do
  tokens "<%==" >> still (parseNot () $ token '$')
  code <- parseInner
  still (parseNot () $ tokenBack '-')
  tokens "%>"
  return $ (Inner, mkOutputCode code)

parseEqShow  = do
  tokens "<%=$"
  code <- parseInner
  tokens "%>"
  return $ (Inner, mkOutputReturnShowCode code)

parseEqEqShow  = do
  tokens "<%==$"
  code <- parseInner
  tokens "%>"
  return $ (Inner, mkOutputShowCode code)

parseImport = do
  tokens "<%%"
  list $ spot isSpace
  tokens "import"
  list $ spot isSpace
  mn  <- neList (spot $ not . isSpace) >>= skipRet (still $ spot $ isSpace)
  list $ spot isSpace
  ips <- optional parseParenthesis
  list $ spot isSpace
  tokens "%%>"
  return $ (Import, mkOutputImport mn ips)

parseDef = do
  tokens "<%%"
  var <- parseInner
  list $ spot isSpace
  token '='
  list $ spot isSpace
  val <- parseInner
  tokens "%%>"
  return $ (Definition, mkOutputDef var val)

parseTop = do
  tokens "<%%"
  code <- parseInner
  tokens "%%>"
  return $ (Top, mkOutputTop code)

parseApply = do
  b  <- parseApplyBegin
  c  <- surround <$> parseEHaskell
  cs <- list $ do
          ci <- parseApplyContinue
          t  <- surround <$> parseEHaskell
          return $ (Inner, ci) : t
  e <- parseApplyEnd
  return $ (Inner, "lift (") : (Inner, b) : c ++ concat cs ++ [(Inner, e)] ++
           [(Inner, ") >>= ")] ++ [(Inner, putStrStr)] ++ [(Inner, ";\n")]
  where
  surround = ( ++ [ (Inner, " } `runStateT` \"\")") ] ).( (Inner, "(fmap snd $ do{\n") : )

parseApplyBegin = do
  tokens "<%"
  code <- parseInner
  tokens "-%>"
  return code

parseApplyContinue = do
  tokens "<%-"
  code <- parseInner
  tokens "-%>"
  return code

parseApplyEnd = do
  tokens "<%-"
  code <- parseInner
  tokens "%>"
  return code

parseInner = do
  greedyList (spot isSpace)
  still $ spot $ not . isSpace
  fmap concat $ list $ (parseInnerPlain >>= skipRet (still $ parseNot () $ parseInnerPlain))
                       `mplus`
                       parseString
		       `mplus`
		       parseParenthesis
		       `mplus`
		       parseNotOnlyEq

parseInnerPlain = neList $ do
  still (parseNot () $ tokens "%>")
  still (parseNot () $ tokens "-%>")
  still (parseNot () $ tokens "%%>")
  spot (flip notElem "\"(=")

parseNotOnlyEq
  = (spot isAscSymbol >>= skipRet (token '=') >>= (return . (:"=")))
    `mplus`
    (token '=' >> spot isAscSymbol >>= return . ('=':) . (:[]))

isAscSymbol :: Char -> Bool
isAscSymbol = flip elem "!#$%&*+./<=>?@\\^|-~"

parseString = do
  token '"'
  ret <- fmap concat $ list
                     $ ((:[]) <$> spot (flip notElem "\"\\")) `mplus`
		       do { token '\\'; c <- spot (const True); return ['\\', c] }
  token '"'
  return $ '"' : ret ++ "\""

parseParenthesis = do
  token '('
  ret <- fmap concat $ list
                     $ ((:[]) <$> spot (flip notElem "()")) `mplus`
		       parseParenthesis
  token ')'
  return $ '(' : ret ++ ")"

parseVarid = do
  still $ parseNot () $ spotBack isTail
  h <- spot isHead
  t <- list $ spot isTail
  still $ parseNot () $ spot isTail
  return $ h : t
  where
  isHead c = isLower c || c == '_'
  isTail c = isHead c || isUpper c || isDigit c || c == '\''

mkOutputText txt            = "    " ++ putStrStr ++ " $ " ++ show txt ++ ";\n"
mkOutputImport md []        = "import " ++ md ++ "\n"
mkOutputImport md [ips]     = "import " ++ md ++ ips ++ "\n"
mkOutputImport _ _          = error "mkOutputImport import should single"
mkOutputDef var val         = var ++ "= " ++ val ++ ";\n"
mkOutputTop code            =          code ++ ";\n"
mkOutputLet code            = "  " ++ code ++ ";\n"
mkOutputHere code
  = case getbrsRegexPR "^\\s*(\\S+)\\s*<-(.+)" code of
       (_:brs) -> "  " ++ (brs !! 0) ++ " <- lift $ " ++ (brs !! 1) ++ ";\n"
       []      -> "  lift $ "  ++ code ++ ";\n"
mkOutputCode code           = "  lift (" ++ code ++ ") >>= " ++ putStrStr ++ ";\n"
mkOutputShowCode code       = "  lift (" ++ code ++ ") >>= " ++ putStrStr ++ ". show ;\n"
mkOutputReturnCode code     = "  " ++ putStrStr ++ " $ (" ++ code ++ ") ;\n"
mkOutputReturnShowCode code = "  " ++ putStrStr ++ " $ show (" ++ code ++ ") ;\n"
