{-# OPTIONS -fno-warn-incomplete-patterns #-}

--------------------------------------------------------------------
-- |
-- Module    : Text.Feed.Constructor
-- Copyright : (c) Galois, Inc. 2008,
--             (c) Sigbjorn Finne 2009-
-- License   : BSD3
--
-- Maintainer: Sigbjorn Finne <sof@forkIO.com>
-- Stability : provisional
-- Description: Module for an abstraction layer between different kinds of feeds.
--
--------------------------------------------------------------------
module Text.Feed.Constructor
  ( FeedKind(..)
  , newFeed -- :: FeedKind  -> Feed
  , feedFromRSS -- :: RSS       -> Feed
  , feedFromAtom -- :: Atom.Feed -> Feed
  , feedFromRDF -- :: RSS1.Feed -> Feed
  , feedFromXML -- :: Element   -> Feed
  , getFeedKind -- :: Feed      -> FeedKind
  , FeedSetter -- type _ a = a -> Feed -> Feed
  , addItem -- :: FeedSetter Item
  , withFeedTitle -- :: FeedSetter Text
  , withFeedHome -- :: FeedSetter URLString
  , withFeedHTML -- :: FeedSetter URLString
  , withFeedDescription -- :: FeedSetter Text
  , withFeedPubDate -- :: FeedSetter DateString
  , withFeedLastUpdate -- :: FeedSetter DateString
  , withFeedDate -- :: FeedSetter DateString
  , withFeedLogoLink -- :: FeedSetter URLString
  , withFeedLanguage -- :: FeedSetter Text
  , withFeedCategories -- :: FeedSetter [(Text, Maybe Text)]
  , withFeedGenerator -- :: FeedSetter Text
  , withFeedItems -- :: FeedSetter [Item]
  , newItem -- :: FeedKind   -> Item
  , getItemKind -- :: Item       -> FeedKind
  , atomEntryToItem -- :: Atom.Entry -> Item
  , rssItemToItem -- :: RSS.Item   -> Item
  , rdfItemToItem -- :: RSS1.Item  -> Item
  , ItemSetter -- type _ a = a -> Item -> Item
  , withItemTitle -- :: ItemSetter Text
  , withItemLink -- :: ItemSetter URLString
  , withItemPubDate -- :: ItemSetter DateString
  , withItemDate -- :: ItemSetter DateString
  , withItemAuthor -- :: ItemSetter Text
  , withItemCommentLink -- :: ItemSetter Text
  , withItemEnclosure -- :: Text -> Maybe Text -> ItemSetter Integer
  , withItemFeedLink -- :: Text -> ItemSetter Text
  , withItemId -- :: Bool   -> ItemSetter Text
  , withItemCategories -- :: ItemSetter [(Text, Maybe Text)]
  , withItemDescription -- :: ItemSetter Text
  , withItemRights -- :: ItemSetter Text
  ) where

import Prelude.Compat

import Text.Feed.Types as Feed.Types

import Text.Atom.Feed as Atom
import Text.DublinCore.Types
import Text.RSS.Syntax as RSS
import Text.RSS1.Syntax as RSS1

import Data.XML.Compat
import Data.XML.Types as XML

import Data.Char (toLower)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Text (Text, pack)

-- ToDo:
--
--  - complete set of constructors over feeds
--  - provide a unified treatment of date string reps.
--    (i.e., I know they differ across formats, but ignorant what
--    the constraints are at the moment.)
-- | Construct an empty feed document, intending to output it in
-- the 'fk' feed format.
newFeed :: FeedKind -> Feed.Types.Feed
newFeed fk =
  case fk of
    AtomKind ->
      AtomFeed
        (Atom.nullFeed
           "feed-id-not-filled-in"
           (TextString "dummy-title")
           "dummy-and-bogus-update-date")
    RSSKind mbV ->
      let def = RSS.nullRSS "dummy-title" "default-channel-url"
       in RSSFeed $ maybe def (\v -> def {RSS.rssVersion = v}) mbV
    RDFKind mbV ->
      let def = RSS1.nullFeed "default-channel-url" "dummy-title"
       in RSS1Feed $ maybe def (\v -> def {RSS1.feedVersion = v}) mbV

feedFromRSS :: RSS.RSS -> Feed.Types.Feed
feedFromRSS = RSSFeed

feedFromAtom :: Atom.Feed -> Feed.Types.Feed
feedFromAtom = AtomFeed

feedFromRDF :: RSS1.Feed -> Feed.Types.Feed
feedFromRDF = RSS1Feed

feedFromXML :: XML.Element -> Feed.Types.Feed
feedFromXML = XMLFeed

getFeedKind :: Feed.Types.Feed -> FeedKind
getFeedKind f =
  case f of
    Feed.Types.AtomFeed {} -> AtomKind
    Feed.Types.RSSFeed r ->
      RSSKind
        (case RSS.rssVersion r of
           "2.0" -> Nothing
           v -> Just v)
    Feed.Types.RSS1Feed r ->
      RDFKind
        (case RSS1.feedVersion r of
           "1.0" -> Nothing
           v -> Just v)
    Feed.Types.XMLFeed {} -> RSSKind (Just "2.0") -- for now, just a hunch..

addItem :: Feed.Types.Item -> Feed.Types.Feed -> Feed.Types.Feed
addItem it f =
  case (it, f) of
    (Feed.Types.AtomItem e, Feed.Types.AtomFeed fe) ->
      Feed.Types.AtomFeed fe {Atom.feedEntries = e : Atom.feedEntries fe}
    (Feed.Types.RSSItem e, Feed.Types.RSSFeed r) ->
      Feed.Types.RSSFeed
        r {RSS.rssChannel = (RSS.rssChannel r) {RSS.rssItems = e : RSS.rssItems (RSS.rssChannel r)}}
    (Feed.Types.RSS1Item e, Feed.Types.RSS1Feed r)
         -- note: do not update the channel item URIs at this point;
         -- will delay doing so until serialization.
     -> Feed.Types.RSS1Feed r {RSS1.feedItems = e : RSS1.feedItems r}
    _ ->
      error "addItem: currently unable to automatically convert items from one feed type to another"

withFeedItems :: FeedSetter [Feed.Types.Item]
withFeedItems is fe =
  foldr
    addItem
    (case fe of
       Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {Atom.feedEntries = []}
       Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssItems = []}}
       Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedItems = []})
    is

newItem :: FeedKind -> Feed.Types.Item
newItem fk =
  case fk of
    AtomKind ->
      Feed.Types.AtomItem $
      Atom.nullEntry
        "entry-id-not-filled-in"
        (TextString "dummy-entry-title")
        "dummy-and-bogus-entry-update-date"
    RSSKind {} -> Feed.Types.RSSItem $ RSS.nullItem "dummy-rss-item-title"
    RDFKind {} ->
      Feed.Types.RSS1Item $ RSS1.nullItem "dummy-item-uri" "dummy-item-title" "dummy-item-link"

getItemKind :: Feed.Types.Item -> FeedKind
getItemKind f =
  case f of
    Feed.Types.AtomItem {} -> AtomKind
    Feed.Types.RSSItem {} -> RSSKind (Just "2.0") -- good guess..
    Feed.Types.RSS1Item {} -> RDFKind (Just "1.0")
    Feed.Types.XMLItem {} -> RSSKind (Just "2.0")

type FeedSetter a = a -> Feed.Types.Feed -> Feed.Types.Feed

withFeedTitle :: FeedSetter Text
withFeedTitle tit fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedTitle = TextString tit}
    Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssTitle = tit}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelTitle = tit}}
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "title"
                            then Just (unode "title" tit)
                            else Nothing)
                       e)
             else Nothing)
        f

withFeedHome :: FeedSetter URLString
withFeedHome url fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedLinks = newSelf : Atom.feedLinks f}
      -- ToDo: fix, the <link> element is for the HTML home of the channel, not the
      -- location of the feed itself. Struggling to find if there is a common way
      -- to represent this outside of RSS 2.0 standard elements..
    Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLink = url}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelURI = url}}
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "link"
                            then Just (unode "link" url)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    newSelf = (nullLink url) {linkRel = Just (Left "self"), linkType = Just "application/atom+xml"}

-- | 'withFeedHTML' sets the URL where an HTML version of the
-- feed is published.
withFeedHTML :: FeedSetter URLString
withFeedHTML url fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedLinks = newAlt : Atom.feedLinks f}
    Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLink = url}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelLink = url}}
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "link"
                            then Just (unode "link" url)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    newAlt = (nullLink url) {linkRel = Just (Left "alternate"), linkType = Just "text/html"}

-- | 'withFeedHTML' sets the URL where an HTML version of the
-- feed is published.
withFeedDescription :: FeedSetter Text
withFeedDescription desc fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedSubtitle = Just (TextString desc)}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssDescription = desc}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelDesc = desc}}
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "description"
                            then Just (unode "description" desc)
                            else Nothing)
                       e)
             else Nothing)
        f

withFeedPubDate :: FeedSetter Text
withFeedPubDate dateStr fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedUpdated = dateStr}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssPubDate = Just dateStr}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed $
      case break isDate $ RSS1.channelDC (RSS1.feedChannel f) of
        (as, dci:bs) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = dateStr} : bs}
            }
        (_, []) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f)
                  { RSS1.channelDC =
                      DCItem {dcElt = DC_Date, dcText = dateStr} :
                      RSS1.channelDC (RSS1.feedChannel f)
                  }
            }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "pubDate"
                            then Just (unode "pubDate" dateStr)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    isDate dc = dcElt dc == DC_Date

withFeedLastUpdate :: FeedSetter DateString
withFeedLastUpdate dateStr fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedUpdated = dateStr}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLastUpdate = Just dateStr}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed $
      case break isDate $ RSS1.channelDC (RSS1.feedChannel f) of
        (as, dci:bs) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = dateStr} : bs}
            }
        (_, []) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f)
                  { RSS1.channelDC =
                      DCItem {dcElt = DC_Date, dcText = dateStr} :
                      RSS1.channelDC (RSS1.feedChannel f)
                  }
            }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "lastUpdate"
                            then Just (unode "lastUpdate" dateStr)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    isDate dc = dcElt dc == DC_Date

-- | 'withFeedDate dt' is the composition of 'withFeedPubDate'
-- and 'withFeedLastUpdate', setting both publication date and
-- last update date to 'dt'. Notice that RSS2.0 is the only format
-- supporting both pub and last-update.
withFeedDate :: FeedSetter DateString
withFeedDate dt f = withFeedPubDate dt (withFeedLastUpdate dt f)

withFeedLogoLink :: URLString -> FeedSetter URLString
withFeedLogoLink imgURL lnk fe =
  case fe of
    Feed.Types.AtomFeed f ->
      Feed.Types.AtomFeed f {feedLogo = Just imgURL, feedLinks = newSelf : Atom.feedLinks f}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed
        f
          { rssChannel =
              (rssChannel f) {rssImage = Just $ RSS.nullImage imgURL (rssTitle (rssChannel f)) lnk}
          }
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed $
      f
        { feedImage = Just $ RSS1.nullImage imgURL (RSS1.channelTitle (RSS1.feedChannel f)) lnk
        , feedChannel = (feedChannel f) {channelImageURI = Just imgURL}
        }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "image"
                            then Just
                                   (unode
                                      "image"
                                      [unode "url" imgURL, unode "title" title, unode "link" lnk])
                            else Nothing)
                       e)
             else Nothing)
        f
      where title =
              case fmap (findChild "title") (findChild "channel" f) of
                Just (Just e1) -> strContent e1
                _ -> "feed_title" -- shouldn't happen..
  where
    newSelf = (nullLink lnk) {linkRel = Just (Left "self"), linkType = Just "application/atom+xml"}

withFeedLanguage :: FeedSetter Text
withFeedLanguage lang fe =
  case fe of
    Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {Atom.feedAttrs = langAttr : Atom.feedAttrs f}
      where langAttr = (name, [ContentText lang])
            name = Name {nameLocalName = "lang", nameNamespace = Nothing, namePrefix = Just "xml"}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLanguage = Just lang}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed $
      case break isLang $ RSS1.channelDC (RSS1.feedChannel f) of
        (as, dci:bs) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = lang} : bs}
            }
        (_, []) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f)
                  { RSS1.channelDC =
                      DCItem {dcElt = DC_Language, dcText = lang} :
                      RSS1.channelDC (RSS1.feedChannel f)
                  }
            }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "language"
                            then Just (unode "language" lang)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    isLang dc = dcElt dc == DC_Language

withFeedCategories :: FeedSetter [(Text, Maybe Text)]
withFeedCategories cats fe =
  case fe of
    Feed.Types.AtomFeed f ->
      Feed.Types.AtomFeed
        f
          { Atom.feedCategories =
              map (\(t, mb) -> (Atom.newCategory t) {Atom.catScheme = mb}) cats ++ feedCategories f
          }
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed
        f
          { rssChannel =
              (rssChannel f)
                { RSS.rssCategories =
                    map (\(t, mb) -> (RSS.newCategory t) {RSS.rssCategoryDomain = mb}) cats ++
                    RSS.rssCategories (rssChannel f)
                }
          }
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed
        f
          { feedChannel =
              (feedChannel f)
                { RSS1.channelDC =
                    map (\(t, _) -> DCItem {dcElt = DC_Subject, dcText = t}) cats ++
                    RSS1.channelDC (feedChannel f)
                }
          }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (foldr
                       (\(t, mb) acc ->
                          addChild
                            (unode
                               "category"
                               (maybe (: []) (\v x -> [mkAttr "domain" v, x]) mb (mkAttr "term" t)))
                            acc)
                       e
                       cats)
             else Nothing)
        f

withFeedGenerator :: FeedSetter (Text, Maybe URLString)
withFeedGenerator (gen, mbURI) fe =
  case fe of
    Feed.Types.AtomFeed f ->
      Feed.Types.AtomFeed $
      f {Atom.feedGenerator = Just ((Atom.nullGenerator gen) {Atom.genURI = mbURI})}
    Feed.Types.RSSFeed f ->
      Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssGenerator = Just gen}}
    Feed.Types.RSS1Feed f ->
      Feed.Types.RSS1Feed $
      case break isSource $ RSS1.channelDC (RSS1.feedChannel f) of
        (as, dci:bs) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = gen} : bs}
            }
        (_, []) ->
          f
            { RSS1.feedChannel =
                (RSS1.feedChannel f)
                  { RSS1.channelDC =
                      DCItem {dcElt = DC_Source, dcText = gen} : RSS1.channelDC (RSS1.feedChannel f)
                  }
            }
    Feed.Types.XMLFeed f ->
      Feed.Types.XMLFeed $
      mapMaybeChildren
        (\e ->
           if elementName e == "channel"
             then Just
                    (mapMaybeChildren
                       (\e2 ->
                          if elementName e2 == "generator"
                            then Just (unode "generator" gen)
                            else Nothing)
                       e)
             else Nothing)
        f
  where
    isSource dc = dcElt dc == DC_Source

-- Item constructors (all the way to the end):
atomEntryToItem :: Atom.Entry -> Feed.Types.Item
atomEntryToItem = Feed.Types.AtomItem

rssItemToItem :: RSS.RSSItem -> Feed.Types.Item
rssItemToItem = Feed.Types.RSSItem

rdfItemToItem :: RSS1.Item -> Feed.Types.Item
rdfItemToItem = Feed.Types.RSS1Item

type ItemSetter a = a -> Feed.Types.Item -> Feed.Types.Item

-- | 'withItemPubDate dt' associates the creation\/ publication date 'dt'
-- with a feed item.
withItemPubDate :: ItemSetter DateString
withItemPubDate dt fi =
  case fi of
    Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryUpdated = dt}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemPubDate = Just dt}
    Feed.Types.RSS1Item i ->
      case break isDate $ RSS1.itemDC i of
        (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = dt} : bs}
        (_, []) ->
          Feed.Types.RSS1Item
            i {RSS1.itemDC = DCItem {dcElt = DC_Date, dcText = dt} : RSS1.itemDC i}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "pubDate" dt) $ filterChildren (\e -> elementName e /= "pubDate") i
  where
    isDate dc = dcElt dc == DC_Date

-- | 'withItemDate' is a synonym for 'withItemPubDate'.
withItemDate :: ItemSetter DateString
withItemDate = withItemPubDate

-- | 'withItemTitle myTitle' associates a new title, 'myTitle',
-- with a feed item.
withItemTitle :: ItemSetter Text
withItemTitle tit fi =
  case fi of
    Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryTitle = TextString tit}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemTitle = Just tit}
    Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemTitle = tit}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "title" tit) $ filterChildren (\e -> elementName e /= "title") i

-- | 'withItemAuthor auStr' associates new author info
-- with a feed item.
withItemAuthor :: ItemSetter Text
withItemAuthor au fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem
        e {Atom.entryAuthors = [nullPerson {personName = au, personURI = Just au}]}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemAuthor = Just au}
    Feed.Types.RSS1Item i ->
      case break isAuthor $ RSS1.itemDC i of
        (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = au} : bs}
        (_, []) ->
          Feed.Types.RSS1Item
            i {RSS1.itemDC = DCItem {dcElt = DC_Creator, dcText = au} : RSS1.itemDC i}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "author" au) $ filterChildren (\e -> elementName e /= "author") i
  where
    isAuthor dc = dcElt dc == DC_Creator

-- | 'withItemFeedLink name myFeed' associates the parent feed URL 'myFeed'
-- with a feed item. It is labelled as 'name'.
withItemFeedLink :: Text -> ItemSetter Text
withItemFeedLink tit url fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem
        e
          { Atom.entrySource =
              Just Atom.nullSource {sourceId = Just url, sourceTitle = Just (TextString tit)}
          }
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemSource = Just (RSS.nullSource url tit)}
    Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemTitle = tit}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "source" ([mkAttr "url" url], tit)) $
      filterChildren (\e -> elementName e /= "source") i

-- | 'withItemCommentLink url' sets the URL reference to the comment page to 'url'.
withItemCommentLink :: ItemSetter Text
withItemCommentLink url fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem
        e {Atom.entryLinks = ((nullLink url) {linkRel = Just (Left "replies")}) : Atom.entryLinks e}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemComments = Just url}
    Feed.Types.RSS1Item i ->
      case break isRel $ RSS1.itemDC i of
        (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = url} : bs}
        (_, []) ->
          Feed.Types.RSS1Item
            i {RSS1.itemDC = DCItem {dcElt = DC_Relation, dcText = url} : RSS1.itemDC i}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "comments" url) $ filterChildren (\e -> elementName e /= "comments") i
  where
    isRel dc = dcElt dc == DC_Relation

-- | 'withItemEnclosure url mbTy len' sets the URL reference to the comment page to 'url'.
withItemEnclosure :: Text -> Maybe Text -> ItemSetter (Maybe Integer)
withItemEnclosure url ty mb_len fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem
        e
          { Atom.entryLinks =
              ((nullLink url)
                 { linkRel = Just (Left "enclosure")
                 , linkType = ty
                 , linkLength = fmap (pack . show) mb_len
                 }) :
              Atom.entryLinks e
          }
    Feed.Types.RSSItem i ->
      Feed.Types.RSSItem
        i {RSS.rssItemEnclosure = Just (nullEnclosure url mb_len (fromMaybe "text/html" ty))}
    Feed.Types.RSS1Item i ->
      Feed.Types.RSS1Item
        i
          { RSS1.itemContent =
              nullContentInfo {contentURI = Just url, contentFormat = ty} : RSS1.itemContent i
          }
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild
        ((unode "enclosure" url)
           {elementAttributes = [mkAttr "length" "0", mkAttr "type" (fromMaybe "text/html" ty)]}) $
      filterChildren (\e -> elementName e /= "enclosure") i

-- | 'withItemId isURL id' associates new unique identifier with a feed item.
-- If 'isURL' is 'True', then the id is assumed to point to a valid web resource.
withItemId :: Bool -> ItemSetter Text
withItemId isURL idS fi =
  case fi of
    Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryId = idS}
    Feed.Types.RSSItem i ->
      Feed.Types.RSSItem
        i {RSS.rssItemGuid = Just (nullGuid idS) {rssGuidPermanentURL = Just isURL}}
    Feed.Types.RSS1Item i ->
      case break isId $ RSS1.itemDC i of
        (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = idS} : bs}
        (_, []) ->
          Feed.Types.RSS1Item
            i {RSS1.itemDC = DCItem {dcElt = DC_Identifier, dcText = idS} : RSS1.itemDC i}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "guid" ([mkAttr "isPermaLink" (showBool isURL)], idS)) $
      filterChildren (\e -> elementName e /= "guid") i
  where
    showBool x = pack $ map toLower (show x)
    isId dc = dcElt dc == DC_Identifier

-- | 'withItemDescription desc' associates a new descriptive string (aka summary)
-- with a feed item.
withItemDescription :: ItemSetter Text
withItemDescription desc fi =
  case fi of
    Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entrySummary = Just (TextString desc)}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemDescription = Just desc}
    Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemDesc = Just desc}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "description" desc) $ filterChildren (\e -> elementName e /= "description") i

-- | 'withItemRights rightStr' associates the rights information 'rightStr'
-- with a feed item.
withItemRights :: ItemSetter Text
withItemRights desc fi =
  case fi of
    Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryRights = Just (TextString desc)}
     -- Note: per-item copyright information isn't supported by RSS2.0 (and earlier editions),
     -- you can only attach this at the feed/channel level. So, there's not much we can do
     -- except dropping the information on the floor here. (Rolling our own attribute or
     -- extension element is an option, but would prefer if someone else had started that
     -- effort already.
    Feed.Types.RSSItem {} -> fi
    Feed.Types.RSS1Item i ->
      case break ((== DC_Rights) . dcElt) $ RSS1.itemDC i of
        (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = desc} : bs}
        (_, []) ->
          Feed.Types.RSS1Item
            i {RSS1.itemDC = DCItem {dcElt = DC_Rights, dcText = desc} : RSS1.itemDC i}
     -- Since we're so far assuming that a shallow XML rep. of an item
     -- is of RSS2.0 ilk, pinning on the rights info is hard (see above.)
    Feed.Types.XMLItem {} -> fi

-- | 'withItemTitle myLink' associates a new URL, 'myLink',
-- with a feed item.
withItemLink :: ItemSetter URLString
withItemLink url fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem e {Atom.entryLinks = replaceAlternate url (Atom.entryLinks e)}
    Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemLink = Just url}
    Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemLink = url}
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      addChild (unode "link" url) $ filterChildren (\e -> elementName e /= "link") i
  where
    replaceAlternate _ [] = []
    replaceAlternate x (lr:xs)
      | toStr (Atom.linkRel lr) == "alternate" = lr {Atom.linkHref = x} : xs
      | otherwise = lr : replaceAlternate x xs
    toStr Nothing = ""
    toStr (Just (Left x)) = x
    toStr (Just (Right x)) = x

withItemCategories :: ItemSetter [(Text, Maybe Text)]
withItemCategories cats fi =
  case fi of
    Feed.Types.AtomItem e ->
      Feed.Types.AtomItem
        e
          { Atom.entryCategories =
              map (\(t, mb) -> (Atom.newCategory t) {Atom.catScheme = mb}) cats ++ entryCategories e
          }
    Feed.Types.RSSItem i ->
      Feed.Types.RSSItem
        i
          { RSS.rssItemCategories =
              map (\(t, mb) -> (RSS.newCategory t) {RSS.rssCategoryDomain = mb}) cats ++
              rssItemCategories i
          }
    Feed.Types.RSS1Item i ->
      Feed.Types.RSS1Item
        i
          { RSS1.itemDC =
              map (\(t, _) -> DCItem {dcElt = DC_Subject, dcText = t}) cats ++ RSS1.itemDC i
          }
    Feed.Types.XMLItem i ->
      Feed.Types.XMLItem $
      foldr
        (\(t, mb) acc ->
           addChild
             (unode "category" (maybe (: []) (\v x -> [mkAttr "domain" v, x]) mb (mkAttr "term" t)))
             acc)
        i
        cats

-- helpers..
filterChildren :: (XML.Element -> Bool) -> XML.Element -> XML.Element
filterChildren pre e =
  case elementNodes e of
    [] -> e
    cs -> e {elementNodes = mapMaybe filterElt cs}
  where
    filterElt xe@(XML.NodeElement el)
      | pre el = Just xe
      | otherwise = Nothing
    filterElt xe = Just xe

addChild :: XML.Element -> XML.Element -> XML.Element
addChild a b = b {elementNodes = XML.NodeElement a : elementNodes b}

mapMaybeChildren :: (XML.Element -> Maybe XML.Element) -> XML.Element -> XML.Element
mapMaybeChildren f e =
  case elementNodes e of
    [] -> e
    cs -> e {elementNodes = map procElt cs}
  where
    procElt xe@(XML.NodeElement el) =
      case f el of
        Nothing -> xe
        Just el1 -> XML.NodeElement el1
    procElt xe = xe