{-# LANGUAGE CPP #-}
{-# LANGUAGE PatternGuards #-}
--
-- Copyright (C) 2004-5 Don Stewart - http://www.cse.unsw.edu.au/~dons
--
-- 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 2 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, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
-- 02111-1307, USA.
--

module System.Plugins.Parser (
        parse, mergeModules, pretty, parsePragmas,
        HsModule(..) ,
        replaceModName
  ) where

#include "config.h"

import Data.List
import Data.Char
import Data.Either ( )

#if defined(WITH_HSX)
import Language.Haskell.Hsx
#else
import Language.Haskell.Parser
import Language.Haskell.Syntax
import Language.Haskell.Pretty
#endif

--
-- | parse a file (as a string) as Haskell src
--
parse :: FilePath                -- ^ module name
      -> String                  -- ^ haskell src
      -> Either String HsModule  -- ^ abstract syntax

parse f fsrc =
#if defined(WITH_HSX)
    case parseFileContentsWithMode (ParseMode f) fsrc of
#else
    case parseModuleWithMode (ParseMode f) fsrc of
#endif
        ParseOk src       -> Right src
        ParseFailed loc _ -> Left $ srcmsg loc
  where
    srcmsg loc = "parse error in " ++ f ++ "\n" ++
                  "line: "  ++ (show $ srcLine loc) ++
                  ", col: " ++ (show $ srcColumn loc)++ "\n"

--
-- | pretty print haskell src
--
-- doesn't handle operators with '#' at the end. i.e. unsafeCoerce#
--
pretty :: HsModule -> String
pretty code = prettyPrintWithMode (defaultMode { linePragmas = True }) code


-- | mergeModules : generate a full Haskell src file, give a .hs config
-- file, and a stub to take default syntax and decls from. Mostly we
-- just ensure they don't do anything bad, and that the names are
-- correct for the module.
--
-- Transformations:
--
--      . Take src location pragmas from the conf file (1st file)
--      . Use the template's (2nd argument) module name
--      . Only use export list from template (2nd arg)
--      . Merge top-level decls
--      . need to force the type of the plugin to match the stub,
--      overwriting any type they supply.
--
mergeModules :: HsModule ->    -- Configure module
                HsModule ->    -- Template module
                HsModule       -- A merge of the two

mergeModules (HsModule l  _   _  is  ds )
             (HsModule _  m' es' is' ds')
         = (HsModule l  m' es'
                        (mImps m' is is')
                        (mDecl ds ds') )

--
-- | replace Module name with String.
--
replaceModName :: HsModule -> String -> HsModule
replaceModName (HsModule l _ es is ds) nm = (HsModule l (Module nm) es is ds)

--
-- | merge import declarations:
--
--  *   ensure that the config file doesn't import the stub name
--  *   merge import lists uniquely, and when they match, merge their decls
--
-- TODO * we don't merge imports of the same module from both files.
--      We should, and then merge the decls in their import list
--      * rename args, too confusing.
--
-- quick fix: strip all type signatures from the source.
--
mImps :: Module ->              -- plugin module name
        [HsImportDecl] ->       -- conf file imports
        [HsImportDecl] ->       -- stub file imports
        [HsImportDecl]

mImps plug_mod cimps timps =
    case filter (!~ self) cimps of cimps' -> unionBy (=~) cimps' timps
  where
    self = ( HsImportDecl undefined plug_mod undefined undefined undefined )

--
-- | merge top-level declarations
--
-- Remove decls found in template, using those from the config file.
-- Need to sort decls by types, then decls first, in both.
--
-- Could we write a pass to handle editor, foo :: String ?
-- We must keep the type from the template.
--
mDecl ds es = let ds' = filter (not.typeDecl) ds
              in sortBy decls $! unionBy (=~) ds' es
  where
    decls a b = compare (encoding a) (encoding b)

    typeDecl :: HsDecl -> Bool
    typeDecl (HsTypeSig _ _ _) = True
    typeDecl _ = False

    encoding :: HsDecl -> Int
    encoding d = case d of
           HsFunBind _        -> 1
           HsPatBind _ _ _ _  -> 1
           _                  -> 0

--
-- syntactic equality over the useful Haskell abstract syntax
-- this may be extended if we try to merge the files more thoroughly
--
class SynEq a where
    (=~) :: a -> a -> Bool
    (!~) :: a -> a -> Bool
    n !~ m = not (n =~ m)

instance SynEq HsDecl where
    (HsPatBind _ (HsPVar n) _ _) =~ (HsPatBind _ (HsPVar m) _ _) = n == m
    (HsTypeSig _ (n:_) _)        =~ (HsTypeSig _ (m:_) _)        = n == m
    _ =~ _ = False

instance SynEq HsImportDecl where
    (HsImportDecl _ m _ _ _) =~ (HsImportDecl _ n _ _ _)    = n == m


--
-- | Parsing option pragmas.
--
-- This is not a type checker. If the user supplies bogus options,
-- they'll get slightly mystical error messages. Also, we /want/ to
-- handle -package options, and other /static/ flags. This is more than
-- GHC.
--
-- GHC user's guide :
--
-- >    OPTIONS pragmas are only looked for at the top of your source
-- >    files, up to the first (non-literate,non-empty) line not
-- >    containing OPTIONS. Multiple OPTIONS pragmas are recognised.
--
-- based on getOptionsFromSource(), in main\/DriverUtil.hs
--
parsePragmas :: String              -- ^ input src
            -> ([String],[String])  -- ^ normal options, global options

parsePragmas s = look $ lines s
    where
        look [] = ([],[])
        look (l':ls) =
            let l = remove_spaces l'
            in case () of
                () | null l                      -> look ls
                   | prefixMatch "#" l           -> look ls
                   | prefixMatch "{-# LINE" l    -> look ls
                   | Just (Option o) <- matchPragma l
                        -> let (as,bs) = look ls in (words o ++ as,bs)
                   | Just (Global g) <- matchPragma l
                        -> let (as,bs) = look ls in (as,words g ++ bs)
                   | otherwise -> ([],[])

--
-- based on main\/DriverUtil.hs
--
-- extended to handle dynamic options too
--

data Pragma = Option !String | Global !String

matchPragma :: String -> Maybe Pragma
matchPragma s
        | Just s1 <- maybePrefixMatch "{-#" s, -- -}
          Just s2 <- maybePrefixMatch "OPTIONS" (remove_spaces s1),
          Just s3 <- maybePrefixMatch "}-#" (reverse s2)
        = Just (Option (reverse s3))

        | Just s1 <- maybePrefixMatch "{-#" s, -- -}
          Just s2 <- maybePrefixMatch "GLOBALOPTIONS" (remove_spaces s1),
          Just s3 <- maybePrefixMatch "}-#" (reverse s2)
        = Just (Global (reverse s3))

        | otherwise
        = Nothing

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

--
-- verbatim from utils\/Utils.lhs
--
prefixMatch :: Eq a => [a] -> [a] -> Bool
prefixMatch [] _str = True
prefixMatch _pat [] = False
prefixMatch (p:ps) (s:ss) | p == s    = prefixMatch ps ss
                          | otherwise = False

maybePrefixMatch :: String -> String -> Maybe String
maybePrefixMatch []    rest = Just rest
maybePrefixMatch (_:_) []   = Nothing
maybePrefixMatch (p:pat) (r:rest)
        | p == r    = maybePrefixMatch pat rest
        | otherwise = Nothing