{-# LANGUAGE RankNTypes #-}

module HsDev.Symbols.Parsed (
        Ann, Parsed,
        qnames, names, binders, locals, globals, references, unresolveds,
        usages, named, imports, declarations, moduleNames,

        annL, symbolL, file, pos, defPos, resolvedName,
        isBinder, isLocal, isGlobal, isReference, isUnresolved, resolveError,
        refsTo, refsToName,
        nameInfoL, positionL, regionL, fileL,
        symbolNameL,

        prettyPrint
        ) where

import Control.Lens
import Data.Data
import Data.Data.Lens
import Data.Maybe (isJust)
import Data.Text (Text)
import Language.Haskell.Exts hiding (Name(..))
import qualified Language.Haskell.Exts as E (Name(..))
import Language.Haskell.Names

import HsDev.Symbols.Name
import HsDev.Symbols.Location (Position(..), positionLine, positionColumn, Region(..), region)

-- | Annotation of parsed and resolved nodes
type Ann = Scoped SrcSpanInfo

-- | Parsed and resolved module
type Parsed = Module Ann

-- | Get all qualified names
qnames :: Data (ast Ann) => Traversal' (ast Ann) (QName Ann)
qnames = biplate

-- | Get all names
names :: Data (ast Ann) => Traversal' (ast Ann) (E.Name Ann)
names = biplate

-- | Get all binders
binders :: Annotated ast => Traversal' (ast Ann) (ast Ann)
binders = filtered isBinder

-- | Get all names locally defined
locals :: Annotated ast => Traversal' (ast Ann) (ast Ann)
locals = filtered isLocal

-- | Get all names, references global symbol
globals :: Annotated ast => Traversal' (ast Ann) (ast Ann)
globals = filtered isGlobal

-- | Get all resolved references
references :: Annotated ast => Traversal' (ast Ann) (ast Ann)
references = filtered isReference

-- | Get all names with not in scope error
unresolveds :: Annotated ast => Traversal' (ast Ann) (ast Ann)
unresolveds = filtered isUnresolved

-- | Get all usages of symbol
usages :: Annotated ast => Name -> Traversal' (ast Ann) (ast Ann)
usages = filtered . refsTo

-- | Get usages of symbols with unqualified name
named :: Annotated ast => Text -> Traversal' (ast Ann) (ast Ann)
named = filtered . refsToName

-- | Get imports
imports :: Data (ast Ann) => Traversal' (ast Ann) (ImportDecl Ann)
imports = biplate

-- | Get declarations
declarations :: Data (ast Ann) => Traversal' (ast Ann) (Decl Ann)
declarations = biplate

-- | Get module names
moduleNames :: Data (ast Ann) => Traversal' (ast Ann) (ModuleName Ann)
moduleNames = biplate

-- | Get annotation
annL :: Annotated ast => Lens' (ast a) a
annL = lens ann (\v a' -> amap (const a') v)

-- | Get haskell-names symbols
symbolL :: Data a => Traversal' a Symbol
symbolL = biplate

-- | Get source file
file :: Annotated ast => Lens' (ast Ann) FilePath
file = annL . fileL

-- | Get source location
pos :: (Annotated ast, SrcInfo isrc, Data isrc) => Lens' (ast isrc) Position
pos = annL . positionL

-- | Definition position, if binder - returns current position
defPos :: Annotated ast => Traversal' (ast Ann) Position
defPos = annL . defLoc' where
        defLoc' :: Traversal' Ann Position
        defLoc' f (Scoped (LocalValue s) i) = Scoped <$> (LocalValue <$> positionL f s) <*> pure i
        defLoc' f (Scoped (TypeVar s) i) = Scoped <$> (TypeVar <$> positionL f s) <*> pure i
        defLoc' f (Scoped ValueBinder i) = Scoped ValueBinder <$> positionL f i
        defLoc' f (Scoped TypeBinder i) = Scoped TypeBinder <$> positionL f i
        defLoc' _ s = pure s

-- | Resolved global name
resolvedName :: Annotated ast => Traversal' (ast Ann) Name
resolvedName = annL . nameInfoL . symbolL . symbolNameL

-- | Does ast node binds something
isBinder :: Annotated ast => ast Ann -> Bool
isBinder e = (e ^. annL . nameInfoL) `elem` [TypeBinder, ValueBinder]

-- | Does ast node locally defined
isLocal :: Annotated ast => ast Ann -> Bool
isLocal e = case e ^. annL . nameInfoL of
        LocalValue _ -> True
        TypeVar _ -> True
        _ -> False

-- | Does ast node reference something
isGlobal :: Annotated ast => ast Ann -> Bool
isGlobal e = case e ^. annL . nameInfoL of
        GlobalSymbol _ _ -> True
        _ -> False

-- | Does ast node reference something
isReference :: Annotated ast => ast Ann -> Bool
isReference e = isLocal e || isGlobal e

-- | Is ast node not resolved
isUnresolved :: Annotated ast => ast Ann -> Bool
isUnresolved = isJust . resolveError

-- | Resolve error
resolveError :: Annotated ast => ast Ann -> Maybe String
resolveError e = case e ^. annL . nameInfoL of
        ScopeError err -> Just $ ppError err
        _ -> Nothing

-- | Node references to specified symbol
refsTo :: Annotated ast => Name -> ast Ann -> Bool
refsTo n a = Just n == a ^? resolvedName

-- | Node references to specified unqualified name
refsToName :: Annotated ast => Text -> ast Ann -> Bool
refsToName n a = Just n == fmap nameIdent (a ^? resolvedName)

nameInfoL :: Lens' (Scoped a) (NameInfo a)
nameInfoL = lens g' s' where
        g' (Scoped i _) = i
        s' (Scoped _ s) i' = Scoped i' s

positionL :: (SrcInfo isrc, Data isrc) => Lens' isrc Position
positionL = lens g' s' where
        g' i = Position l c where
                SrcLoc _ l c = getPointLoc i
        s' i (Position l c) = over biplate upd i where
                Position sl sc = g' i -- Old location
                -- main line: set new line and move column
                -- other lines: just move line, because altering first line's column doesn't affect other lines
                upd :: SrcLoc -> SrcLoc
                upd (SrcLoc f' l' c')
                        | l' == sl = SrcLoc f' l (c' - sc + c)
                        | otherwise = SrcLoc f' (l' - sl + l) c'

regionL :: Annotated ast => Lens' (ast Ann) Region
regionL = lens g' s' where
        g' i = case ann i of
                Scoped _ sinfo -> toPos (srcSpanStart span') `region` toPos (srcSpanEnd span') where
                        span' = srcInfoSpan sinfo
                        toPos = uncurry Position
        s' i (Region s e) = amap (fmap upd) i where
                upd :: SrcSpanInfo -> SrcSpanInfo
                upd sinfo = sinfo {
                        srcInfoSpan = (srcInfoSpan sinfo) {
                                srcSpanStartLine = s ^. positionLine,
                                srcSpanStartColumn = s ^. positionColumn,
                                srcSpanEndLine = e ^. positionLine,
                                srcSpanEndColumn = e ^. positionColumn },
                        srcInfoPoints = [] }

fileL :: (SrcInfo isrc, Data isrc) => Lens' isrc FilePath
fileL = lens g' s' where
        g' = fileName
        s' i f = set biplate f i

-- | Get 'Symbol' as 'Name'
symbolNameL :: Lens' Symbol Name
symbolNameL = lens g' s' where
        g' sym' = Qual () (symbolModule sym') (symbolName sym')
        s' sym' (Qual _ m n) = sym' { symbolModule = m, symbolName = n }
        s' sym' (UnQual _ n) = sym' { symbolName = n }
        s' sym' _ = sym'