{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | A basic query facility.
module Zenacy.HTML.Internal.Query
  ( HTMLQuery
  , htmlQueryRun
  , htmlQueryExec
  , htmlQueryTry
  , htmlQueryStop
  , htmlQueryCont
  , htmlQuerySucc
  , htmlQueryZipper
  , htmlQueryNode
  , htmlQueryFirst
  , htmlQueryLast
  , htmlQueryNext
  , htmlQueryPrev
  , htmlQueryUp
  , htmlQueryTest
  , htmlQueryName
  , htmlQueryIsFirst
  , htmlQueryIsLast
  , htmlQuerySave
  , htmlQueryGet
  , htmlQueryGetZipper
  , htmlQuerySrc
  , htmlQueryAttr
  , htmlQueryAttrVal
  , htmlQueryId
  , htmlQueryHasClass
  , htmlQueryOnly
  ) where

import Prelude
import Zenacy.HTML.Internal.HTML
import Zenacy.HTML.Internal.Oper
import Zenacy.HTML.Internal.Zip
import Control.Monad.State
  ( MonadState
  , State
  , evalState
  , modify
  , gets
  )
import Control.Monad.Trans.Maybe
  ( MaybeT(..)
  , runMaybeT
  )
import Data.Bool
  ( bool
  )
import Data.IntMap
  ( IntMap
  )
import qualified Data.IntMap as IntMap
  ( fromList
  , lookup
  , insert
  )
import Data.Maybe
  ( fromMaybe
  , isNothing
  )
import Data.Text
  ( Text
  )

-- | Defines the query state.
type QueryState = (HTMLZipper, IntMap HTMLZipper)

-- | Defines the type for a query.
newtype HTMLQuery a = HTMLQuery { htmlQueryState :: MaybeT (State QueryState) a }
  deriving (Functor, Applicative, Monad, MonadState QueryState)

-- | Runs a query and returns a result.
htmlQueryRun :: HTMLNode -> HTMLQuery a -> Maybe a
htmlQueryRun x q = evalState (runMaybeT $ htmlQueryState q) s
  where
    z = htmlZip x
    s = (z, IntMap.fromList [(0,z)])

-- | Same as run with the arguments flipped.
htmlQueryExec :: HTMLQuery a -> HTMLNode -> Maybe a
htmlQueryExec = flip htmlQueryRun

-- | Same as run with the arguments flipped.
htmlQueryTry :: HTMLQuery HTMLNode -> HTMLNode -> HTMLNode
htmlQueryTry q x = fromMaybe x $ htmlQueryRun x q

-- | Wraps a value as a query result.
htmlQueryWrap :: Maybe a -> HTMLQuery a
htmlQueryWrap = HTMLQuery . MaybeT . pure

-- | Returns a result that stops the query.
htmlQueryStop :: HTMLQuery a
htmlQueryStop = htmlQueryWrap $ Nothing

-- | Returns a result that continues the query.
htmlQueryCont :: HTMLQuery ()
htmlQueryCont = htmlQuerySucc ()

-- | Returns a successful query result.
htmlQuerySucc :: a -> HTMLQuery a
htmlQuerySucc = htmlQueryWrap . Just

-- | Gets the current query zipper.
htmlQueryZipper :: HTMLQuery HTMLZipper
htmlQueryZipper = gets fst

-- | Gets the current query node.
htmlQueryNode :: HTMLQuery HTMLNode
htmlQueryNode = htmlZipNode <$> htmlQueryZipper

-- | Performs a query step with a zipper operation.
withZip :: (HTMLZipper -> Maybe HTMLZipper) -> HTMLQuery ()
withZip f = do
  z <- htmlQueryZipper
  case f z of
    Nothing ->
      htmlQueryStop
    Just z' -> do
      modify $ \(_, m) -> (z', m)
      htmlQueryCont

-- | Moves the query to the first child node.
htmlQueryFirst :: HTMLQuery ()
htmlQueryFirst = withZip htmlZipFirst

-- | Moves the query to the last child node.
htmlQueryLast :: HTMLQuery ()
htmlQueryLast = withZip htmlZipLast

-- | Moves the query to the next sibling node.
htmlQueryNext :: HTMLQuery ()
htmlQueryNext = withZip htmlZipNext

-- | Moves the query to the previous sibling node.
htmlQueryPrev :: HTMLQuery ()
htmlQueryPrev = withZip htmlZipPrev

-- | Moves the query to the parent node.
htmlQueryUp :: HTMLQuery ()
htmlQueryUp = withZip htmlZipUp

-- | Evaluates a test result and continues the query if true.
htmlQueryTest :: Bool -> HTMLQuery ()
htmlQueryTest = bool htmlQueryStop htmlQueryCont

-- | Tests the current element name.
htmlQueryName :: Text -> HTMLQuery ()
htmlQueryName x = do
  n <- htmlQueryNode
  htmlQueryTest $ htmlElemName n == x

-- | Tests the current node to see if it is the first sibling.
htmlQueryIsFirst :: HTMLQuery ()
htmlQueryIsFirst = do
  z <- htmlQueryZipper
  htmlQueryTest $ isNothing $ htmlZipPrev z

-- | Tests the current node to see if it is the last sibling.
htmlQueryIsLast :: HTMLQuery ()
htmlQueryIsLast = do
  z <- htmlQueryZipper
  htmlQueryTest $ isNothing $ htmlZipNext z

-- | Saves the current query state.
htmlQuerySave :: Int -> HTMLQuery ()
htmlQuerySave x = do
  modify $ \(z, m) -> (z, IntMap.insert x z m)
  htmlQueryCont

-- | Gets a saved query node.
htmlQueryGet :: Int -> HTMLQuery HTMLNode
htmlQueryGet x = htmlZipNode <$> htmlQueryGetZipper x

-- | Gets a saved query zipper.
htmlQueryGetZipper :: Int -> HTMLQuery HTMLZipper
htmlQueryGetZipper x = do
  m <- gets snd
  case IntMap.lookup x m of
    Just z -> htmlQuerySucc z
    Nothing -> htmlQueryStop

-- | Gets the source input node.
htmlQuerySrc :: HTMLQuery HTMLNode
htmlQuerySrc = htmlQueryGet 0

-- | Tests if the current node has an attribute.
htmlQueryAttr :: Text -> HTMLQuery ()
htmlQueryAttr x = htmlQueryNode >>= htmlQueryTest . htmlElemHasAttrName x

-- | Tests if the current node has an attribute value.
htmlQueryAttrVal :: Text -> Text -> HTMLQuery ()
htmlQueryAttrVal n v = htmlQueryNode >>= htmlQueryTest . htmlElemHasAttrVal n v

-- | Tests if the current node has an id.
htmlQueryId :: Text -> HTMLQuery ()
htmlQueryId x = htmlQueryNode >>= htmlQueryTest . htmlElemHasID x

-- | Tests if the current node has a class.
htmlQueryHasClass :: Text -> HTMLQuery ()
htmlQueryHasClass x = htmlQueryNode >>= htmlQueryTest . htmlElemClassesContains x

-- | Moves to the child and require that it is the only child.
htmlQueryOnly :: Text -> HTMLQuery ()
htmlQueryOnly x = htmlQueryFirst >> htmlQueryName x >> htmlQueryIsLast