module Text.XML.DOM.Parser.Types
  (-- * Element matching
    ElemMatcher(..)
  , emMatch
  , emShow
  , matchElemName
  , elMatch
    -- * Name matching
  , NameMatcher(..)
  , nmMatch
  , nmShow
  , matchName
  , matchLocalName
  , matchCILocalName
    -- * Parser internals
  , DomPath(..)
  , ParserError(..)
  , pePath
  , peDetails
  , peAttributeName
  , ParserErrors(..)
  , _ParserErrors
  , ParserData(..)
  , pdElements
  , pdPath
  , DomParserT
  , DomParser
  , runDomParserT
  , runDomParser
    -- * Auxiliary
  , throwParserError
  ) where

import Control.Exception
import Control.Lens
import Control.Monad.Except
import Control.Monad.Reader
import Data.CaseInsensitive as CI
import Data.String
import Data.Text as T
import GHC.Generics (Generic)
import Text.XML
import Text.XML.Lens

-- | Arbitrary element matcher
--
-- @since 2.0.0
data ElemMatcher = ElemMatcher
  { _emMatch :: Element -> Bool
  , _emShow  :: Text
    -- ^ Field for 'Show' instance and bulding usefull errors
  }

makeLenses ''ElemMatcher

-- | Instance using instance of 'NameMatcher'
instance IsString ElemMatcher where
  fromString = matchElemName . fromString

instance Show ElemMatcher where
  show = T.unpack . _emShow

-- | Match element by name
--
-- @since 2.0.0
matchElemName :: NameMatcher -> ElemMatcher
matchElemName (NameMatcher matchName showName) = ElemMatcher
  { _emMatch = views name matchName
  , _emShow  = showName
  }

-- | Match over elements
--
-- @since 2.0.0
elMatch :: ElemMatcher -> Traversal' Element Element
elMatch (ElemMatcher match _) = filtered match

-- | Arbitrary name matcher. Match name any way you want, but
-- considered to be used as comparator with some name with some rules
--
-- @since 2.0.0
data NameMatcher = NameMatcher
  { _nmMatch :: Name -> Bool
    -- ^ Name matching function, usually should be simple comparsion
    -- function takin in account only local name or other components
    -- of 'Name'
  , _nmShow :: Text
    -- ^ Field for 'Show' instance and bulding usefull errors
  }

makeLenses ''NameMatcher

-- | Instance use 'matchCILocalName' as most general and liberal
-- matching strategy (while XML is often malformed).
--
-- @since 2.0.0
instance IsString NameMatcher where
  fromString = matchCILocalName . T.pack

instance Show NameMatcher where
  show = T.unpack . _nmShow

-- | Makes matcher which matches only local part of name igoring
-- namespace and prefix. Local name matching is case sensitive.
--
-- @since 2.0.0
matchLocalName :: Text -> NameMatcher
matchLocalName tname = NameMatcher
  { _nmMatch = \n -> nameLocalName n == tname
  , _nmShow  = tname
  }

-- | Makes matcher which matches only local part of name igoring
-- namespace and prefix. Local name matching is case insensitive. This
-- is the most common case.
--
-- @since 2.0.0
matchCILocalName :: Text -> NameMatcher
matchCILocalName tname = NameMatcher
  { _nmMatch = \n -> CI.mk (nameLocalName n) == CI.mk tname
  , _nmShow  = tname
  }

-- | Makes matcher which match name by 'Eq' with given
--
-- @since 2.0.0
matchName :: Name -> NameMatcher
matchName n = NameMatcher
  { _nmMatch = (== n)
  , _nmShow  = nameLocalName n
  }

-- | Path some element should be found at. Path starts from the root
-- element of the document. Errors are much more usefull with path.
newtype DomPath = DomPath
  { unDomPath :: [Text]
  } deriving (Eq, Ord, Show, Monoid)

-- | DOM parser error description.
data ParserError
  -- | Tag not found which should be.
  = PENotFound
    { _pePath :: DomPath
      -- ^ Path of element error occured in
    }

  -- | Expected attribute but not found
  --
  -- @since 1.0.0
  | PEAttributeNotFound
    { _peAttributeName :: NameMatcher
    , _pePath          :: DomPath
    }

  -- | Could not parse attribute
  --
  -- @since 1.0.0
  | PEAttributeWrongFormat
    { _peAttributeName :: NameMatcher
    , _peDetails       :: Text
    , _pePath          :: DomPath
    }

  -- | Node should have text content, but it does not.
  | PEContentNotFound
    { _pePath :: DomPath
    }

  -- | Tag contents has wrong format, (could not read text to value)
  | PEContentWrongFormat
    { _peDetails :: Text
    , _pePath    :: DomPath
    }

  -- | Some other error
  | PEOther
    { _peDetails :: Text
    , _pePath    :: DomPath
    } deriving (Show, Generic)

makeLenses ''ParserError

instance Exception ParserError

newtype ParserErrors = ParserErrors
  { unParserErrors :: [ParserError]
  } deriving (Show, Monoid, Generic)

makePrisms ''ParserErrors

instance Exception ParserErrors


{- | Parser scope.

Functor argument is usually @Identity@ or @[]@.

If functor is @Identity@ then parser expects exactly ONE current element. This
is common behavior for content parsers, or parsers expecting strict XML
structure.

If functor is @[]@ then parser expects arbitrary current elements count. This is
the case when you use combinators 'divePath' or 'diveElem' (posible other
variants of similar combinators). This kind of combinators performs search for
elements somewhere in descendants and result have arbitrary length in common
case.
-}

data ParserData f = ParserData
    { _pdElements :: f Element
      -- ^ Current element(s). Functor is intended to be either @Identity@ or
      -- @[]@
    , _pdPath     :: DomPath
      -- ^ Path for error reporting
    }

makeLenses ''ParserData

type DomParserT f m = ReaderT (ParserData f) (ExceptT ParserErrors m)
type DomParser f = DomParserT f Identity

-- | Run parser on root element of Document.
runDomParserT
  :: (Monad m)
  => Document
  -> DomParserT Identity m a
  -> m (Either ParserErrors a)
runDomParserT doc par =
  let
    pd = ParserData
      { _pdElements = doc ^. root . to pure
      , _pdPath     = DomPath [doc ^. root . name . to nameLocalName]
      }
  in runExceptT $ runReaderT par pd

runDomParser
  :: Document
  -> DomParser Identity a
  -> Either ParserErrors a
runDomParser doc par = runIdentity $ runDomParserT doc par

throwParserError
  :: (MonadError ParserErrors m, MonadReader (ParserData f) m)
  => (DomPath -> ParserError)
  -> m a
throwParserError mkerr = do
  path <- view pdPath
  throwError $ ParserErrors $ [mkerr path]