{-# LANGUAGE DefaultSignatures   #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns        #-}

-- |
-- Module      : Network.AWS.Data.XML
-- Copyright   : (c) 2013-2017 Brendan Hay
-- License     : Mozilla Public License, v. 2.0.
-- Maintainer  : Brendan Hay <brendan.g.hay+amazonka@gmail.com>
-- Stability   : provisional
-- Portability : non-portable (GHC extensions)
--
module Network.AWS.Data.XML where

import           Control.Applicative
import           Control.Monad

import           Data.Bifunctor
import           Data.Conduit
import           Data.Conduit.Lazy           (lazyConsume)
import           Data.Maybe
import           Data.Monoid
import           Data.Traversable            (traverse)
import           Data.XML.Types              (Event (..))

import           GHC.Exts

import           Network.AWS.Data.ByteString
import           Network.AWS.Data.Text

import           Numeric.Natural

import           System.IO.Unsafe            (unsafePerformIO)

import           Text.XML
import           Text.XML.Unresolved         (toEvents)

import qualified Data.ByteString.Lazy        as LBS
import qualified Data.Conduit.List           as Conduit
import qualified Text.XML.Stream.Render      as Stream

infixl 7 .@, .@?

(.@) :: FromXML a => [Node] -> Text -> Either String a
ns .@ n = findElement n ns >>= parseXML

(.@?) :: FromXML a => [Node] -> Text -> Either String (Maybe a)
ns .@? n =
    case findElement n ns of
        Left _   -> Right Nothing
        Right xs -> parseXML xs

infixr 7 @=, @@=

(@=) :: ToXML a => Name -> a -> XML
n @= x =
    case toXML x of
        XNull -> XNull
        xs    -> XOne . NodeElement $ mkElement n xs

(@@=) :: ToText a => Name -> a -> XML
n @@= x = XAttr n (toText x)

decodeXML :: FromXML a => LazyByteString -> Either String a
decodeXML lbs =
    bimap show documentRoot (parseLBS def lbs)
        >>= parseXML . childrenOf

-- The following is taken from xml-conduit.Text.XML which uses
-- unsafePerformIO anyway, with the following caveat:
--   'not generally safe, but we know that runResourceT
--    will not deallocate any of the resources being used
--    by the process.'
encodeXML :: ToElement a => a -> LazyByteString
encodeXML x = LBS.fromChunks . unsafePerformIO . lazyConsume
     $  Conduit.sourceList (toEvents doc)
    =$= Conduit.map rename
    =$= Stream.renderBytes def
  where
    doc = toXMLDocument $ Document
        { documentRoot     = root
        , documentEpilogue = []
        , documentPrologue =
            Prologue
                { prologueBefore  = []
                , prologueDoctype = Nothing
                , prologueAfter   = []
                }
        }

    rename = \case
        EventBeginElement n xs -> EventBeginElement (f n) (map (first f) xs)
        EventEndElement   n    -> EventEndElement   (f n)
        evt                    -> evt
      where
        f n | isNothing (nameNamespace n) = n { nameNamespace = ns }
            | otherwise                   = n

    ns   = nameNamespace (elementName root)
    root = toElement x

class FromXML a where
    parseXML :: [Node] -> Either String a

instance FromXML [Node] where
    parseXML = pure

instance FromXML a => FromXML (Maybe a) where
    parseXML [] = pure Nothing
    parseXML ns = Just <$> parseXML ns

instance FromXML Text where
    parseXML = fmap (fromMaybe mempty) . withContent "Text"

instance FromXML Char       where parseXML = parseXMLText "Char"
instance FromXML ByteString where parseXML = parseXMLText "ByteString"
instance FromXML Int        where parseXML = parseXMLText "Int"
instance FromXML Integer    where parseXML = parseXMLText "Integer"
instance FromXML Natural    where parseXML = parseXMLText "Natural"
instance FromXML Double     where parseXML = parseXMLText "Double"
instance FromXML Bool       where parseXML = parseXMLText "Bool"

class ToElement a where
    toElement :: a -> Element

instance ToElement Element where
    toElement = id

-- | Convert to an 'Element', only if the resulting element contains @> 0@ nodes.
maybeElement :: ToElement a => a -> Maybe Element
maybeElement x =
    case toElement x of
        e@(Element _ _ ns)
            | null ns   -> Nothing
            | otherwise -> Just e

-- | Provides a way to make the operators for ToXML instance
-- declaration be consistent WRT to single nodes or lists of nodes.
data XML
    = XNull
    | XAttr Name Text
    | XOne  Node
    | XMany [(Name, Text)] [Node]
      deriving (Show)

instance Monoid XML where
    mempty              = XNull
    mappend XNull XNull = XNull
    mappend a     XNull = a
    mappend XNull b     = b
    mappend a     b     =
        XMany (listXMLAttributes a <> listXMLAttributes b)
              (listXMLNodes      a <> listXMLNodes      b)

listXMLNodes :: XML -> [Node]
listXMLNodes = \case
    XNull      -> []
    XAttr {}   -> []
    XOne  n    -> [n]
    XMany _ ns -> ns

listXMLAttributes :: XML -> [(Name, Text)]
listXMLAttributes = \case
    XNull      -> []
    XAttr n t  -> [(n, t)]
    XOne  {}   -> []
    XMany as _ -> as

class ToXML a where
    toXML :: a -> XML

instance ToXML XML where
    toXML = id

instance ToXML a => ToXML (Maybe a) where
    toXML (Just x) = toXML x
    toXML Nothing  = XNull

instance ToXML Text       where toXML = toXMLText
instance ToXML ByteString where toXML = toXMLText
instance ToXML Int        where toXML = toXMLText
instance ToXML Integer    where toXML = toXMLText
instance ToXML Natural    where toXML = toXMLText
instance ToXML Double     where toXML = toXMLText
instance ToXML Bool       where toXML = toXMLText

parseXMLList :: FromXML a
             => Text
             -> [Node]
             -> Either String [a]
parseXMLList n = traverse parseXML . mapMaybe (childNodesOf n)

parseXMLText :: FromText a => String -> [Node] -> Either String a
parseXMLText n = withContent n >=>
    maybe (Left $ "empty node list, when expecting single node " ++ n)
        fromText

toXMLList :: (IsList a, ToXML (Item a)) => Name -> a -> XML
toXMLList n = XMany [] . map (NodeElement . mkElement n) . toList

toXMLText :: ToText a => a -> XML
toXMLText = XOne . NodeContent . toText

mkElement :: ToXML a => Name -> a -> Element
mkElement n (toXML -> x) =
    Element n (fromList (listXMLAttributes x)) (listXMLNodes x)

withContent :: String -> [Node] -> Either String (Maybe Text)
withContent k = \case
    []              -> Right Nothing
    [NodeContent x] -> Right (Just x)
    _               -> Left $ "encountered many nodes, when expecting text: " ++ k

-- | Find a specific named NodeElement, at the current depth in the node tree.
--
-- Fails if absent.
findElement :: Text -> [Node] -> Either String [Node]
findElement n ns =
     missingElement n ns
   . listToMaybe
   $ mapMaybe (childNodesOf n) ns

-- | Find the first specific named NodeElement, at any depth in the node tree.
--
-- Fails if absent.
firstElement :: Text -> [Node] -> Either String [Node]
firstElement n ns =
      missingElement n ns
    . listToMaybe
    $ mapMaybe go ns
  where
    go x = case x of
        NodeElement e
            | Just n == localName x -> Just (childrenOf e)
            | otherwise             -> listToMaybe (mapMaybe go (elementNodes e))
        _                           -> Nothing

childNodesOf :: Text -> Node -> Maybe [Node]
childNodesOf n x = case x of
    NodeElement e
        | Just n == localName x
              -> Just (childrenOf e)
    _         -> Nothing

childrenOf :: Element -> [Node]
childrenOf e = elementNodes e <> map node (toList (elementAttributes e))
  where
    node (k, v) = NodeElement (Element (name k) mempty [NodeContent v])

    name k = Name
        { nameLocalName = fromMaybe "" (namePrefix k) <> ":" <> nameLocalName k
        , nameNamespace = mempty
        , namePrefix    = mempty
        }

localName :: Node -> Maybe Text
localName = \case
    NodeElement e -> Just (nameLocalName (elementName e))
    _             -> Nothing

-- | An inefficient mechanism for retreiving the root
-- element name of an XML document.
rootElementName :: LazyByteString -> Maybe Text
rootElementName bs =
    either (const Nothing)
           (Just . nameLocalName . elementName . documentRoot)
           (parseLBS def bs)

missingElement :: Text -> [Node] -> Maybe a -> Either String a
missingElement n ns = maybe (Left err) Right
  where
    err = "unable to find element "
        ++ show n
        ++ " in nodes "
        ++ show (mapMaybe localName ns)