-- | This module will implement pattern matching on Haskell source as parsed by `haskell-src-exts`.
--
{-# LANGUAGE QuasiQuotes     #-}
{-# LANGUAGE LambdaCase      #-}
{-# LANGUAGE TemplateHaskell #-}
module Language.Haskell.SourceMatch
    ( isStringMatchedToDecl
    , isFileMatchedToDecl
    , MatchResult
    --
    , T_1, T_2, T_3, T_4, T_5, T_6, T_7, T_8, T_9, T_10
    ) where


import           Control.Monad
import           Control.Monad.Trans.Class           as T
import           Control.Monad.Trans.Except
import           Control.Monad.Trans.State
import           Data.Either
import           Data.List
import           Data.List.Split
import           Data.Map.Strict (Map)
import           Data.Maybe
import           Language.Haskell.Exts
import           Language.Haskell.Exts.Parser
import           Language.Haskell.Exts.Syntax        as HS
import           Language.Haskell.TH                 as TH
import           Language.Haskell.TH.Lib
import           Language.Haskell.TH.Quote(QuasiQuoter(..))
import           Language.Haskell.TH.Syntax          as TH
import           Text.InterpolatedString.Perl6 (qc)
import qualified Data.Map.Strict                     as Map
import qualified Language.Haskell.TH.Syntax          as TH

-- | Dummy Types to use as metavariables
data T_1
data T_2
data T_3
data T_4
data T_5
data T_6
data T_7
data T_8
data T_9
data T_10


matchExpr, matchDecl :: QuasiQuoter
-- | This should return TH expression that will type to `Matches Exp`
matchExpr  = QuasiQuoter {
               quoteExp  = quoterToBeImplemented
             , quotePat  = quoterUndefined
             , quoteType = quoterUndefined
             , quoteDec  = quoterUndefined
             }


quoterToBeImplemented _ = [| undefined |]
quoterUndefined       _ = undefined


-- | This should return TH expression that will type to `Matches Decl`
matchDecl  = QuasiQuoter {
               quoteExp = quoterToBeImplemented
             , quotePat  = quoterUndefined
             , quoteType = quoterUndefined
             , quoteDec  = quoterUndefined
             }


-- | All declarations for:
--   * `data` types
--   * `newtype` types
--   * `type` aliases
dataDecls :: HS.Module a -> Map.Map String [Decl a]
dataDecls  = undefined


instanceDecls :: HS.Module a -> Map.Map String [Decl a]
instanceDecls  = undefined


-- | Milestones for `Match`:
type Matches a = a -> Bool
-- ^ Above is key to writing first tests for Homplexity!


defParseMode :: ParseMode
defParseMode = defaultParseMode


-- | This function should read input string, and check that it contains given declaration.
--
isStringMatchedToDecl :: String -> DecsQ -> IO (Either MatchingError MatchResult)
isStringMatchedToDecl declarationStr expectedDecl =
    case parseDeclWithMode defParseMode declarationStr of
        ParseFailed _ errorMessage ->
            fail [qc|Can't parse "{declarationStr}", error occurs: {errorMessage}|] -- TODO
        ParseOk hsParseResult -> do
            allDecls@(thParseResult:_) <- runQ expectedDecl
            when (null allDecls)        $ fail [qc|Empty `expectedDecl`|]
            when (length allDecls >= 2) $ fail [qc|Too long `expectedDecl` is not supported|]
            let result = tryMatch thParseResult hsParseResult
            return result


isFileMatchedToDecl :: FilePath -> DecsQ -> IO (Either MatchingError MatchResult)
isFileMatchedToDecl filepath expectedDecl = do
    allDecls@(thParseResult:_) <- runQ expectedDecl
    when (null allDecls)        $ fail [qc|Empty `expectedDecl`|]
    when (length allDecls >= 2) $ fail [qc|Too long `expectedDecl` is not supported|]
    --
    file <- readFile filepath
    let extensions = maybe [] snd $ readExtensions file
    case parseModuleWithMode (defParseMode { extensions = extensions }) file of
        ParseFailed _ errorMessage ->
            fail [qc|Can't parse "{filepath}", error occurs: {errorMessage}|]
        ParseOk (HS.Module _ _ _ _ decls) -> do
            let someMatchings = rights $ map (tryMatch thParseResult) decls
            case someMatchings of
                []    -> return $ Left (MatchingError "No matches")
                (m:_) -> return $ Right m


tryMatch :: (Show a)
         => TH.Dec
         -> HS.Decl a
         -> Either MatchingError MatchResult
tryMatch thDecl hsDecl = runExcept $ execStateT (thToHsMatchDeclaration thDecl hsDecl) Map.empty


--
-- * Matching itself
--

data MetaVariable = MetaVariableDecl String
                  | MetaVariableConstructor String
                  | MetaVariableVar String
                  | MetaVariableType String
                  deriving (Eq, Ord, Show)


-- | Result of matching (metavariables unification)
--
type MatchResult = Map MetaVariable String


newtype MatchingError = MatchingError String deriving Show


matchError :: String -> MatchState a
matchError = T.lift . throwE . MatchingError


-- | State of matching process
--
type MatchState a = StateT MatchResult (Except MatchingError) a


--
-- * Some useful utilities for matching state
--

-- | Add match to match result. Fail if metavariable already matched
addMatch :: MetaVariable -> String -> MatchState ()
addMatch metaVar hsName = get >>= Map.alterF insertKey metaVar >>= put
  where
    insertKey Nothing = return $ Just hsName
    insertKey k@(Just hsName')
      | hsName == hsName' = return k
      | otherwise         = matchError [qc|Var ({metaVar}, {hsName}) already matched|]


-- | Check length of lists and `zipWithM` their
zipWithLengthCheckM_ :: (Show a, Show b)
                     => String -> (a -> b -> MatchState c) -> [a] -> [b] -> MatchState ()
zipWithLengthCheckM_ listName f as bs
    | length as /= length bs = matchError [qc|{listName} list length not matched|]
    | otherwise              = zipWithM_ f as bs


--
-- * Matcher
--


thToHsMatchDeclaration :: (Show a)
                       => TH.Dec
                       -> HS.Decl a
                       -> MatchState ()
thToHsMatchDeclaration
    (TH.DataD _ thDataName _ _ thConstructors thDerivings)
    (HS.DataDecl _ (DataType _) _context (DHead _ hsDataName) hsConstructors hsDerivings) = do
        thToHsMatchIdents datatypeDeclMatcher thDataName hsDataName
        zipWithLengthCheckM_ "contstructors" thToHsMatchConstructors thConstructors hsConstructors
        zipWithLengthCheckM_ "derivings"     thToHsMatchDerivings    thDerivings    hsDerivings
thToHsMatchDeclaration th hs = matchError [qc|Mismatch: {th} =/= {hs}|]


thToHsMatchConstructors :: (Show a)
                        => Con
                        -> QualConDecl a
                        -> MatchState ()
thToHsMatchConstructors
    (TH.RecC thConstructorName thFields)
    (HS.QualConDecl _ _ _ (RecDecl _ hsConstructorName hsFields)) = do
        thToHsMatchIdents constructorMatcher thConstructorName hsConstructorName
        zipWithLengthCheckM_ "fields" thToHsMatchConstructorArgs thFields hsFields
thToHsMatchConstructors th hs = matchError [qc|Constructors mismatch: {th} =/ {hs}|]


thToHsMatchConstructorArgs :: (Show a)
                           => TH.VarBangType
                           -> HS.FieldDecl a
                           -> MatchState ()
thToHsMatchConstructorArgs
    (thName, _thBang, thType)
    (HS.FieldDecl _ names@(hsName:_) hsType) = do
        when (null names)       $ matchError "Number of TH names and HS names not matched"
        when (length names > 1) $ matchError "Many names is record declaration not supported"
        thToHsMatchIdents varMatcher thName hsName
        thToHsMatchTypes thType hsType
thToHsMatchConstructorArgs th hs = matchError [qc|Constructor args mismatch: {th} =/ {hs}|]


thToHsMatchTypes :: (Show a)
                 => TH.Type
                 -> HS.Type a
                 -> MatchState ()
thToHsMatchTypes
    (TH.AppT ListT (ConT    tsType))
    (HS.TyList _   (TyCon _ hsType')) = do
        thToHsMatchIdents typenameMatcher tsType (hsQNameToName hsType')
thToHsMatchTypes
    (TH.ConT    tsType)
    (HS.TyCon _ hsType') = do
        thToHsMatchIdents typenameMatcher tsType (hsQNameToName hsType')
thToHsMatchTypes th hs = matchError [qc|Types args mismatch: {th} =/ {hs}|]


thToHsMatchDerivings :: (Show a)
                     => TH.DerivClause
                     -> HS.Deriving a
                     -> MatchState ()
thToHsMatchDerivings
    (TH.DerivClause _ thTypes)
    (HS.Deriving _ _ instRules) = do
        let thDerivNames  = map thDerivTypeToNameStr thTypes
            thDerivNames' = sort thDerivNames -- TODO Does order of derivings important or not?
            hsDerivNames  = map hsInstRuleToNameStr instRules
            hsDerivNames' = sort hsDerivNames
        when (thDerivNames' /= hsDerivNames') $
            matchError [qc|Derivings TH: {thDerivNames'} is not matched to {hsDerivNames'}|]
        return ()
  where
    thDerivTypeToNameStr (ConT name) = thUnqualName name
    thDerivTypeToNameStr t = error [qc|Unsupported {t}|]
    hsInstRuleToNameStr (IRule _ _ _ (IHCon _ qname)) =
        case hsQNameToName qname of
            Ident  _ sname -> sname
            Symbol _ sname -> sname
    hsInstRuleToNameStr inst = error [qc|Unsupported {inst}|]
    thUnqualName (Name (OccName occ) _) = occ


thToHsMatchIdents :: (Show a) => IdentMatcher -> TH.Name -> HS.Name a -> MatchState ()
thToHsMatchIdents matcher thName (Ident _ hsName) =
    maybe (matchError [qc|TH: '{thNameStr}' is not matched to '{hsName}'|])
          (\(thName', hsName') -> addMatch thName' hsName')
          (matcher thNameStr hsName)
  where
        thNameStr = show thName
thToHsMatchIdents matcher thName hsName = matchError [qc|Mismatch: {thName} {hsName}|]


--
-- * Utilities for matchers
--


hsQNameToName :: (Show a) => HS.QName a -> HS.Name a
hsQNameToName (Qual _ _ name) = name
hsQNameToName (UnQual _ name) = name
hsQNameToName t               = error [qc|Type identifier '{t}' unsupported|]
-- TODO implement qualification support


type IdentMatcher = String -> String -> Maybe (MetaVariable, String)


-- | Matcher for data types
datatypeDeclMatcher :: IdentMatcher
datatypeDeclMatcher th hs
    | hs `isPrefixOf` th = Just (MetaVariableDecl hs, hs)
    | otherwise          = Nothing


-- | Matcher for constructor (meta)variables
constructorMatcher :: IdentMatcher
constructorMatcher = templateMatcher False MetaVariableConstructor "C"


-- | Matcher for variable (meta)variables
varMatcher :: IdentMatcher
varMatcher = templateMatcher False MetaVariableVar "v"


-- | Matcher for type (meta)variables
typenameMatcher :: IdentMatcher
typenameMatcher th hs = templateMatcher True MetaVariableType "T" (removeQualificationIfTemplate th) hs
  where
    -- Main.T_2 -> T_2
    removeQualificationIfTemplate name =
        case splitOn "." name of
            []             -> error "Impossible happened"
            [_justOneName] -> name
            path           -> last path


templateMatcher :: Bool -> (String -> MetaVariable) -> String -> IdentMatcher
templateMatcher exactMatch metavarConstr prefix th hs
    | length sth >= (if exactMatch then 2 else 3) && head sth == prefix
    = if exactMatch then Just (metavarConstr th, hs)
                    else Just (metavarConstr $ intercalate "_" (init sth), hs)
    | hs `isPrefixOf` th = Just (metavarConstr hs, hs)
    | otherwise          = Nothing
  where
    sth = splitOn "_" th