/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;

import org.dspace.content.Bitstream;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DCDate;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.handle.HandleManager;

import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndEnclosure;
import com.sun.syndication.feed.synd.SyndEnclosureImpl;
import com.sun.syndication.feed.synd.SyndImage;
import com.sun.syndication.feed.synd.SyndImageImpl;
import com.sun.syndication.feed.synd.SyndPerson;
import com.sun.syndication.feed.synd.SyndPersonImpl;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.module.DCModuleImpl;
import com.sun.syndication.feed.module.DCModule;
import com.sun.syndication.feed.module.Module;
import com.sun.syndication.feed.module.itunes.*;
import com.sun.syndication.feed.module.itunes.types.Duration;
import com.sun.syndication.io.SyndFeedOutput;
import com.sun.syndication.io.FeedException;

import org.apache.log4j.Logger;
import org.dspace.content.Bundle;

/**
 * Invoke ROME library to assemble a generic model of a syndication
 * for the given list of Items and scope.  Consults configuration for the
 * metadata bindings to feed elements.  Uses ROME's output drivers to
 * return any of the implemented formats, e.g. RSS 1.0, RSS 2.0, ATOM 1.0.
 *
 * The feed generator and OpenSearch call on this class so feed contents are
 * uniform for both.
 *
 * @author Larry Stone
 */
public class SyndicationFeed
{
    private static final Logger log = Logger.getLogger(SyndicationFeed.class);


    /** i18n key values */
    public static final String MSG_UNTITLED = "notitle";
    public static final String MSG_LOGO_TITLE = "logo.title";
    public static final String MSG_FEED_TITLE = "feed.title";
    public static final String MSG_FEED_DESCRIPTION = "general-feed.description";
    public static final String MSG_METADATA = "metadata.";
    public static final String MSG_UITYPE = "ui.type";

    // UI keywords
    public static final String UITYPE_XMLUI = "xmlui";
    public static final String UITYPE_JSPUI = "jspui";

    // default DC fields for entry
    private static String defaultTitleField = "dc.title";
    private static String defaultAuthorField = "dc.contributor.author";
    private static String defaultDateField = "dc.date.issued";
    private static String defaultDescriptionFields = "dc.description.abstract, dc.description, dc.title.alternative, dc.title";
    private static String defaultExternalMedia = "dc.source.uri";

    // metadata field for Item title in entry:
    private static String titleField =
        getDefaultedConfiguration("webui.feed.item.title", defaultTitleField);

    // metadata field for Item publication date in entry:
    private static String dateField =
        getDefaultedConfiguration("webui.feed.item.date", defaultDateField);

    // metadata field for Item description in entry:
    private static String descriptionFields[] =
        getDefaultedConfiguration("webui.feed.item.description", defaultDescriptionFields).split("\\s*,\\s*");

    private static String authorField =
        getDefaultedConfiguration("webui.feed.item.author", defaultAuthorField);

    // metadata field for Podcast external media source url
    private static String externalSourceField = getDefaultedConfiguration("webui.feed.podcast.sourceuri", defaultExternalMedia);

    // metadata field for Item dc:creator field in entry's DCModule (no default)
    private static String dcCreatorField = ConfigurationManager.getProperty("webui.feed.item.dc.creator");

    // metadata field for Item dc:date field in entry's DCModule (no default)
    private static String dcDateField = ConfigurationManager.getProperty("webui.feed.item.dc.date");

    // metadata field for Item dc:author field in entry's DCModule (no default)
    private static String dcDescriptionField = ConfigurationManager.getProperty("webui.feed.item.dc.description");

    // List of available mimetypes that we'll add to podcast feed. Multiple values separated by commas
    private static String podcastableMIMETypes = getDefaultedConfiguration("webui.feed.podcast.mimetypes", "audio/x-mpeg");

    // -------- Instance variables:

    // the feed object we are building
    private SyndFeed feed = null;

    // memory of UI that called us, "xmlui" or "jspui"
    // affects Bitstream retrieval URL and I18N keys
    private String uiType = null;

    private HttpServletRequest request = null;

    /**
     * Constructor.
     * @param ui either "xmlui" or "jspui"
     */
    public SyndicationFeed(String ui)
    {
        feed = new SyndFeedImpl();
        uiType = ui;
    }

    /**
     * Returns list of metadata selectors used to compose the description element
     *
     * @return selector list - format 'schema.element[.qualifier]'
     */
    public static String[] getDescriptionSelectors()
    {
        return (String[]) ArrayUtils.clone(descriptionFields);
    }


    /**
     * Fills in the feed and entry-level metadata from DSpace objects.
     */
    public void populate(HttpServletRequest request, DSpaceObject dso,
                         DSpaceObject items[], Map<String, String> labels)
    {
        String logoURL = null;
        String objectURL = null;
        String defaultTitle = null;
        boolean podcastFeed = false;
        this.request = request;

        // dso is null for the whole site, or a search without scope
        if (dso == null)
        {
            defaultTitle = ConfigurationManager.getProperty("dspace.name");
            feed.setDescription(localize(labels, MSG_FEED_DESCRIPTION));
            objectURL = resolveURL(request, null);
            logoURL = ConfigurationManager.getProperty("webui.feed.logo.url");
        }
        else
        {
            Bitstream logo = null;
            if (dso.getType() == Constants.COLLECTION)
            {
                Collection col = (Collection)dso;
                defaultTitle = col.getMetadata("name");
                feed.setDescription(col.getMetadata("short_description"));
                logo = col.getLogo();
                String cols = ConfigurationManager.getProperty("webui.feed.podcast.collections");
                if(cols != null && cols.length() > 1 && cols.contains(col.getHandle()) ) {
                    podcastFeed = true;
                }
            }
            else if (dso.getType() == Constants.COMMUNITY)
            {
                Community comm = (Community)dso;
                defaultTitle = comm.getMetadata("name");
                feed.setDescription(comm.getMetadata("short_description"));
                logo = comm.getLogo();
                String comms = ConfigurationManager.getProperty("webui.feed.podcast.communities");
                if(comms != null && comms.length() > 1 && comms.contains(comm.getHandle()) ){
                    podcastFeed = true;
                }
            }
            objectURL = resolveURL(request, dso);
            if (logo != null)
            {
                logoURL = urlOfBitstream(request, logo);
            }
        }
        feed.setTitle(labels.containsKey(MSG_FEED_TITLE) ?
                            localize(labels, MSG_FEED_TITLE) : defaultTitle);
        feed.setLink(objectURL);
        feed.setPublishedDate(new Date());
        feed.setUri(objectURL);

        // add logo if we found one:
        if (logoURL != null)
        {
            // we use the path to the logo for this, the logo itself cannot
            // be contained in the rdf. Not all RSS-viewers show this logo.
            SyndImage image = new SyndImageImpl();
            image.setLink(objectURL);
            if (StringUtils.isNotBlank(feed.getTitle())) {
                image.setTitle(feed.getTitle());
            } else {
                image.setTitle(localize(labels, MSG_LOGO_TITLE));
            }
            image.setUrl(logoURL);
            feed.setImage(image);
        }

        // add entries for items
        if (items != null)
        {
            List<SyndEntry> entries = new ArrayList<SyndEntry>();
            for (DSpaceObject itemDSO : items)
            {
                if (itemDSO.getType() != Constants.ITEM)
                {
                    continue;
                }
                Item item = (Item)itemDSO;
                boolean hasDate = false;
                SyndEntry entry = new SyndEntryImpl();
                entries.add(entry);
             
                String entryURL = resolveURL(request, item);
                entry.setLink(entryURL);
                entry.setUri(entryURL);
             
                String title = getOneDC(item, titleField);
                entry.setTitle(title == null ? localize(labels, MSG_UNTITLED) : title);
             
                // "published" date -- should be dc.date.issued
                String pubDate = getOneDC(item, dateField);
                if (pubDate != null)
                {
                    entry.setPublishedDate((new DCDate(pubDate)).toDate());
                    hasDate = true;
                }
                // date of last change to Item
                entry.setUpdatedDate(item.getLastModified());
             
                StringBuffer db = new StringBuffer();
                for (String df : descriptionFields)
                {
                    // Special Case: "(date)" in field name means render as date
                    boolean isDate = df.indexOf("(date)") > 0;
                    if (isDate)
                    {
                        df = df.replaceAll("\\(date\\)", "");
                    }
             
                    DCValue dcv[] = item.getMetadata(df);
                    if (dcv.length > 0)
                    {
                        String fieldLabel = labels.get(MSG_METADATA + df);
                        if (fieldLabel != null && fieldLabel.length()>0)
                        {
                            db.append(fieldLabel).append(": ");
                        }
                        boolean first = true;
                        for (DCValue v : dcv)
                        {
                            if (first)
                            {
                                first = false;
                            }
                            else
                            {
                                db.append("; ");
                            }
                            db.append(isDate ? new DCDate(v.value).toString() : v.value);
                        }
                        db.append("\n");
                    }
                }
                if (db.length() > 0)
                {
                    SyndContent desc = new SyndContentImpl();
                    desc.setType("text/plain");
                    desc.setValue(db.toString());
                    entry.setDescription(desc);
                }

                // This gets the authors into an ATOM feed
                DCValue authors[] = item.getMetadata(authorField);
                if (authors.length > 0)
                {
                    List<SyndPerson> creators = new ArrayList<SyndPerson>();
                    for (DCValue author : authors)
                    {
                        SyndPerson sp = new SyndPersonImpl();
                        sp.setName(author.value);
                        creators.add(sp);
                    }
                    entry.setAuthors(creators);
                }

                // only add DC module if any DC fields are configured
                if (dcCreatorField != null || dcDateField != null ||
                    dcDescriptionField != null)
                {
                    DCModule dc = new DCModuleImpl();
                    if (dcCreatorField != null)
                    {
                        DCValue dcAuthors[] = item.getMetadata(dcCreatorField);
                        if (dcAuthors.length > 0)
                        {
                            List<String> creators = new ArrayList<String>();
                            for (DCValue author : dcAuthors)
                            {
                                creators.add(author.value);
                            }
                            dc.setCreators(creators);
                        }
                    }
                    if (dcDateField != null && !hasDate)
                    {
                        DCValue v[] = item.getMetadata(dcDateField);
                        if (v.length > 0)
                        {
                            dc.setDate((new DCDate(v[0].value)).toDate());
                        }
                    }
                    if (dcDescriptionField != null)
                    {
                        DCValue v[] = item.getMetadata(dcDescriptionField);
                        if (v.length > 0)
                        {
                            StringBuffer descs = new StringBuffer();
                            for (DCValue d : v)
                            {
                                if (descs.length() > 0)
                                {
                                    descs.append("\n\n");
                                }
                                descs.append(d.value);
                            }
                            dc.setDescription(descs.toString());
                        }
                    }
                    entry.getModules().add(dc);
                }

                //iTunes Podcast Support - START
                if (podcastFeed)
                {
                    // Add enclosure(s)
                    List<SyndEnclosure> enclosures = new ArrayList();
                    try {
                        Bundle[] bunds = item.getBundles("ORIGINAL");
                        if (bunds[0] != null) {
                            Bitstream[] bits = bunds[0].getBitstreams();
                            for (int i = 0; (i < bits.length); i++) {
                                String mime = bits[i].getFormat().getMIMEType();
                                if(podcastableMIMETypes.contains(mime)) {
                                    SyndEnclosure enc = new SyndEnclosureImpl();
                                    enc.setType(bits[i].getFormat().getMIMEType());
                                    enc.setLength(bits[i].getSize());
                                    enc.setUrl(urlOfBitstream(request, bits[i]));
                                    enclosures.add(enc);
                                } else {
                                    continue;
                                }
                            }
                        }
                        //Also try to add an external value from dc.identifier.other
                        // We are assuming that if this is set, then it is a media file
                        DCValue[] externalMedia = item.getMetadata(externalSourceField);
                        if(externalMedia.length > 0)
                        {
                            for(int i = 0; i< externalMedia.length; i++)
                            {
                                SyndEnclosure enc = new SyndEnclosureImpl();
                                enc.setType("audio/x-mpeg");        //We can't determine MIME of external file, so just picking one.
                                enc.setLength(1);
                                enc.setUrl(externalMedia[i].value);
                                enclosures.add(enc);
                            }
                        }

                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    entry.setEnclosures(enclosures);

                    // Get iTunes specific fields: author, subtitle, summary, duration, keywords
                    EntryInformation itunes = new EntryInformationImpl();

                    String author = getOneDC(item, authorField);
                    if (author != null && author.length() > 0) {
                        itunes.setAuthor(author);                               // <itunes:author>
                    }

                    itunes.setSubtitle(title == null ? localize(labels, MSG_UNTITLED) : title); // <itunes:subtitle>

                    if (db.length() > 0) {
                        itunes.setSummary(db.toString());                       // <itunes:summary>
                    }

                    String extent = getOneDC(item, "dc.format.extent");         // assumed that user will enter this field with length of song in seconds
                    if (extent != null && extent.length() > 0) {
                        extent = extent.split(" ")[0];
                        Integer duration = Integer.parseInt(extent);
                        itunes.setDuration(new Duration(duration));             // <itunes:duration>
                    }

                    String subject = getOneDC(item, "dc.subject");
                    if (subject != null && subject.length() > 0) {
                        String[] subjects = new String[1];
                        subjects[0] = subject;
                        itunes.setKeywords(subjects);                           // <itunes:keywords>
                    }

                    entry.getModules().add(itunes);
                }
            }
            feed.setEntries(entries);
        }
    }

    /**
     * Sets the feed type for XML delivery, e.g. "rss_1.0", "atom_1.0"
     * Must match one of ROME's configured generators, see rome.properties
     * (currently rss_1.0, rss_2.0, atom_1.0, atom_0.3)
     */
    public void setType(String feedType)
    {
        feed.setFeedType(feedType);
        // XXX FIXME: workaround ROME 1.0 bug, it puts invalid image element in rss1.0
        if ("rss_1.0".equals(feedType))
        {
            feed.setImage(null);
        }
    }

    /**
     * @return the feed we built as DOM Document
     */
    public Document outputW3CDom()
        throws FeedException
    {
        try
        {
            SyndFeedOutput feedWriter = new SyndFeedOutput();
            return feedWriter.outputW3CDom(feed);
        }
        catch (FeedException e)
        {
            log.error(e);
            throw e;
        }
    }

    /**
     * @return the feed we built as serialized XML string
     */
    public String outputString()
        throws FeedException
    {
        SyndFeedOutput feedWriter = new SyndFeedOutput();
        return feedWriter.outputString(feed);
    }

    /**
     * send the output to designated Writer
     */
    public void output(java.io.Writer writer)
        throws FeedException, IOException
    {
        SyndFeedOutput feedWriter = new SyndFeedOutput();
        feedWriter.output(feed, writer);
    }

    /**
     * Add a ROME plugin module (e.g. for OpenSearch) at the feed level.
     */
    public void addModule(Module m)
    {
        feed.getModules().add(m);
    }

    // utility to get config property with default value when not set.
    private static String getDefaultedConfiguration(String key, String dfl)
    {
        String result = ConfigurationManager.getProperty(key);
        return (result == null) ? dfl : result;
    }

    // returns absolute URL to download content of bitstream (which might not belong to any Item)
    private String urlOfBitstream(HttpServletRequest request, Bitstream logo)
    {
        String name = logo.getName();
        return resolveURL(request,null) +
                 (uiType.equalsIgnoreCase(UITYPE_XMLUI) ?"/bitstream/id/":"/retrieve/") +
                 logo.getID()+"/"+(name == null?"":name);
    }

    /**
     * Return a url to the DSpace object, either use the official
     * handle for the item or build a url based upon the current server.
     *
     * If the dspaceobject is null then a local url to the repository is generated.
     *
     * @param dso The object to reference, null if to the repository.
     * @return
     */
    private String baseURL = null;  // cache the result for null

    private String resolveURL(HttpServletRequest request, DSpaceObject dso)
    {
        // If no object given then just link to the whole repository,
        // since no offical handle exists so we have to use local resolution.
        if (dso == null)
        {
            if (baseURL == null)
            {
                if (request == null)
                {
                    baseURL = ConfigurationManager.getProperty("dspace.url");
                }
                else
                {
                    baseURL = (request.isSecure()) ? "https://" : "http://";
                    baseURL += ConfigurationManager.getProperty("dspace.hostname");
                    baseURL += ":" + request.getServerPort();
                    baseURL += request.getContextPath();
                }
            }
            return baseURL;
        }

        // return a link to handle in repository
        else if (ConfigurationManager.getBooleanProperty("webui.feed.localresolve"))
        {
            return resolveURL(request, null) + "/handle/" + dso.getHandle();
        }

        // link to the Handle server or other persistent URL source
        else
        {
            return HandleManager.getCanonicalForm(dso.getHandle());
        }
    }

    // retrieve text for localization key, or mark untranslated
    private String localize(Map<String, String> labels, String s)
    {
        return labels.containsKey(s) ? labels.get(s) : ("Untranslated:"+s);
    }

    // spoonful of syntactic sugar when we only need first value
    private String getOneDC(Item item, String field)
    {
        DCValue dcv[] = item.getMetadata(field);
        return (dcv.length > 0) ? dcv[0].value : null;
    }
}

