
module Main where

import Data.Char as Char hiding (isSymbol)
import Data.List as List (nub, isPrefixOf, sortBy)
import Data.Maybe as Maybe (catMaybes)
import Control.Monad (unless)
import System.Environment (getArgs)

main = do
  [name, original, modified] <- getArgs
  diff name original modified

diff :: String -> FilePath -> FilePath -> IO ()
diff moduleName originalFile modifiedFile = do
  original <- readFile originalFile
  modified <- readFile modifiedFile
  let (added, removed, changed) =
        classify (extractFunctionsFromHi original)
                 (extractFunctionsFromHi modified)

--  putStrLn "Functions added:"
--  mapM_ putStrLn added

  unless (null removed && null changed) $
    putStrLn ("module " ++ moduleName)

  unless (null removed) $ do
    putStrLn " functions removed:"
    mapM_ (putStrLn . ("  "++)) removed
    putStrLn ""

  unless (null changed) $ do
    putStrLn " functions changed:"
    mapM_ putStrLn [ "  " ++ name ++ " changed from"
                          ++ "\n     :: " ++ originalType
                          ++ "\n  to :: " ++ modifiedType
                   | (name, originalType, modifiedType) <- changed ]
    putStrLn ""


classify :: [(String, String)] -> [(String, String)] -> ([String], [String], [(String, String, String)])
classify originalFuns modifiedFuns =
  ( [ originalName | (originalName, originalType) <- added ]
  , [ modifiedName | (modifiedName, modifiedType) <- removed
    , not $ "lvl" `isPrefixOf` modifiedName
    , not $ "gtk_" `isPrefixOf` modifiedName ]
  , [ (originalName, originalType, modifiedType)
    | ((originalName, originalType), (modifiedName, modifiedType)) <- changed
    ,    canonicaliseQuantifiedVars originalType
      /= canonicaliseQuantifiedVars modifiedType] )
  where (removed, changed, added) =
          mergeBy (comparing fst)
                  (sortBy (comparing fst) originalFuns)
                  (sortBy (comparing fst) modifiedFuns)


canonicaliseQuantifiedVars :: String -> String
canonicaliseQuantifiedVars ty =
  unwords [ case lookup w varMapping of
              Nothing -> w
              Just w' -> w'
          | w <- words ty ]
  where quantifiedVars = [ w | w@(c:_) <- words ty, Char.isLower c ]
        varMapping = zip (List.nub quantifiedVars) (map (\c -> [c]) ['a'..'z'])


-- mergeBy cmp xs ys = (only_in_xs, in_both, only_in_ys)
mergeBy :: (a -> b -> Ordering) -> [a] -> [b] -> ([a], [(a, b)], [b])
mergeBy cmp = merge [] [] []
  where merge l m r []     ys     = (reverse l, reverse m, reverse (ys++r))
        merge l m r xs     []     = (reverse (xs++l), reverse m, reverse r)
        merge l m r (x:xs) (y:ys) =
          case x `cmp` y of
            GT -> merge    l         m  (y:r) (x:xs)    ys
            EQ -> merge    l  ((x,y):m)    r     xs     ys
            LT -> merge (x:l)        m     r     xs  (y:ys)

comparing :: (Ord a) => (b -> a) -> b -> b -> Ordering
comparing p x y = compare (p x) (p y)


-- returns a list of function names with their types
extractFunctionsFromHi :: String -> [(String, String)]
extractFunctionsFromHi = catMaybes . map (eval . lexer) . init . parse


ignore = ["module", "__interface", "__export", "package", "orphans",
          ";", "$", "import", ":", "infixr", "infixl", "infix", "("]

eval :: [String] -> Maybe (String, String)
eval (x:_) | x `elem` ignore = Nothing


eval ("instance":ls) = Nothing
eval ("type":ls) = Nothing
eval ("data":ls) = Nothing
-- functions
eval ls@(name:"::":type_) = Just (name, respace (filterBraces type_))
eval xs = Nothing

filterBraces ("{":"}":xs) = filterBraces xs
filterBraces (x:xs) = x : filterBraces xs
filterBraces [] = []

respace :: [String] -> String
respace x = f (filter (/= ";") x)
    where
        f [] = ""
        f [x] = x
        f (x1:x2:xs) = if shouldspace x1 x2
                       then x1 ++ " " ++ f (x2:xs)
                       else x1 ++        f (x2:xs)

lBrack = "({["
rBrack = ")}]"

isRight [x] = x `elem` rBrack
isRight _ = False

isLeft [x] = x `elem` lBrack
isLeft _ = False


shouldspace l r = not $
    isRight r || isLeft l || r == ","


splitTerms :: [String] -> [[String]]
splitTerms xs@(x:_) | isLeft x = left : splitTerms (drop (length left) xs)
    where
        left = readBrack 0 xs

        readBrack 1 (x:xs) | isRight x = [x]
        readBrack n (x:xs) | isRight x = x : readBrack (n-1) xs
                           | isLeft  x = x : readBrack (n+1) xs
                           | otherwise = x : readBrack   n   xs

splitTerms (x:xs) = [x] : splitTerms xs
splitTerms [] = []



splitOn :: Eq a => a -> [a] -> [[a]]
splitOn x [] = []
splitOn x as = takeWhile (/= x) as : splitOn x (safeTail (dropWhile (/= x) as))


safeTail (x:xs) = xs
safeTail [] = []



parse :: String -> [String]
parse xs = rejoin (lines xs)
    where
        rejoin (x1:x2@(x2h:x2t):xs) | isSpace x2h = rejoin ((x1 ++ " " ++ dropWhile isSpace x2) : xs)
        rejoin (x:xs) = x : rejoin xs
        rejoin [] = []


lexer :: String -> [String]
lexer = demodule . lexRaw

demodule :: [String] -> [String]
demodule ("(":".":xs) = "(":".": demodule xs
demodule (x:".":xs) = demodule xs
demodule (x:xs) = x : demodule xs
demodule [] = []


-- Chunks taken from NHC's lexer, with modifications
lexRaw :: String -> [String]

lexRaw "" = []
lexRaw (x:xs) | isSpace x = lexRaw xs

lexRaw (x:xs) | x == '\'' || x == '\"' = f [x] xs
    where
        f done [] = [reverse done]
        f done ('\\':x:xs) = f (x:'\\':done) xs
        f done (a:xs) | a == x = reverse (a:done) : lexRaw xs
                      | otherwise = f (a:done) xs

lexRaw ('{':'-':x) = f x
    where
        f ('-':'}':x) = lexRaw x
        f (x:xs) = f xs
        f [] = []

lexRaw ('[':']':xs) = "[]" : lexRaw xs
lexRaw ('(':')':xs) = "()" : lexRaw xs

lexRaw ('(':x:xs) | isSymbol x && b == ')' = a : lexRaw bs
    where (a, b:bs) = span isSymbol (x:xs)

lexRaw (x:xs) | x `elem` ",;()[]{}`" = [x] : lexRaw xs
              | isDigit x = lexRaw xs -- drop digits, not needed -- continue isDigit
              | isSymbol x = continue isSymbol
              | isIdFirst x = continue isIdAny
    where
        isIdFirst c = isAlpha c || c == '_'
        isIdAny c = isAlphaNum c || c `elem` "_'#"

        continue f = a : lexRaw b
            where (a, b) = span f (x:xs)

isSymbol c = c `elem` "!@#$%&*+./<=>?\\^|:-~"

{-

lex (c:s) | isSingle c  = [([c],s)]
      | isSym c = [(c:sym,t)         | (sym,t) <- [span isSym s]]
      | isIdInit c  = [(c:nam,t)         | (nam,t) <- [span isIdChar s]]
      | isDigit c   = [(c:ds++fe,t)      | (ds,s)  <- [span isDigit s],
                           (fe,t)  <- lexFracExp s     ]
      | otherwise   = []    -- bad character

        where
        isSingle c  =  c `elem` ",;()[]{}`"
        isSym c     =  c `elem` "!@#$%&*+./<=>?\\^|:-~"
        isIdInit c  =
        isIdChar c  =  isAlphaNum c || c `elem` "_'"

        lexFracExp ('.':c:s) | isDigit c
                                   = [('.':ds++e,u) | (ds,t) <- lexDigits (c:s),
                              (e,u)  <- lexExp t    ]
        lexFracExp s       = lexExp s

        lexExp (e:s) | e `elem` "eE"
             = [(e:c:ds,u) | (c:t)  <- [s], c `elem` "+-",
                           (ds,u) <- lexDigits t] ++
               [(e:ds,t)   | (ds,t) <- lexDigits s]
        lexExp s = [("",s)]



lexRaw "" = []
lexRaw (' ':x) = lexRaw x
lexRaw (';':x) = lexRaw x

-- to make up for Hugs being wrong
lexRaw ('_':x) = lexRaw x

lexRaw ('{':'-':x) = f x
    where
        f ('-':'}':x) = lexRaw x
        f (x:xs) = f xs
        f [] = []

lexRaw x  = a : lexRaw b
    where [(a, b)] = lex x
-}
