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

-- | Defines the HTML DOM types and functions.
module Zenacy.HTML.Internal.DOM
  ( DOM(..)
  , DOMNode(..)
  , DOMAttr(..)
  , DOMType(..)
  , DOMQuirks(..)
  , DOMPos(..)
  , DOMID
  , DOMMap
  , domAttrMake
  , domDefaultDocument
  , domDefaultDoctype
  , domDefaultFragment
  , domDefaultElement
  , domDefaultTemplate
  , domDefaultText
  , domDefaultComment
  , domDefaultType
  , domMakeTypeHTML
  , domMakeTypeMathML
  , domMakeTypeSVG
  , domPos
  , domNull
  , domRoot
  , domRootPos
  , domDocument
  , domQuirksSet
  , domQuirksGet
  , domNewID
  , domGetNode
  , domPutNode
  , domInsert
  , domInsertNew
  , domAppend
  , domAppendNew
  , domElementHasAttr
  , domElementFindAttr
  , domElementAttrValue
  , domAttrMerge
  , domMatch
  , domLastChild
  , domMapID
  , domFindParent
  , domSetParent
  , domMapChild
  , domRemoveChild
  , domRemoveChildren
  , domMove
  , domMoveChildren
  , domChildren
  , domHasChild
  , domNodeID
  , domNodeParent
  , domNodeIsHTML
  , domNodeIsSVG
  , domNodeIsMathML
  , domNodeIsDocument
  , domNodeIsFragment
  , domNodeIsElement
  , domNodeIsTemplate
  , domNodeIsHtmlElement
  , domNodeIsText
  , domNodeElementName
  , domNodeElementNamespace
  , domNodeType
  , domTypesHTML
  , domTypesMathML
  , domTypesSVG
  , domRender
  ) where

import Zenacy.HTML.Internal.BS
import Zenacy.HTML.Internal.Core
import Zenacy.HTML.Internal.Types
import Data.Foldable
  ( toList
  )
import Data.List
  ( find
  )
import Data.Maybe
  ( fromJust
  , listToMaybe
  , isJust
  , mapMaybe
  )
import Data.Monoid
  ( (<>)
  )
import Data.Word
  ( Word8
  )
import Data.Default
  ( Default(..)
  )
import Data.IntMap
  ( IntMap
  )
import qualified Data.IntMap as IntMap
  ( singleton
  , lookup
  , insert
  , keys
  )
import Data.Sequence
  ( Seq(..)
  , ViewL(..)
  , ViewR(..)
  , (<|)
  , (|>)
  , (><)
  )
import qualified Data.Sequence as Seq
  ( breakl
  , empty
  , filter
  , viewl
  , viewr
  )

-- | DOM represents an HTML document while being parsed.
data DOM = DOM
  { domNodes  :: !DOMMap
  , domNextID :: !DOMID
  } deriving (Eq, Ord, Show)

-- | Defines an ID for a node in a document.
type DOMID = Int

-- | Defines a mapping between node references and nodes.
type DOMMap = IntMap DOMNode

-- | Node is the model type for an HTML document.
data DOMNode
  = DOMDocument
    { domDocumentID          :: DOMID
    , domDocumentParent      :: DOMID
    , domDocumentName        :: BS
    , domDocumentChildren    :: Seq DOMID
    , domDocumentQuirksMode  :: DOMQuirks
    }
  | DOMDoctype
    { domDoctypeID           :: DOMID
    , domDoctypeParent       :: DOMID
    , domDoctypeName         :: BS
    , domDoctypePublicID     :: Maybe BS
    , domDoctypeSystemID     :: Maybe BS
    }
  | DOMFragment
    { domFragmentID          :: DOMID
    , domFragmentParent      :: DOMID
    , domFragmentName        :: BS
    , domFragmentChildren    :: Seq DOMID
    }
  | DOMElement
    { domElementID           :: DOMID
    , domElementParent       :: DOMID
    , domElementName         :: BS
    , domElementNamespace    :: HTMLNamespace
    , domElementAttributes   :: Seq DOMAttr
    , domElementChildren     :: Seq DOMID
    }
  | DOMTemplate
    { domTemplateID          :: DOMID
    , domTemplateParent      :: DOMID
    , domTemplateNamespace   :: HTMLNamespace
    , domTemplateAttributes  :: Seq DOMAttr
    , domTemplateContents    :: DOMID
    }
  | DOMText
    { domTextID              :: DOMID
    , domTextParent          :: DOMID
    , domTextData            :: BS
    }
  | DOMComment
    { domCommentID           :: DOMID
    , domCommentParent       :: DOMID
    , domCommentData         :: BS
    }
    deriving (Eq, Ord, Show)

-- | An HTML element attribute type.
data DOMAttr = DOMAttr
  { domAttrName      :: BS
  , domAttrVal       :: BS
  , domAttrNamespace :: HTMLAttrNamespace
  } deriving (Eq, Ord, Show)


-- | Identifies the type of an element.
data DOMType = DOMType
  { domTypeName      :: BS
  , domTypeNamespace :: HTMLNamespace
  } deriving (Eq, Ord, Show)

-- | Indentifies the quirks mode.
data DOMQuirks
  = DOMQuirksNone
  | DOMQuirksMode
  | DOMQuirksLimited
    deriving (Eq, Ord, Show)

-- | Defines a position in the DOM.
data DOMPos = DOMPos
  { domPosParent :: DOMID
  , domPosChild  :: Maybe DOMID
  } deriving (Eq, Ord, Show)

-- | Defines a default DOM.
instance Default DOM where
  def = DOM
    { domNodes = IntMap.singleton domRoot domDefaultDocument
    , domNextID = domRoot + 1
    }

-- | Defines a default attribute.
instance Default DOMAttr where
  def = DOMAttr
    { domAttrName      = bsEmpty
    , domAttrVal       = bsEmpty
    , domAttrNamespace = def
    }

-- | Makes an attribute.
domAttrMake :: BS -> BS -> DOMAttr
domAttrMake n v = DOMAttr n v def

-- | Defines a default document.
domDefaultDocument :: DOMNode
domDefaultDocument =
  DOMDocument
  { domDocumentID         = domNull
  , domDocumentName       = bsEmpty
  , domDocumentChildren   = Seq.empty
  , domDocumentQuirksMode = DOMQuirksNone
  , domDocumentParent     = domNull
  }

-- | Defines a default document type.
domDefaultDoctype :: DOMNode
domDefaultDoctype =
  DOMDoctype
  { domDoctypeID       = domNull
  , domDoctypeName     = bsEmpty
  , domDoctypePublicID = Nothing
  , domDoctypeSystemID = Nothing
  , domDoctypeParent   = domNull
  }

-- | Defines a default document fragment.
domDefaultFragment :: DOMNode
domDefaultFragment =
  DOMFragment
  { domFragmentID       = domNull
  , domFragmentName     = bsEmpty
  , domFragmentChildren = Seq.empty
  , domFragmentParent   = domNull
  }

-- | Defines a default element.
domDefaultElement :: DOMNode
domDefaultElement =
  DOMElement
  { domElementID         = domNull
  , domElementName       = bsEmpty
  , domElementNamespace  = HTMLNamespaceHTML
  , domElementAttributes = Seq.empty
  , domElementChildren   = Seq.empty
  , domElementParent     = domNull
  }

-- | Defines a default template.
domDefaultTemplate :: DOMNode
domDefaultTemplate =
  DOMTemplate
  { domTemplateID         = domNull
  , domTemplateNamespace  = HTMLNamespaceHTML
  , domTemplateAttributes = Seq.empty
  , domTemplateContents   = domNull
  , domTemplateParent     = domNull
  }

-- | Defines a default text.
domDefaultText :: DOMNode
domDefaultText =
  DOMText
  { domTextID     = domNull
  , domTextData   = bsEmpty
  , domTextParent = domNull
  }

-- | Defines a default comment.
domDefaultComment :: DOMNode
domDefaultComment =
  DOMComment
  { domCommentID     = domNull
  , domCommentData   = bsEmpty
  , domCommentParent = domNull
  }

-- | Defines a default type.
domDefaultType :: DOMType
domDefaultType = domMakeTypeHTML bsEmpty

-- | Makes a new HTML element type.
domMakeTypeHTML :: BS -> DOMType
domMakeTypeHTML = flip DOMType HTMLNamespaceHTML

-- | Makes a new MathML element type.
domMakeTypeMathML :: BS -> DOMType
domMakeTypeMathML = flip DOMType HTMLNamespaceMathML

-- | Makes a new SVG element type.
domMakeTypeSVG :: BS -> DOMType
domMakeTypeSVG = flip DOMType HTMLNamespaceSVG

-- | Makes a new position.
domPos :: DOMID -> DOMPos
domPos x = DOMPos x Nothing

-- | The null node ID.
domNull :: DOMID
domNull = 0

-- | The root document node ID.
domRoot :: DOMID
domRoot = 1

-- | Defines an appending position in a document node.
domRootPos :: DOMPos
domRootPos = domPos domRoot

-- | Gets the document node for a DOM.
domDocument :: DOM -> DOMNode
domDocument d = fromJust $ IntMap.lookup domRoot $ domNodes d

-- | Sets the quirks mode for a document.
domQuirksSet :: DOMQuirks -> DOM -> DOM
domQuirksSet x d = domPutNode domRoot y' d
  where
    y = domDocument d
    y' = y { domDocumentQuirksMode = x }

-- | Gets the quirks mode for a document.
domQuirksGet :: DOM -> DOMQuirks
domQuirksGet = domDocumentQuirksMode . domDocument

-- | Adds a new node to a DOM.
domNewID :: DOM -> DOMNode -> (DOM, DOMID)
domNewID d n = (d', i)
  where
    i = domNextID d
    n' = domSetID n i
    d' = d { domNodes = IntMap.insert i n' $ domNodes d
           , domNextID = i + 1
           }

-- | Sets the ID for a node.
domSetID :: DOMNode -> DOMID -> DOMNode
domSetID x y =
  case x of
    DOMDocument{} -> x { domDocumentID = y }
    DOMDoctype{}  -> x { domDoctypeID = y }
    DOMFragment{} -> x { domFragmentID = y }
    DOMElement{}  -> x { domElementID = y }
    DOMTemplate{} -> x { domTemplateID = y }
    DOMText{}     -> x { domTextID = y }
    DOMComment{}  -> x { domCommentID = y }

-- | Gets a node for a node ID.
domGetNode :: DOM -> DOMID -> Maybe DOMNode
domGetNode d x = IntMap.lookup x $ domNodes d

-- | Updates a node in the DOM.
domPutNode :: DOMID -> DOMNode -> DOM -> DOM
domPutNode x n d = d { domNodes = IntMap.insert x n $ domNodes d }

-- | Inserts a node at a position.
domInsert :: DOMPos -> DOMID -> DOM -> DOM
domInsert p@(DOMPos r c) x d =
  case domGetNode d r of
    Just n@(DOMDocument { domDocumentChildren = a }) ->
      f $ n { domDocumentChildren = g a }
    Just n@(DOMElement { domElementChildren = a }) ->
      f $ n { domElementChildren = g a }
    Just n@(DOMFragment { domFragmentChildren = a }) ->
      f $ n { domFragmentChildren = g a }
    Just n@(DOMTemplate { domTemplateContents = a }) ->
      domInsert (DOMPos a c) x d
    _otherwise -> d
  where
    f a = domSetParent x r (domPutNode r a d)
    g = domInsertChild p x

-- | Inserts a node at a position.
domInsertNew :: DOMPos -> DOMNode -> DOM -> (DOM, DOMID)
domInsertNew p x d =
  (domInsert p i d', i)
  where
    (d', i) = domNewID d x

-- | Inserts a child in a list of children.
domInsertChild :: DOMPos -> DOMID -> Seq DOMID -> Seq DOMID
domInsertChild (DOMPos _ Nothing) x = (|> x)
domInsertChild (DOMPos _ (Just a)) x = seqInsertBefore (==a) x

-- | Appends a node ID to a node.
domAppend :: DOMID -> DOMID -> DOM -> DOM
domAppend x y d =
  case domGetNode d x of
    Just (DOMDocument i p n c q) ->
      f $ DOMDocument i p n (c |> y) q
    Just (DOMElement i p n s a c) ->
      f $ DOMElement i p n s a (c |> y)
    Just (DOMFragment i p n c) ->
      f $ DOMFragment i p n (c |> y)
    Just (DOMTemplate _ _ _ _ c) ->
      domAppend c y d
    _otherwise -> d
  where
    f a = domSetParent y x (domPutNode x a d)

-- | Appends a node to a node.
domAppendNew :: DOMID -> DOMNode -> DOM -> DOM
domAppendNew x y d = domAppend x i d'
  where (d', i) = domNewID d y

-- | Finds an attribute for an element.
domElementFindAttr :: DOMNode -> BS -> Maybe DOMAttr
domElementFindAttr node name = case node of
  DOMElement{..} -> f domElementAttributes
  DOMTemplate{..} -> f domTemplateAttributes
  _otherwise -> Nothing
  where
    f = seqFind (\DOMAttr{..} -> domAttrName == name)

-- | Gets the last element in a sequence if it exists.
seqLast :: Seq a -> Maybe a
seqLast (Seq.viewr -> EmptyR) = Nothing
seqLast (Seq.viewr -> _ :> a) = Just a
seqLast _ = Nothing

-- | Finds an element in a sequence.
seqFind :: (a -> Bool) -> Seq a -> Maybe a
seqFind f x = go x
  where
    go (Seq.viewl -> EmptyL) = Nothing
    go (Seq.viewl -> a :< b) = if f a then Just a else go b
    go _ = Nothing

-- | Inserts an element into a sequence before the element satisfying a predicate.
seqInsertBefore :: (a -> Bool) -> a -> Seq a -> Seq a
seqInsertBefore f x y =
  (a |> x) <> b
  where
    (a, b) = Seq.breakl f y

-- | Finds an attribute value for an element.
domElementAttrValue :: DOMNode -> BS -> Maybe BS
domElementAttrValue x n = domAttrVal <$> domElementFindAttr x n

-- | Determines if a node has a named attribute.
domElementHasAttr  :: DOMNode -> BS -> Bool
domElementHasAttr x = isJust . domElementFindAttr x

-- | Merges attributes into an existing node.
domAttrMerge :: DOMID -> Seq DOMAttr -> DOM -> DOM
domAttrMerge x y d =
  case domGetNode d x of
    Just n@(DOMElement { domElementAttributes = a }) ->
      domPutNode x (n { domElementAttributes = a <> f n y }) d
    Just n@(DOMTemplate { domTemplateAttributes = a }) ->
      domPutNode x (n { domTemplateAttributes = a <> f n y }) d
    _otherwise -> d
  where
    f n = Seq.filter (not . domElementHasAttr n . domAttrName)

-- | Detmermines if two elements match.
domMatch :: DOM -> DOMID -> DOMID -> Bool
domMatch d i j =
  case (domGetNode d i, domGetNode d j) of
    (Just (DOMElement _ _ n1 s1 a1 _), Just (DOMElement _ _ n2 s2 a2 _)) ->
      n1 == n2 && s1 == s2 && a1 == a1
    (Just (DOMTemplate _ _ s1 a1 _ ), Just (DOMTemplate _ _ s2 a2 _)) ->
      s1 == s2 && a1 == a1
    _otherwise ->
      False

-- | Returns the last child of a node if it exists.
domLastChild :: DOM -> DOMID -> Maybe DOMID
domLastChild d x =
  domGetNode d x >>= \case
    DOMDocument{..} -> seqLast domDocumentChildren
    DOMFragment{..} -> seqLast domFragmentChildren
    DOMElement{..}  -> seqLast domElementChildren
    DOMTemplate{..} -> domLastChild d domTemplateContents
    _otherwise -> Nothing

-- | Converts a list of node IDs to nodes.
domMapID :: DOM -> [DOMID] -> [DOMNode]
domMapID d = mapMaybe $ domGetNode d

-- | Finds the parent node for a node.
domFindParent :: DOM -> DOMID -> Maybe DOMID
domFindParent d x = find (domHasChild d x) $ IntMap.keys $ domNodes d

-- | Sets the parent for a node.
domSetParent :: DOMID -> DOMID -> DOM -> DOM
domSetParent x y d =
  case domGetNode d x of
    Just a -> case a of
      DOMDocument{} -> f a { domDocumentParent = y }
      DOMDoctype{}  -> f a { domDoctypeParent = y }
      DOMFragment{} -> f a { domFragmentParent = y }
      DOMElement{}  -> f a { domElementParent = y }
      DOMTemplate{} -> f a { domTemplateParent = y }
      DOMText{}     -> f a { domTextParent = y }
      DOMComment{}  -> f a { domCommentParent = y }
    Nothing -> d
  where
    f z = domPutNode x z d

-- | Maps a function over children of a node.
domMapChild :: DOMID -> (Seq DOMID -> Seq DOMID)-> DOM -> DOM
domMapChild x f d =
  case domGetNode d x of
    Just a -> case a of
      DOMDocument { domDocumentChildren = c } ->
        domPutNode x a { domDocumentChildren = f c } d
      DOMFragment { domFragmentChildren = c } ->
        domPutNode x a { domFragmentChildren = f c } d
      DOMElement { domElementChildren = c } ->
        domPutNode x a { domElementChildren = f c } d
      DOMTemplate { domTemplateContents = c } ->
        domMapChild c f d
      _otherwise -> d
    Nothing -> d

-- | Removes a child from a node.
domRemoveChild :: DOMID -> DOMID -> DOM -> DOM
domRemoveChild parent child = domMapChild parent $ Seq.filter (/=child)

-- | Removes all the children from a node.
domRemoveChildren :: DOMID -> DOM -> DOM
domRemoveChildren x = domMapChild x $ const Seq.empty

-- | Moves a node to another parent.
domMove :: DOMID -> DOMID -> DOM -> DOM
domMove x newParent d =
  case domGetNode d x of
    Just a ->
      let d' = domRemoveChild (domNodeParent a) x d
      in domAppend newParent x d'
    Nothing -> d

-- | Moves the children of a node to another node.
domMoveChildren :: DOMID -> DOMID -> DOM -> DOM
domMoveChildren x y d =
  foldl (\d' c -> domAppend y c d') (domRemoveChildren x d) $ domChildren d x

-- | Gets the children of a node.
domChildren :: DOM -> DOMID -> Seq DOMID
domChildren d x =
  case domGetNode d x of
    Just (DOMDocument{..}) -> domDocumentChildren
    Just (DOMFragment{..}) -> domFragmentChildren
    Just (DOMElement{..})  -> domElementChildren
    Just (DOMTemplate{..}) -> domChildren d domTemplateContents
    _otherwise             -> Seq.empty

-- | Determines if a node has a specific child.
domHasChild :: DOM -> DOMID -> DOMID -> Bool
domHasChild d x z = z `elem` domChildren d x

-- | Gets the id for a node.
domNodeID :: DOMNode -> DOMID
domNodeID = \case
  DOMDocument{..} -> domDocumentID
  DOMDoctype{..}  -> domDoctypeID
  DOMFragment{..} -> domFragmentID
  DOMElement{..}  -> domElementID
  DOMTemplate{..} -> domTemplateID
  DOMText{..}     -> domTextID
  DOMComment{..}  -> domCommentID

-- | Gets the parent for a node.
domNodeParent :: DOMNode -> DOMID
domNodeParent = \case
  DOMDocument{..} -> domDocumentParent
  DOMDoctype{..}  -> domDoctypeParent
  DOMFragment{..} -> domFragmentParent
  DOMElement{..}  -> domElementParent
  DOMTemplate{..} -> domTemplateParent
  DOMText{..}     -> domTextParent
  DOMComment{..}  -> domCommentParent

-- | Detmermines if a node is in the HTML namespace.
domNodeIsHTML :: DOMNode -> Bool
domNodeIsHTML = \case
  DOMElement{..}  -> domElementNamespace == HTMLNamespaceHTML
  DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceHTML
  _otherwise      -> False

-- | Detmermines if a node is in the SVG namespace.
domNodeIsSVG :: DOMNode -> Bool
domNodeIsSVG = \case
  DOMElement{..}  -> domElementNamespace == HTMLNamespaceSVG
  DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceSVG
  _otherwise      -> False

-- | Detmermines if a node is in the MathML namespace.
domNodeIsMathML :: DOMNode -> Bool
domNodeIsMathML = \case
  DOMElement{..}  -> domElementNamespace == HTMLNamespaceMathML
  DOMTemplate{..} -> domTemplateNamespace == HTMLNamespaceMathML
  _otherwise      -> False

-- | Detmermines if a node is a document node.
domNodeIsDocument :: DOMNode -> Bool
domNodeIsDocument DOMDocument{} = True
domNodeIsDocument _ = False

-- | Detmermines if a node is a document fragment node.
domNodeIsFragment :: DOMNode -> Bool
domNodeIsFragment DOMFragment{} = True
domNodeIsFragment _ = False

-- | Detmermines if a node is an element node.
domNodeIsElement :: DOMNode -> Bool
domNodeIsElement DOMElement{} = True
domNodeIsElement _ = False

-- | Detmermines if a node is a template node.
domNodeIsTemplate :: DOMNode -> Bool
domNodeIsTemplate DOMTemplate{} = True
domNodeIsTemplate _ = False

-- | Detmermines if a node is an HTML element node.
domNodeIsHtmlElement :: DOMNode -> Bool
domNodeIsHtmlElement x = domNodeIsElement x && domNodeIsHTML x

-- | Detmermines if a node is a text node.
domNodeIsText :: DOMNode -> Bool
domNodeIsText DOMText{} = True
domNodeIsText _ = False

-- | Gets the name for an element node.
domNodeElementName :: DOMNode -> BS
domNodeElementName DOMElement{..} = domElementName
domNodeElementName DOMTemplate{} = "template"
domNodeElementName _ = ""

-- | Gets the name for an element node.
domNodeElementNamespace :: DOMNode -> HTMLNamespace
domNodeElementNamespace DOMElement{..} = domElementNamespace
domNodeElementNamespace DOMTemplate{..} = domTemplateNamespace
domNodeElementNamespace _ = HTMLNamespaceHTML

-- | Gets the type for an element node.
domNodeType :: DOMNode -> DOMType
domNodeType x = DOMType (domNodeElementName x) (domNodeElementNamespace x)

-- | Gets a list of HTML types for element names.
domTypesHTML :: [BS] -> [DOMType]
domTypesHTML = map domMakeTypeHTML

-- | Gets a list of MathML types for element names.
domTypesMathML :: [BS] -> [DOMType]
domTypesMathML = map domMakeTypeMathML

-- | Gets a list of SVG types for element names.
domTypesSVG :: [BS] -> [DOMType]
domTypesSVG = map domMakeTypeSVG

-- | Renders the DOM.
domRender :: DOM -> BS
domRender d = domRenderIndent d 0 domRoot

-- | Renders the DOM with indentaion.
domRenderIndent :: DOM -> Int -> DOMID -> BS
domRenderIndent d x y =
  case fromJust (domGetNode d y) of
    DOMDocument{..} ->
      bsConcat $ map (domRenderIndent d x) $ toList domDocumentChildren
    DOMDoctype{} ->
      bsEmpty
    DOMFragment{..} ->
      bsConcat $ map (domRenderIndent d x) $ toList domFragmentChildren
    DOMElement{..} ->
      bsConcat
        [ indent
        , domElementName
        , "\n"
        , bsConcat $ map (domRenderIndent d $ x + 1) $ toList domElementChildren
        ]
    DOMTemplate{..} ->
      bsConcat
        [ indent
        , "template"
        , "\n"
        , domRenderIndent d (x + 1) domTemplateContents
        ]
    DOMText{..} ->
      bsConcat
        [ indent
        , domTextData
        , "\n"
        ]
    DOMComment{} ->
      bsEmpty
  where
    indent = bsPack $ take x $ repeat 0x20
