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

-- | Defines types for zipping and iterating over HTML trees.
module Zenacy.HTML.Internal.Zip
  ( HTMLZipper
  , HTMLZipAction
  , HTMLIter
  , HTMLZipPath(..)
  , htmlZip
  , htmlZipM
  , htmlUnzip
  , htmlUnzipM
  , htmlZipNode
  , htmlZipNodeM
  , htmlZipRoot
  , htmlZipRootM
  , htmlZipUp
  , htmlZipParent
  , htmlZipFirst
  , htmlZipLast
  , htmlZipFind
  , htmlZipNext
  , htmlZipPrev
  , htmlZipGet
  , htmlZipTest
  , htmlZipTestNode
  , htmlZipTestName
  , htmlZipTestFirst
  , htmlZipTestLast
  , htmlZipModify
  , htmlZipModifyM
  , htmlZipDelete
  , htmlZipCollapse
  , htmlZipInsertBefore
  , htmlZipInsertAfter
  , htmlZipContentBefore
  , htmlZipContentAfter
  , htmlZipContentLeft
  , htmlZipContentRight
  , htmlZipDropBefore
  , htmlZipDropAfter
  , htmlZipDropLeft
  , htmlZipDropRight
  , htmlZipPruneBefore
  , htmlZipPruneAfter
  , htmlZipPruneLeft
  , htmlZipPruneRight
  , htmlZipRepeat
  , htmlZipStepNext
  , htmlZipStepBack
  , htmlZipSearch
  , htmlZipIndex
  , htmlIter
  , htmlIterZipper
  , htmlIterSearch
  , htmlIterModify
  , htmlIterNext
  , htmlIterBack
  , htmlZipPath
  , htmlZipPathEmpty
  , htmlZipPathFind
  ) where

import Zenacy.HTML.Internal.Core
import Zenacy.HTML.Internal.HTML
import Zenacy.HTML.Internal.Oper
import Control.Monad
  ( (>=>)
  )
import Data.Bool
  ( bool
  )
import Data.Default
  ( Default(..)
  )
import Data.Maybe
  ( fromMaybe
  , isNothing
  )
import Data.Monoid
  ( (<>)
  )
import Data.Text
  ( Text
  )
import qualified Data.Text as T
  ( unpack
  )

-- | The zipper crumb definition.
data HTMLCrumb = HTMLCrumb HTMLNode [HTMLNode] [HTMLNode]

-- | The zipper type.
data HTMLZipper = HTMLZipper HTMLNode [HTMLCrumb]

-- | Defines an action on a zipper.
type HTMLZipAction = HTMLZipper -> Maybe HTMLZipper

-- | Defines a zip direction.
data Direction = Down | Across | Up

-- | The zipper iterator type.
data HTMLIter = HTMLIter Direction HTMLZipper

-- | Defines the type for a path.
newtype HTMLZipPath = HTMLZipPath [Int] deriving (Show, Eq, Ord)

-- | Defaults for zip path.
instance Default HTMLZipPath where
  def = htmlZipPathEmpty

-- | Creates a zipper for a HTML node.
htmlZip :: HTMLNode -> HTMLZipper
htmlZip x = HTMLZipper x []

-- | Creates a zipper for a HTML node in a Monad.
htmlZipM :: Monad m => HTMLNode -> m HTMLZipper
htmlZipM = pure . htmlZip

-- | Extracts the HTML node from a zipper.
htmlUnzip :: HTMLZipper -> HTMLNode
htmlUnzip = htmlZipNode . htmlZipRoot

-- | Extracts the HTML node from a zipper in a Monad.
htmlUnzipM :: Monad m => HTMLZipper -> m HTMLNode
htmlUnzipM = pure . htmlUnzip

-- | Gets the current HTML node.
htmlZipNode :: HTMLZipper -> HTMLNode
htmlZipNode (HTMLZipper x _) = x

-- | Gets the current HTML node in a Monad.
htmlZipNodeM :: Monad m => HTMLZipper -> m HTMLNode
htmlZipNodeM = pure . htmlZipNode

-- | Moves the zipper to the root HTML node.
htmlZipRoot :: HTMLZipper -> HTMLZipper
htmlZipRoot x = maybe x htmlZipRoot $ htmlZipParent x

-- | Moves the zipper to the root HTML node in a Monad.
htmlZipRootM :: Monad m => HTMLZipper -> m HTMLZipper
htmlZipRootM = pure . htmlZipRoot

-- | Moves the zipper to the parent node.
htmlZipUp :: HTMLZipper -> Maybe HTMLZipper
htmlZipUp = htmlZipParent

-- | Moves the zipper to the parent node.
htmlZipParent :: HTMLZipper -> Maybe HTMLZipper
htmlZipParent = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n ls rs):cs) ->
    let c = reverse ls <> [x] <> rs
    in case n of
      HTMLDocument n [] ->
        Just $ HTMLZipper (HTMLDocument n c) cs
      HTMLDoctype n p s ->
        Nothing
      HTMLFragment n [] ->
        Just $ HTMLZipper (HTMLFragment n c) cs
      HTMLElement n s a [] ->
        Just $ HTMLZipper (HTMLElement n s a c) cs
      HTMLTemplate s a c ->
        Nothing
      HTMLText t ->
        Nothing
      HTMLComment c ->
        Nothing

-- | Moves the zipper to the first child node.
htmlZipFirst :: HTMLZipper -> Maybe HTMLZipper
htmlZipFirst (HTMLZipper y z) = case y of
  HTMLDocument n c ->
    f c $ HTMLDocument n []
  HTMLFragment n c ->
    f c $ HTMLFragment n []
  HTMLElement n s a c ->
    f c $ HTMLElement n s a []
  _ -> Nothing
  where
    f [] n = Nothing
    f (h:rs) n = Just $ HTMLZipper h ((HTMLCrumb n [] rs):z)

-- | Moves the zipper to the last child node.
htmlZipLast :: HTMLZipper -> Maybe HTMLZipper
htmlZipLast (HTMLZipper y z) = case y of
  HTMLDocument n c ->
    f c $ HTMLDocument n []
  HTMLFragment n c ->
    f c $ HTMLFragment n []
  HTMLElement n s a c ->
    f c $ HTMLElement n s a []
  _ -> Nothing
  where
    f [] n = Nothing
    f xs n = let (h:ls) = reverse xs in Just $
      HTMLZipper h ((HTMLCrumb n ls []):z)

-- | Moves the zipper to a named child element.
htmlZipFind :: (HTMLNode -> Bool) -> HTMLZipper -> Maybe HTMLZipper
htmlZipFind p (HTMLZipper y z) = case y of
  HTMLDocument n c ->
    f c $ HTMLDocument n []
  HTMLFragment n c ->
    f c $ HTMLFragment n []
  HTMLElement n s a c ->
    f c $ HTMLElement n s a []
  _ -> Nothing
  where
    f c n = case break p c of
      (ls, []) -> Nothing
      (ls, h:rs) -> Just $
        HTMLZipper h ((HTMLCrumb n (reverse ls) rs):z)

-- | Moves to the next sibling.
htmlZipNext :: HTMLZipper -> Maybe HTMLZipper
htmlZipNext = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n ls []):cs) -> Nothing
  HTMLZipper x ((HTMLCrumb n ls (h:rs)):cs) ->
    Just $ HTMLZipper h ((HTMLCrumb n (x:ls) rs):cs)

-- | Moves to the previous sibling.
htmlZipPrev :: HTMLZipper -> Maybe HTMLZipper
htmlZipPrev = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n [] rs):cs) -> Nothing
  HTMLZipper x ((HTMLCrumb n (h:ls) rs):cs) ->
    Just $ HTMLZipper h ((HTMLCrumb n ls (x:rs)):cs)

-- | Gets the child specified by an index.
htmlZipGet :: Int -> HTMLZipper -> Maybe HTMLZipper
htmlZipGet n z
  | n < 0 = Nothing
  | n == 0 = htmlZipFirst z
  | otherwise = htmlZipFirst z >>= f n
  where
    f 0 = Just
    f n = htmlZipNext >=> f (n - 1)

-- | Continues a zipper if a test is passed.
htmlZipTest :: (HTMLZipper -> Bool) -> HTMLZipper -> Maybe HTMLZipper
htmlZipTest f z = bool Nothing (Just z) $ f z

-- | Continues a zipper if a node test is passed.
htmlZipTestNode :: (HTMLNode -> Bool) -> HTMLZipper -> Maybe HTMLZipper
htmlZipTestNode f = htmlZipTest $ f . htmlZipNode

-- | Tests the current node for an element name.
htmlZipTestName :: Text -> HTMLZipper -> Maybe HTMLZipper
htmlZipTestName x = htmlZipTest (htmlElemHasName x . htmlZipNode)

-- | Test whether the zipper is at the first child node.
htmlZipTestFirst :: HTMLZipper -> Maybe HTMLZipper
htmlZipTestFirst = htmlZipTest (isNothing . htmlZipPrev)

-- | Test whether the zipper is at the last child node.
htmlZipTestLast :: HTMLZipper -> Maybe HTMLZipper
htmlZipTestLast = htmlZipTest (isNothing . htmlZipNext)

-- | Modifies the currently focused node.
htmlZipModify :: (HTMLNode -> HTMLNode) -> HTMLZipper -> HTMLZipper
htmlZipModify f (HTMLZipper y z) = HTMLZipper (f y) z

-- | Modifies the currently focused node in a Monad.
htmlZipModifyM :: Monad m => (HTMLNode -> HTMLNode) -> HTMLZipper -> m HTMLZipper
htmlZipModifyM f = pure . htmlZipModify f

-- | Deletes the current node.
htmlZipDelete :: HTMLZipper -> Maybe HTMLZipper
htmlZipDelete = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n l r):cs) ->
    let c = reverse l <> r
    in case n of
      HTMLDocument n [] ->
        Just $ HTMLZipper (HTMLDocument n c) cs
      HTMLFragment n [] ->
        Just $ HTMLZipper (HTMLFragment n c) cs
      HTMLElement n s a [] ->
        Just $ HTMLZipper (HTMLElement n s a c) cs
      _ -> Nothing

-- | Collapses the current node.
htmlZipCollapse :: HTMLZipper -> Maybe HTMLZipper
htmlZipCollapse = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n l r):cs) ->
    let c = reverse l <> htmlNodeContent x <> r
    in case n of
      HTMLDocument n [] ->
        Just $ HTMLZipper (HTMLDocument n c) cs
      HTMLFragment n [] ->
        Just $ HTMLZipper (HTMLFragment n c) cs
      HTMLElement n s a [] ->
        Just $ HTMLZipper (HTMLElement n s a c) cs
      _ -> Nothing

-- | Inserts a node before the current node.
htmlZipInsertBefore :: HTMLNode -> HTMLZipper -> Maybe HTMLZipper
htmlZipInsertBefore y = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n l r):cs) ->
    Just $ HTMLZipper x ((HTMLCrumb n (y:l) r):cs)

-- | Inserts a node after the current node.
htmlZipInsertAfter :: HTMLNode -> HTMLZipper -> Maybe HTMLZipper
htmlZipInsertAfter y = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n l r):cs) ->
    Just $ HTMLZipper x ((HTMLCrumb n l (y:r)):cs)

-- | Gets the siblings to the left of the current node.
htmlZipContentBefore :: HTMLZipper -> [HTMLNode]
htmlZipContentBefore = \case
  HTMLZipper x [] -> []
  HTMLZipper x ((HTMLCrumb n l r):cs) -> reverse l

-- | Gets the siblings to the right of the current node.
htmlZipContentAfter :: HTMLZipper -> [HTMLNode]
htmlZipContentAfter = \case
  HTMLZipper x [] -> []
  HTMLZipper x ((HTMLCrumb n l r):cs) -> r

-- | Synonym for htmlZipContentBefore.
htmlZipContentLeft :: HTMLZipper -> [HTMLNode]
htmlZipContentLeft = htmlZipContentBefore

-- | Synonym for htmlZipContentAfter.
htmlZipContentRight :: HTMLZipper -> [HTMLNode]
htmlZipContentRight = htmlZipContentAfter

-- | Drops the siblings to the left of the current node.
htmlZipDropBefore :: HTMLZipper -> Maybe HTMLZipper
htmlZipDropBefore = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n _ r):cs) ->
    Just $ HTMLZipper x ((HTMLCrumb n [] r):cs)

-- | Drops the siblings to the right of the current node.
htmlZipDropAfter :: HTMLZipper -> Maybe HTMLZipper
htmlZipDropAfter = \case
  HTMLZipper x [] -> Nothing
  HTMLZipper x ((HTMLCrumb n l _):cs) ->
    Just $ HTMLZipper x ((HTMLCrumb n l []):cs)

-- | Synonym for htmlZipDropBefore.
htmlZipDropLeft :: HTMLZipper -> Maybe HTMLZipper
htmlZipDropLeft = htmlZipDropBefore

-- | Synonym for htmlZipDropAfter.
htmlZipDropRight :: HTMLZipper -> Maybe HTMLZipper
htmlZipDropRight = htmlZipDropAfter

-- | Drops all of the branches to the left of the current node
--   while moving up to and ending at the root.
htmlZipPruneBefore :: HTMLZipper -> Maybe HTMLZipper
htmlZipPruneBefore = htmlZipRepeat safeDrop htmlZipParent
  where
    safeDrop z = Just $ fromMaybe z $ htmlZipDropBefore z

-- | Drops all of the branches to the right of the current node
--   while moving up to and ending at the root.
htmlZipPruneAfter :: HTMLZipper -> Maybe HTMLZipper
htmlZipPruneAfter = htmlZipRepeat safeDrop htmlZipParent
  where
    safeDrop z = Just $ fromMaybe z $ htmlZipDropAfter z

-- | Synonym for htmlZipPruneBefore.
htmlZipPruneLeft :: HTMLZipper -> Maybe HTMLZipper
htmlZipPruneLeft = htmlZipPruneBefore

-- | Synonym for htmlZipPruneAfter.
htmlZipPruneRight :: HTMLZipper -> Maybe HTMLZipper
htmlZipPruneRight = htmlZipPruneAfter

-- | Repeats a zipper action until another zipper returns Nothing.
htmlZipRepeat :: HTMLZipAction -> HTMLZipAction -> HTMLZipAction
htmlZipRepeat f g z =
  case f z of
    Nothing -> Nothing
    Just z1 -> case g z1 of
      Nothing -> Just z1
      Just z2 -> htmlZipRepeat f g z2

-- | Step a zipper forward one node.
htmlZipStepNext :: HTMLZipper -> Maybe HTMLZipper
htmlZipStepNext = htmlZipStep htmlZipFirst htmlZipNext

-- | Step a zipper back one node.
htmlZipStepBack :: HTMLZipper -> Maybe HTMLZipper
htmlZipStepBack = htmlZipStep htmlZipLast htmlZipPrev

-- | Step a zipper.
htmlZipStep :: HTMLZipAction -> HTMLZipAction -> HTMLZipAction
htmlZipStep first next z =
  case first z of
    Just x -> Just x
    Nothing -> f z
  where
    f x = case next x of
      Just a -> Just a
      Nothing -> case htmlZipParent x of
        Just b -> f b
        Nothing -> Nothing

-- | Searches a zipper until a predicate is true.
htmlZipSearch
  :: (HTMLZipper -> Maybe HTMLZipper)
  -> (HTMLZipper -> Bool)
  -> HTMLZipper
  -> Maybe HTMLZipper
htmlZipSearch step test x
  | test x = Just x
  | otherwise = maybe Nothing (htmlZipSearch step test) $ step x

-- | Gets the index for a node.
htmlZipIndex :: HTMLZipper -> Maybe Int
htmlZipIndex = \case
  HTMLZipper _ [] -> Nothing
  HTMLZipper _ ((HTMLCrumb _ ls _):_) -> Just $ length ls

-- | Dumps a zipper to a string.
htmlZipDump :: HTMLZipper -> String
htmlZipDump (HTMLZipper n cs) =
  name n <> "\n" <> go cs
  where
    go :: [HTMLCrumb] -> String
    go [] = ""
    go ((HTMLCrumb n ls rs):cs) =
      name n <> "\n"
      <> " ls: " <> names ls <> "\n"
      <> " rs: " <> names rs <> "\n"
      <> go cs
    name = T.unpack . htmlElemName
    names = show . map name

-- | Returns an iterator for a zipper.
htmlIter :: HTMLZipper -> HTMLIter
htmlIter = HTMLIter Down

-- | Gets the iterator for a zipper.
htmlIterZipper :: HTMLIter -> HTMLZipper
htmlIterZipper (HTMLIter _ z) = z

-- | Modifies the zipper for an interator.
htmlIterModify :: (HTMLZipper -> HTMLZipper) -> HTMLIter -> HTMLIter
htmlIterModify f (HTMLIter d z) = (HTMLIter d $ f z)

-- | Advances an iterator to the next element.
htmlIterNext :: HTMLIter -> Maybe HTMLIter
htmlIterNext = iterStep htmlZipFirst htmlZipNext

-- | Advances an iterator to the previous element.
htmlIterBack :: HTMLIter -> Maybe HTMLIter
htmlIterBack = iterStep htmlZipLast htmlZipPrev

-- | Steps an iterator.
iterStep
  :: (HTMLZipper -> Maybe HTMLZipper)
  -> (HTMLZipper -> Maybe HTMLZipper)
  -> HTMLIter
  -> Maybe HTMLIter
iterStep first next = go
  where
    go (HTMLIter d z) =
      case d of
        Down ->
          case first z of
            Just x -> Just $ HTMLIter Down x
            Nothing -> go $ HTMLIter Across z
        Across ->
          case next z of
            Just x -> Just $ HTMLIter Down x
            Nothing -> go $ HTMLIter Up z
        Up ->
          case htmlZipParent z of
            Just x -> Just $ HTMLIter Across x
            Nothing -> Nothing

-- | Searches an iterator until a predicate is true.
htmlIterSearch
  :: (HTMLIter -> Maybe HTMLIter)
  -> (HTMLZipper -> Bool)
  -> HTMLIter
  -> Maybe HTMLIter
htmlIterSearch step test x@(HTMLIter _ z)
  | test z = Just x
  | otherwise = maybe Nothing (htmlIterSearch step test) $ step x

-- | Defines an empty path.
htmlZipPathEmpty :: HTMLZipPath
htmlZipPathEmpty = HTMLZipPath []

-- | Gets the path for a node.
htmlZipPath :: HTMLZipper -> HTMLZipPath
htmlZipPath = maybe (HTMLZipPath []) id . go htmlZipPathEmpty
  where
    go :: HTMLZipPath -> HTMLZipper -> Maybe HTMLZipPath
    go (HTMLZipPath p) z =
      case htmlZipIndex z of
        Nothing ->
          Just $ HTMLZipPath p
        Just x ->
          case htmlZipParent z of
            Nothing -> Nothing
            Just y -> go (HTMLZipPath $ x : p) y

-- | Finds the zipper for a path starting from the current node.
htmlZipPathFind :: HTMLZipPath -> HTMLZipper -> Maybe HTMLZipper
htmlZipPathFind (HTMLZipPath p) = f p
  where
    f [] = pure
    f (x:xs) = htmlZipGet x >=> f xs
