{-# LANGUAGE DeriveDataTypeable #-}
module Text.XML.HXT.DOM.QualifiedName
    ( QName
    , XName(unXN)
    , NsEnv
    , mkQName
    , mkName
    , mkNsName
    , mkSNsName
    , mkPrefixLocalPart
    , equivQName
    , equivUri
    , equalQNameBy
    , namePrefix
    , localPart
    , namespaceUri
    , newXName
    , nullXName
    , isNullXName
    , newQName
    , mkQName'
    , namePrefix'
    , localPart'
    , namespaceUri'
    , setNamePrefix'
    , setLocalPart'
    , setNamespaceUri'
    , qualifiedName
    , qualifiedName'
    , universalName
    , universalUri
    , buildUniversalName
    , normalizeNsUri
    , setNamespace                      
    , isNCName
    , isWellformedQualifiedName
    , isWellformedQName
    , isWellformedNSDecl
    , isWellformedNameSpaceName
    , isNameSpaceName
    , isDeclaredNamespace
    , xmlNamespaceXName
    , xmlXName
    , xmlnsNamespaceXName
    , xmlnsXName
    , xmlnsQN
    , toNsEnv
    )
where
import           Control.Arrow                     ((***))
import           Control.DeepSeq
import           Control.FlatSeq
import           Data.AssocList
import           Data.Binary
import           Data.Char                         (toLower)
import           Data.IORef
import           Data.List                         (isPrefixOf)
import qualified Data.Map                          as M
import           Data.Typeable
import           System.IO.Unsafe                  (unsafePerformIO)
import           Text.XML.HXT.DOM.XmlKeywords      (a_xml, a_xmlns,
                                                    xmlNamespace,
                                                    xmlnsNamespace)
import           Data.Char.Properties.XMLCharProps (isXmlNCNameChar,
                                                    isXmlNCNameStartChar)
data XName                      = XN { _idXN :: ! Int        
                                     ,  unXN ::   String
                                     }
                                  deriving (Typeable)
instance Eq XName where
    (XN id1 _) == (XN id2 _)    = id1 == id2
instance Ord XName where
    compare (XN _ n1) (XN _ n2) = compare n1 n2
instance NFData XName where
    rnf (XN _ s)                = rnf s
instance WNFData XName where
    rwnf (XN _ s)               = rnf s
instance Binary XName where
    put (XN _ s)                = put s
    get                         = do
                                  s <- get
                                  return $! newXName s
type NsEnv              = AssocList XName XName
data QName      = QN { localPart'    :: ! XName
                     , namePrefix'   :: ! XName
                     , namespaceUri' :: ! XName
                     }
             deriving (Typeable)
instance Eq QName where
    (QN lp1 px1 ns1) == (QN lp2 px2 ns2)
        | ns1 /= ns2            = False                 
        | not (isNullXName ns1) = lp1 == lp2            
        | otherwise             = lp1 == lp2            
                                  &&                    
                                  px1 == px2
instance Ord QName where
  compare (QN lp1 px1 ns1) (QN lp2 px2 ns2)
      | isNullXName ns1 && isNullXName ns2              
          = compare (px1, lp1) (px2, lp2)
      | otherwise                                       
          = compare (lp1, ns1) (lp2, ns2)
instance NFData  QName where
    rnf x = seq x ()
instance WNFData QName
instance Show QName where
    show = showQN
instance Binary QName where
    put (QN lp px ns)   = put (unXN px) >>
                          put (unXN lp) >>
                          put (unXN ns)
    get                 = do
                          px <- get
                          lp <- get
                          ns <- get
                          return $! newNsName lp px ns
                          
                          
                          
isNullXName             :: XName -> Bool
isNullXName             = (== nullXName)
{-# INLINE isNullXName #-}
namePrefix              :: QName -> String
namePrefix              = unXN . namePrefix'
{-# INLINE namePrefix #-}
localPart               :: QName -> String
localPart               = unXN . localPart'
{-# INLINE localPart #-}
namespaceUri            :: QName -> String
namespaceUri            = unXN . namespaceUri'
{-# INLINE namespaceUri #-}
setNamespaceUri'                        :: XName -> QName -> QName
setNamespaceUri' ns (QN lp px _ns)      = newQName lp px ns
setLocalPart'                           :: XName -> QName -> QName
setLocalPart' lp (QN _lp px ns)         = newQName lp px ns
setNamePrefix'                          :: XName -> QName -> QName
setNamePrefix' px (QN lp _px ns)        = newQName lp px ns
qualifiedName                   :: QName -> String
qualifiedName (QN lp px _ns)
    | isNullXName px            = unXN lp
    | otherwise                 = unXN px ++ (':' : unXN lp)
qualifiedName'                   :: QName -> String -> String
qualifiedName' (QN lp px _ns)
    | isNullXName px            = (unXN lp ++)
    | otherwise                 = (unXN px ++) . (':' :) . (unXN lp ++)
universalName                   :: QName -> String
universalName                   = buildUniversalName (\ ns lp -> '{' : ns ++ '}' : lp)
universalUri                    :: QName -> String
universalUri                    = buildUniversalName (++)
buildUniversalName              :: (String -> String -> String) -> QName -> String
buildUniversalName bf n@(QN _lp _px ns)
    | isNullXName ns            = localPart n
    | otherwise                 = unXN ns `bf` localPart n
showQN                          :: QName -> String
showQN n
    | null ns                   = show $ qualifiedName n
    | otherwise                 = show $ "{" ++ ns ++ "}" ++ qualifiedName n
    where
    ns = namespaceUri n
mkQName'                        :: XName -> XName -> XName -> QName
mkQName' px lp ns               = newQName lp px ns
{-# DEPRECATED mkQName' "use newQName instead with lp px ns param seq " #-}
mkPrefixLocalPart               :: String -> String -> QName
mkPrefixLocalPart px lp
    | null px                   = newLpName lp
    | otherwise                 = newPxName lp px
mkName                          :: String -> QName
mkName n
    | (':' `elem` n)
      &&
      not (null px)                     
                                = newPxName lp px
    | otherwise                 = newLpName n
    where
    (px, (_ : lp)) = span (/= ':') n
mkQName                         :: String -> String -> String -> QName
mkQName px lp ns
    | null ns                   = mkPrefixLocalPart px lp
    | otherwise                 = newNsName lp px ns
mkSNsName                       :: String -> QName
mkSNsName                       = mkName
{-# DEPRECATED mkSNsName "use mkName instead" #-}
mkNsName                          :: String -> String -> QName
mkNsName n ns
    | null ns                   = qn
    | otherwise                 = setNamespaceUri' ns' qn
    where
    qn                          = mkName n
    ns'                         = newXName ns
equivQName                      :: QName -> QName -> Bool
equivQName                      = equalQNameBy equivUri
equivUri                        :: String -> String -> Bool
equivUri x y                    = normalizeNsUri x == normalizeNsUri y
equalQNameBy                    :: (String -> String -> Bool) -> QName -> QName -> Bool
equalQNameBy equiv q1 q2        = localPart q1 == localPart q2
                                  &&
                                  (namespaceUri q1 `equiv` namespaceUri q2)
normalizeNsUri                  :: String -> String
normalizeNsUri                  = map toLower . stripSlash
    where
    stripSlash ""               = ""
    stripSlash s
        | last s == '/'         = init s
        | otherwise             = s
setNamespace                    :: NsEnv -> QName -> QName
setNamespace env n@(QN lp px _ns)
                                = maybe n (\ ns -> newQName lp px ns) . lookup px $ env
isNCName                        :: String -> Bool
isNCName []                     = False
isNCName n                      = and ( zipWith ($)
                                        (isXmlNCNameStartChar : repeat isXmlNCNameChar)
                                        n
                                      )
isWellformedQualifiedName       :: String -> Bool
isWellformedQualifiedName s
    | null lp                   = isNCName px
    | otherwise                 = isNCName px && isNCName (tail lp)
    where
    (px, lp)                    = span (/= ':') s
isWellformedQName               :: QName -> Bool
isWellformedQName (QN lp px _ns)
                                = (isNCName . unXN) lp                          
                                  &&
                                  ( isNullXName px
                                    ||
                                    (isNCName . unXN) px                        
                                  )
isWellformedNSDecl              :: QName -> Bool
isWellformedNSDecl n
                                = not (isNameSpaceName n)
                                  ||
                                  isWellformedNameSpaceName n
isWellformedNameSpaceName       :: QName -> Bool
isWellformedNameSpaceName n@(QN lp px _ns)
    | isNullXName px            = lp == xmlnsXName
    | otherwise                 = px == xmlnsXName
                                  &&
                                  not (null lp')
                                  &&
                                  not (a_xml `isPrefixOf` lp')
    where
    lp'                         = localPart n
isNameSpaceName                         :: QName -> Bool
isNameSpaceName (QN lp px _ns)
    | isNullXName px                    = lp == xmlnsXName
    | otherwise                         = px == xmlnsXName
isDeclaredNamespace                     :: QName -> Bool
isDeclaredNamespace (QN _lp px ns)
    | isNullXName px                    = True                          
    | px == xmlnsXName                  = ns == xmlnsNamespaceXName     
    | px == xmlXName                    = ns == xmlNamespaceXName       
    | otherwise                         = not (isNullXName ns)          
toNsEnv                         :: AssocList String String -> NsEnv
toNsEnv                         = map (newXName *** newXName)
data NameCache          = NC { _newXN   :: ! Int                                       
                             , _xnCache :: ! (M.Map String XName)
                             , _qnCache :: ! (M.Map (XName, XName, XName) QName)       
                             }                                                          
                                                                                        
type ChangeNameCache r  = NameCache -> (NameCache, r)
theNameCache            :: IORef NameCache
theNameCache            = unsafePerformIO (newIORef $ initialCache)
{-# NOINLINE theNameCache #-}
initialXNames           :: [XName]
nullXName
 , xmlnsNamespaceXName
 , xmlnsXName
 , xmlNamespaceXName
 , xmlXName             :: XName
initialXNames@
 [ nullXName
 , xmlnsNamespaceXName
 , xmlnsXName
 , xmlNamespaceXName
 , xmlXName
 ]                      = zipWith XN [0..] $
                          [ ""
                          , xmlnsNamespace
                          , a_xmlns
                          , xmlNamespace
                          , a_xml
                          ]
initialQNames           :: [QName]
xmlnsQN                 :: QName
initialQNames@
 [xmlnsQN]              = [QN xmlnsXName nullXName xmlnsNamespaceXName]
initialCache            :: NameCache
initialCache            = NC
                          (length initialXNames)
                          (M.fromList $ map (\ xn -> (unXN xn, xn)) initialXNames)
                          (M.fromList $ map (\ qn@(QN lp px ns) -> ((lp, px, ns), qn)) initialQNames)
changeNameCache         :: NFData r => ChangeNameCache r -> r
changeNameCache action  = unsafePerformIO changeNameCache'
    where
    action' c =
      let r = action c
      in
       fst r `seq` r    
    changeNameCache' =
      do
      
      res <- atomicModifyIORef theNameCache action'
      
      return res
{-# NOINLINE changeNameCache #-}
newXName'               :: String -> ChangeNameCache XName
newXName' n c@(NC nxn xm qm)
                        = case M.lookup n xm of
                          Just xn       -> (c, xn)
                          Nothing       -> let nxn' = nxn + 1 in
                                           let xn   = (XN nxn n) in
                                           let xm'  = M.insert n xn xm in
                                           
                                           rnf xn `seq` (NC nxn' xm' qm, xn)
newQName'               :: XName -> XName -> XName -> ChangeNameCache QName
newQName' lp px ns c@(NC nxn xm qm)
                        = case M.lookup q' qm of
                          Just qn       -> 
                                           (c, qn)
                          Nothing       -> let qm'  = M.insert q' q qm in
                                           
                                           q `seq` (NC nxn xm qm', q)
    where
    q'                  = (lp, px, ns)
    q                   = QN lp px ns
andThen                 :: ChangeNameCache r1 ->
                           (r1 -> ChangeNameCache r2) -> ChangeNameCache r2
andThen a1 a2 c0        = let (c1, r1) = a1 c0 in
                          (a2 r1) c1
newXName                :: String -> XName
newXName n              = changeNameCache $
                          newXName' n
newQName                :: XName -> XName -> XName -> QName
newQName lp px ns       = lp `seq` px `seq` ns `seq`            
                          ( changeNameCache $
                            newQName' lp px ns
                          )
newLpName               :: String -> QName
newLpName lp            = changeNameCache $
                          newXName' lp `andThen` \ lp' ->
                          newQName' lp' nullXName nullXName
newPxName               :: String -> String -> QName
newPxName lp px         = changeNameCache $
                          newXName' lp `andThen` \ lp' ->
                          newXName' px `andThen` \ px' ->
                          newQName' lp' px' nullXName
newNsName               :: String -> String -> String -> QName
newNsName lp px ns      = changeNameCache $
                          newXName' lp `andThen` \ lp' ->
                          newXName' px `andThen` \ px' ->
                          newXName' ns `andThen` \ ns' ->
                          newQName' lp' px' ns'