{-# LANGUAGE OverloadedStrings, PatternGuards #-}
-----------------------------------------------------------------------------
--
-- Module      :  ErrorAnalyze
-- Copyright   :  Copyright JP Moresmau 2015
-- License     :  BSD3
--
-- Maintainer  : jp@moresmau.fr
-- Stability   : Experimental
-- Portability : Portable
--
-- | The library itself
--
-----------------------------------------------------------------------------

module Language.Haskell.ErrorAnalyze
 ( ErrorPackage, ErrorVersion, ErrorModule, ErrorIdentifier, ErrorType
 , ErrorCause(..)
 , PackageRef(..)
 , ModuleSuggestion(..)
 , IdentifierSuggestion(..)
 , errorCauses
) where

import Data.Char
import Data.Maybe
import qualified Data.Text as T

-- | Simple synonym to indicate package names
type ErrorPackage = T.Text
-- | Simple synonym to indicate package versions
type ErrorVersion = T.Text
-- | Simple synonym to indicate module names
type ErrorModule = T.Text
-- | Simple synonym to indicate identifier names
type ErrorIdentifier = T.Text
-- | Simple synonym to indicate type names
type ErrorType = T.Text


-- | A package is already referenced in the Cabal file or not
data PackageRef = Referenced | Unreferenced
    deriving (Show,Read,Eq,Ord,Bounded,Enum)


-- | A suggestion to use a module (present in the given package/version)
data ModuleSuggestion = ModuleSuggestion ErrorPackage ErrorVersion PackageRef ErrorModule
    deriving (Show,Read,Eq,Ord)

-- | A suggestion to use an identifier (present in the given module)
data IdentifierSuggestion = IdentifierSuggestion ErrorModule ErrorIdentifier
    deriving (Show,Read,Eq,Ord)

-- | The possible error causes
data ErrorCause
  -- | Package referenced in cabal file is unknown, needs to be installed, with the given version (may be -any)
  = UnknownPackage ErrorPackage ErrorVersion
  -- | A module from the package is referenced but the package is not in the build depends section of the cabal file
  | UnreferencedPackage ErrorPackage
  -- | The type signature is missing
  | MissingType T.Text
  -- | A module has been mispelled, give suggestions
  | MispelledModule T.Text [ModuleSuggestion]
  -- | Identifier mispellt
  | MispelledIdentifier ErrorIdentifier [IdentifierSuggestion]
  -- | a full import statement is not needed (or only for instances)
  | UselessImport ErrorModule
  -- | an import element is not needed
  | UselessImportElement ErrorModule T.Text
  -- | A GHC option is missing (to add in current source file or in Cabal file)
  | MissingOption T.Text
  -- | An extension is missing (to add in current source file or in Cabal file)
  | MissingExtension T.Text
  -- | A constructor is imported from a module, instead of the type
  | ConstructorImported ErrorModule ErrorType ErrorIdentifier
  -- | An incorrect cabal version is used, the proper version range is given in the field
  | IncorrectCabalVersion T.Text
    deriving (Show,Read,Eq,Ord)

-- | Get the possible causes for a given error message
errorCauses :: T.Text -> [ErrorCause]
errorCauses msg = let
    low = T.toLower msg
    in concatMap ($ (msg,low)) analyzers
    where analyzers =
            [ unknownPackageAnalyzer
            , moduleErrorAnalyzer
            , overloadedStringAnalyzer
            , missingTypeAnalyzer
            , uselessImportAnalyzer
            , discardedDoAnalyzer
            , mispelledIdentifierAnalyzer
            , constructorImportedAnalyzer
            , missingExtensionAnalyzer
            , cabalVersionAnalyzer]

-- | Shortcut for analyzer: takes the message in original case and lower case, return the causes
type Analyzer = (T.Text,T.Text) -> [ErrorCause]

-- | An unknown package
unknownPackageAnalyzer :: Analyzer
unknownPackageAnalyzer (msg,_)
    | (_,aft) <- T.breakOn "the following dependencies are missing" msg
    , not $ T.null aft
    , (_,ln) <- T.breakOn "\n" aft
    , not $ T.null ln
    , ls <- T.strip ln
    , not $ T.null ls
    , pkgs <- map T.strip $ T.splitOn "," ls
    , nameVersions <- map (T.splitOn " ") pkgs
     = map toUP nameVersions
    | otherwise  = []
    where
        toUP (n:[]) = UnknownPackage n "-any"
        toUP (n:v:_) = UnknownPackage n v
        toUP [] = error "should not happen: empty list in toUP"

-- | An unreferenced package or a mispellt module
moduleErrorAnalyzer :: Analyzer
moduleErrorAnalyzer (msg,_)
    | T.isInfixOf couldNot msg
    , (bef,aft) <- T.breakOnEnd "you need to add" msg
    , not $ T.null bef
    , (bsp,_) <- T.breakOn " " $ T.stripStart aft
       = [UnreferencedPackage $ unquote bsp]
    | (_,aft) <- T.breakOn couldNot msg
    , not $ T.null aft
    , (ln1,aftln) <- T.breakOn "\n" $ T.drop (T.length couldNot) aft
    , not $ T.null ln1
    , modl <- unquote $ T.strip ln1
       = [MispelledModule modl (suggs $ T.strip aftln)]
    | otherwise = []
    where
        couldNot ="Could not find module"
        suggs aftln
            | T.isPrefixOf "Perhaps you meant" aftln
            , lns <- filter (not . T.isInfixOf "Use -v") $ map (unquote . T.strip) $ tail $ T.lines aftln
                = catMaybes $ map sugg lns
            | otherwise = []
        sugg ln
            | (bef,aft) <- T.breakOn " " ln
            , not $ T.null bef
            , Just brkts <- brackets aft
            , Just (pkg,vers,ref) <- suggPkg brkts
                = Just $ ModuleSuggestion pkg vers ref (T.strip bef)
            | otherwise = Nothing
        suggPkg brk
            | (bef,aft) <- T.breakOnEnd "from " brk
            , not $ T.null bef
             = pkgCut aft Referenced
            | (bef,aft) <- T.breakOnEnd "needs flag -package-key " brk
            , not $ T.null bef
             = pkgCut aft Unreferenced
            | otherwise = Nothing
        pkgCut pv ref
            | (bef,aft) <- T.breakOnEnd "-" pv
            , not $ T.null bef
            , (befAt,_) <- T.breakOn "@" aft
                = Just (T.init bef,befAt,ref)
            | otherwise = Nothing

-- | Need OverloadedStrings
overloadedStringAnalyzer :: Analyzer
overloadedStringAnalyzer (_,low)
    | uq <- unquote low
    ,      T.isInfixOf "with actual type [char]" uq
        || T.isInfixOf "with [char]" uq
      = [MissingExtension "OverloadedStrings"]
    | otherwise = []

-- | Missing type signature
missingTypeAnalyzer :: Analyzer
missingTypeAnalyzer (msg,_)
    | (bef,aft) <- T.breakOnEnd "Top-level binding with no type signature:" msg
    , not $ T.null bef
       = beautifyTypes aft
    | (bef,aft) <- T.breakOnEnd "definition but no type signature" msg
    , not $ T.null bef
    , (bef1, aft1) <- T.breakOnEnd "inferred type:" aft
    , not $ T.null bef1
       = beautifyTypes aft1
    | otherwise = []
    where cleanType typ =
            let typs = T.splitOn "::" typ
                noPkg = map (replaceCharArray . removePackage) typs
            in T.intercalate "::" noPkg
          removePackage t =
            let (b,a) = T.breakOnEnd ":" t
            in T.append (T.takeWhile (\c->c `elem` [' ','(']) b) a
          replaceCharArray = T.replace "[Char]" "String"
          beautifyTypes aft =
            let typ = T.strip aft
                (nam,rtyp) = T.breakOn "::" typ
                (_,sname) = T.breakOnEnd "." nam
                cleanTypes = T.intercalate " " $ map cleanType $ T.splitOn " " rtyp
            in [MissingType (T.concat [sname,cleanTypes])]

-- | Useless import
uselessImportAnalyzer :: Analyzer
uselessImportAnalyzer (msg,low)
    | T.isInfixOf "imported, but nothing from it is used" low
    , (bef,aft) <- T.breakOnEnd "Module" msg
    , not $ T.null bef
    , (modl,_) <- T.breakOn " " $ T.stripStart aft
        = [UselessImport (unquote modl)]
    | (bef,aft) <- T.breakOn "is redundant" msg
    , not $ T.null aft
    , (bef1,modl) <- T.breakOnEnd "import of" bef
    , not $ T.null bef1
    , (befM,aftM) <- T.breakOn "from module" modl
       = if T.null aftM
            then [UselessImport (unquote $ T.strip modl)]
            else [UselessImportElement (unquote $ T.strip $ T.drop 11 aftM) (unquote $ T.strip befM)]
    | otherwise = []

-- | Discarded do result
discardedDoAnalyzer :: Analyzer
discardedDoAnalyzer (_,low)
    | T.isInfixOf "a do-notation statement discarded a result" low
        = [MissingOption "-fno-warn-unused-do-bind"]
    | otherwise  = []

-- | A mispelled identifier, with maybe some suggestions
mispelledIdentifierAnalyzer :: Analyzer
mispelledIdentifierAnalyzer (msg,_)
    | (bef,aft) <- T.breakOnEnd "Not in scope:" msg
    , not $ T.null bef
    , (_,aftType) <- T.breakOnEnd "type constructor or class" aft
    , (_,aftData) <- T.breakOnEnd "data constructor" aftType
    , (ident:lns) <- T.lines aftData
        = [MispelledIdentifier (unquoteIfNeeded ident) (suggs lns)]
    |otherwise = []
    where
        suggs ("Perhaps you meant one of these:":lns) = suggs' lns
        suggs [t]
            | (bef,aft) <- T.breakOnEnd "Perhaps you meant" t
            , not $ T.null bef
                = suggs' [aft]
        suggs _ = []
        suggs' = catMaybes . map sugg1
        sugg1 ln
            | (ident,modlImp) <- T.breakOn " " $ T.stripStart ln
            , not $ T.null modlImp
            , Just imp <- brackets $ T.strip modlImp
            , (bef1,modl) <- T.breakOnEnd "imported from" imp
            , not $ T.null bef1
              = Just $ IdentifierSuggestion (T.strip modl) (unquoteIfNeeded ident)
            | otherwise = Nothing

-- | A constructor has been imported instead of the type
constructorImportedAnalyzer :: Analyzer
constructorImportedAnalyzer (msg,_)
   | (bef,aft) <- T.breakOn dataCons msg
   , not $ T.null aft
   , (befM,aftM) <- T.breakOnEnd ":" bef
   , not $ T.null aftM
   , (befM1,aftM1) <- T.breakOnEnd " " (T.init befM)
   , not $ T.null befM1
   , (befLn,aftLn) <- T.breakOn "\n" (T.drop (T.length dataCons) aft)
   , not $ T.null aftLn
     = [ConstructorImported (unquoteIfNeeded aftM1) (unquoteIfNeeded befLn) (unquoteIfNeeded aftM)]
   | otherwise = []
    where dataCons = "is a data constructor of"

-- | An extension is missing
missingExtensionAnalyzer :: Analyzer
missingExtensionAnalyzer (msg,low)
    | T.isInfixOf "naked expression at top level" low
        = [MissingExtension "TemplateHaskell"]
    | T.isInfixOf "parse error on input case" (unquote low)
        = [MissingExtension "LambdaCase"]
    | T.isInfixOf "naked lambda expression" low
        = [MissingExtension "LambdaCase"]
    | T.isInfixOf "enable explicit-forall syntax" low
        = [MissingExtension "RankNTypes",MissingExtension "ScopedTypeVariables",MissingExtension "ExistentialQuantification"]
    | T.isInfixOf "rigid type variable" low
        = [MissingExtension "ScopedTypeVariables"]
    | (bef,aft) <- T.breakOnEnd "(Use " msg
    , not $ T.null bef
    , (bef1,aft1) <- T.breakOn " " $ aft
    , not $ T.null aft1
        = toExtension bef1
    | (bef,aft) <- T.breakOnEnd "intended to use " msg
    , not $ T.null bef
    , (bef1,_) <- T.breakOn " " $ aft
        = toExtension bef1
    | (bef,aft) <- T.breakOnEnd "you need " msg
    , not $ T.null bef
    , (bef1,_) <- T.breakOn " " $ aft
        = toExtension bef1
    | otherwise = []
    where
          -- remove leading -X
          stripX t
            | T.isPrefixOf "-X" t = T.drop 2 t
            | otherwise = t
          -- check we have an upper case initial
          toExtension t
            | st <- stripX $ T.strip t
            , not $ T.null st
            , isUpper $ T.head st
              = [MissingExtension st]
            | otherwise = []

-- | Cabal version is not right
cabalVersionAnalyzer :: Analyzer
cabalVersionAnalyzer (msg,_)
    | (bef,aft) <- T.breakOnEnd "cabal-version:" msg
    , not $ T.null bef
    , (v,aftV) <- T.break isQuote aft
    , not $ T.null aftV
      = [IncorrectCabalVersion (T.strip v)]
    | otherwise = []

-- | Remove all quotes from given text (inside the text as well)
unquote :: T.Text -> T.Text
unquote = T.concatMap addNonQuote
    where addNonQuote c
            | isQuote c = T.empty
            | otherwise = T.singleton c
    --T.dropAround isQuote

-- | identifiers that don't contain quotes are quoted
unquoteIfNeeded :: T.Text -> T.Text
unquoteIfNeeded ident =
    let st=T.strip ident
    in if (not $ T.null st) && isQuote (T.head st)
        then unquote st
        else st


-- | Is a character a quote?
isQuote :: Char -> Bool
isQuote c= c `elem` ['\'', '`','‘','’' ]

-- | Read next text into brackets if any
brackets :: T.Text -> Maybe T.Text
brackets t
    | (_,aft) <- T.breakOn "(" t
    , not $ T.null aft
    , (bef1,aft1) <- T.breakOn ")" $ T.tail aft
    , not $ T.null aft1
      = Just $ T.strip bef1
    |otherwise = Nothing