// This file is part of BOINC.
// http://boinc.berkeley.edu
// Copyright (C) 2014 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

#include "cpp.h"

#ifdef _WIN32
#include "boinc_win.h"
#else
#include "config.h"
#include <string>
#endif

#include "parse.h"
#include "url.h"
#include "filesys.h"
#include "str_replace.h"

#include "client_state.h"
#include "client_msgs.h"
#include "file_names.h"
#include "project.h"

#include "cs_notice.h"

using std::vector;
using std::string;
using std::deque;

NOTICES notices;
RSS_FEEDS rss_feeds;
RSS_FEED_OP rss_feed_op;

////////////// UTILITY FUNCTIONS ///////////////

static bool cmp(NOTICE n1, NOTICE n2) {
    if (n1.arrival_time > n2.arrival_time) return true;
    if (n1.arrival_time < n2.arrival_time) return false;
    return (strcmp(n1.guid, n2.guid) > 0);
}

static void project_feed_list_file_name(PROJ_AM* p, char* buf) {
    char url[256];
    escape_project_url(p->master_url, url);
    sprintf(buf, "notices/feeds_%s.xml", url);
}

// parse feed descs from scheduler reply or feed list file
//
int parse_rss_feed_descs(XML_PARSER& xp, vector<RSS_FEED>& feeds) {
    int retval;
    while (!xp.get_tag()) {
        if (!xp.is_tag) continue;
        if (xp.match_tag("/rss_feeds")) return 0;
        if (xp.match_tag("rss_feed")) {
            RSS_FEED rf;
            retval = rf.parse_desc(xp);
            if (retval) {
                if (log_flags.sched_op_debug) {
                    msg_printf(0, MSG_INFO,
                        "[sched_op] error in <rss_feed> element"
                    );
                }
            } else {
                feeds.push_back(rf);
            }
        }
    }
    return ERR_XML_PARSE;
}

// write a list of feeds to a file
//
static void write_rss_feed_descs(MIOFILE& fout, vector<RSS_FEED>& feeds) {
    if (!feeds.size()) return;
    fout.printf("<rss_feeds>\n");
    for (unsigned int i=0; i<feeds.size(); i++) {
        feeds[i].write(fout);
    }
    fout.printf("</rss_feeds>\n");
}

static void write_project_feed_list(PROJ_AM* p) {
    char buf[256];
    project_feed_list_file_name(p, buf);
    FILE* f = fopen(buf, "w");
    if (!f) return;
    MIOFILE fout;
    fout.init_file(f);
    write_rss_feed_descs(fout, p->proj_feeds);
    fclose(f);
}

// A scheduler RPC returned a list (possibly empty) of feeds.
// Add new ones to the project's set,
// and remove ones from the project's set that aren't in the list.
//
void handle_sr_feeds(vector<RSS_FEED>& feeds, PROJ_AM* p) {
    unsigned int i, j;
    bool feed_set_changed = false;

    // mark current feeds as not found
    //
    for (i=0; i<p->proj_feeds.size(); i++) {
        p->proj_feeds[i].found = false;
    }

    for (i=0; i<feeds.size(); i++) {
        RSS_FEED& rf = feeds[i];
        bool present = false;
        for (j=0; j<p->proj_feeds.size(); j++) {
            RSS_FEED& rf2 = p->proj_feeds[j];
            if (!strcmp(rf.url, rf2.url)) {
                rf2 = rf;
                rf2.found = true;
                present = true;
                break;
            }
        }
        if (!present) {
            rf.found = true;
            p->proj_feeds.push_back(rf);
            feed_set_changed = true;
        }
    }

    // remove ones no longer present
    //
    vector<RSS_FEED>::iterator iter = p->proj_feeds.begin();
    while (iter != p->proj_feeds.end()) {
        RSS_FEED& rf = *iter;
        if (rf.found) {
            iter++;
        } else {
            iter = p->proj_feeds.erase(iter);
            feed_set_changed = true;
        }
    }

    // if anything was added or removed, update master set
    //
    if (feed_set_changed) {
        write_project_feed_list(p);
        rss_feeds.update_feed_list();
    }
}

#ifdef _WIN32
// compensate for lameness
static int month_index(char* x) {
    if (strstr(x, "Jan")) return 0;
    if (strstr(x, "Feb")) return 1;
    if (strstr(x, "Mar")) return 2;
    if (strstr(x, "Apr")) return 3;
    if (strstr(x, "May")) return 4;
    if (strstr(x, "Jun")) return 5;
    if (strstr(x, "Jul")) return 6;
    if (strstr(x, "Aug")) return 7;
    if (strstr(x, "Sep")) return 8;
    if (strstr(x, "Oct")) return 9;
    if (strstr(x, "Nov")) return 10;
    if (strstr(x, "Dec")) return 11;
    return 0;
}
#endif

// convert a date-time string (assumed GMT) to Unix time

static int parse_rss_time(char* buf) {
#ifdef _WIN32
    char day_name[64], month_name[64];
    int day_num, year, h, m, s;
    sscanf(buf, "%s %d %s %d %d:%d:%d",
        day_name, &day_num, month_name, &year, &h, &m, &s
    );

    struct tm tm;
    tm.tm_sec = s;
    tm.tm_min = m;
    tm.tm_hour = h;
    tm.tm_mday = day_num;
    tm.tm_mon = month_index(month_name);
    tm.tm_year = year-1900;
    tm.tm_wday = 0;
    tm.tm_yday = 0;
    tm.tm_isdst = 0;

    return (int)mktime(&tm);
#else
    struct tm tm;
    memset(&tm, 0, sizeof(tm));
    strptime(buf, "%a, %d %b %Y %H:%M:%S", &tm);
    return mktime(&tm);
#endif
}

///////////// NOTICE ////////////////

int NOTICE::parse_rss(XML_PARSER& xp) {
    char buf[256];

    clear();
    while (!xp.get_tag()) {
        if (!xp.is_tag) continue;
        if (xp.match_tag("/item")) return 0;
        if (xp.parse_str("title", title, sizeof(title))) continue;
        if (xp.parse_str("link", link, sizeof(link))) continue;
        if (xp.parse_str("guid", guid, sizeof(guid))) continue;
        if (xp.parse_string("description", description)) continue;
        if (xp.parse_str("pubDate", buf, sizeof(buf))) {
            create_time = parse_rss_time(buf);
            continue;
        }
    }
    return ERR_XML_PARSE;
}

///////////// NOTICES ////////////////

// called at the start of client initialization
//
void NOTICES::init() {
#if 0
    read_archive_file(NOTICES_DIR"/archive.xml", NULL);
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO, "read %d BOINC notices", (int)notices.size());
    }
    write_archive(NULL);
#endif
}

// called at the end of client initialization
//
void NOTICES::init_rss() {
    rss_feeds.init();
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO, "read %d total notices", (int)notices.size());
    }

    // sort by decreasing arrival time, then assign seqnos
    //
    sort(notices.begin(), notices.end(), cmp);
    size_t n = notices.size();
    for (unsigned int i=0; i<n; i++) {
        notices[i].seqno = (int)(n - i);
    }
}

// return true if strings are the same after discarding digits.
// This eliminates showing
// "you need 25 GB more disk space" and
// "you need 24 GB more disk space" as separate notices.
//
static inline bool string_equal_nodigits(string& s1, string& s2) {
    const char *p = s1.c_str();
    const char *q = s2.c_str();
    while (1) {
        if (isascii(*p) && isdigit(*p)) {
            p++;
            continue;
        }
        if (isascii(*q) && isdigit(*q)) {
            q++;
            continue;
        }
        if (!*p || !*q) break;
        if (*p != *q) return false;
        p++;
        q++;
    }
    if (*p || *q) return false;
    return true;
}

static inline bool same_text(NOTICE& n1, NOTICE& n2) {
    if (strcmp(n1.title, n2.title)) {
        return false;
    }
    if (!string_equal_nodigits(n1.description, n2.description)) {
        return false;
    }
    return true;
}

void NOTICES::clear_keep() {
    deque<NOTICE>::iterator i = notices.begin();
    while (i != notices.end()) {
        NOTICE& n = *i;
        n.keep = false;
        i++;
    }
}

void NOTICES::unkeep(const char* url) {
    deque<NOTICE>::iterator i = notices.begin();
    bool removed_something = false;
    while (i != notices.end()) {
        NOTICE& n = *i;
        if (!strcmp(url, n.feed_url) && !n.keep) {
            i = notices.erase(i);
            removed_something = true;
        } else {
            i++;
        }
    }
#ifndef SIM
    if (removed_something) {
        gstate.gui_rpcs.set_notice_refresh();
    }
#endif
}

static inline bool same_guid(NOTICE& n1, NOTICE& n2) {
    if (!strlen(n1.guid)) return false;
    return !strcmp(n1.guid, n2.guid);
}

// we're considering adding a notice n.
// If there's already an identical message n2
//     return false (don't add n)
// If there's a message n2 with same title and text,
//      and n is significantly newer than n2,
//      delete n2
//
// Also remove notices older than 30 days
//
bool NOTICES::remove_dups(NOTICE& n) {
    deque<NOTICE>::iterator i = notices.begin();
    bool removed_something = false;
    bool retval = true;
    double min_time = gstate.now - 30*86400;
    while (i != notices.end()) {
        NOTICE& n2 = *i;

        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO,
                "[notice] scanning old notice %d: %s",
                n2.seqno, strlen(n2.title)?n2.title:n2.description.c_str()
            );
        }
        if (n2.arrival_time < min_time
            || (n2.create_time && n2.create_time < min_time)
        ) {
            i = notices.erase(i);
            removed_something = true;
            if (log_flags.notice_debug) {
                msg_printf(0, MSG_INFO,
                    "[notice] removing old notice %d: %s",
                    n2.seqno, strlen(n2.title)?n2.title:n2.description.c_str()
                );
            }
#if 0
        // this check prevents news item edits from showing; skip it
        } else if (same_guid(n, n2)) {
            n2.keep = true;
            return false;
#endif
        } else if (same_text(n, n2)) {
            int min_diff = 0;

            // show a given scheduler notice at most once a week
            //
            if (!strcmp(n.category, "scheduler")) {
                min_diff = 7*86400;
            }

            if (n.create_time > n2.create_time + min_diff) {
                i = notices.erase(i);
                removed_something = true;
                if (log_flags.notice_debug) {
                    msg_printf(0, MSG_INFO,
                        "[notice] replacing identical older notice %d", n2.seqno
                    );
                }
            } else {
                n2.keep = true;
                retval = false;
                ++i;
                if (log_flags.notice_debug) {
                    msg_printf(0, MSG_INFO,
                        "[notice] keeping identical older notice %d", n2.seqno
                    );
                }
            }
        } else {
            ++i;
        }
    }
#ifndef SIM
    if (removed_something) {
        gstate.gui_rpcs.set_notice_refresh();
    }
#endif
    return retval;
}

// add a notice.
// 
bool NOTICES::append(NOTICE& n) {
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO,
            "[notice] processing notice: %s",
            strlen(n.title)?n.title:n.description.c_str()
        );
    }
    if (!remove_dups(n)) {
        return false;
    }
    if (notices.empty()) {
        n.seqno = 1;
    } else {
        n.seqno = notices.front().seqno + 1;
    }
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO,
            "[notice] adding notice %d", n.seqno
        );
    }
    notices.push_front(n);
#if 0
    if (!strlen(n.feed_url)) {
        write_archive(NULL);
    }
#endif
    return true;
}


// read and parse the contents of an archive file.
// If rfp is NULL it's a system msg, else a feed msg.
// insert items in NOTICES
//
int NOTICES::read_archive_file(const char* path, RSS_FEED* rfp) {
    FILE* f = fopen(path, "r");
    if (!f) {
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO,
                "[notice] no archive file %s", path
            );
        }
        return 0;
    }
    MIOFILE fin;
    fin.init_file(f);
    XML_PARSER xp(&fin);
    while (!xp.get_tag()) {
        if (!xp.is_tag) continue;
        if (xp.match_tag("/notices")) {
            fclose(f);
            return 0;
        }
        if (xp.match_tag("notice")) {
            NOTICE n;
            int retval = n.parse(xp);
            if (retval) {
                if (log_flags.notice_debug) {
                    msg_printf(0, MSG_INFO,
                        "[notice] archive item parse error: %d", retval
                    );
                }
            } else {
                if (rfp) {
                    safe_strcpy(n.feed_url, rfp->url);
                    safe_strcpy(n.project_name, rfp->project_name);
                }
                append(n);
            }
        }
    }
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO, "[notice] archive parse error");
    }
    fclose(f);
    return ERR_XML_PARSE;
}

// write archive file for the given RSS feed
// (or, if NULL, non-RSS notices)
//
void NOTICES::write_archive(RSS_FEED* rfp) {
    char path[MAXPATHLEN];

    if (rfp) {
        rfp->archive_file_name(path);
    } else {
        safe_strcpy(path, NOTICES_DIR"/archive.xml");
    }
    FILE* f = fopen(path, "w");
    if (!f) return;
    MIOFILE fout;
    fout.init_file(f);
    fout.printf("<notices>\n");
    if (!f) return;
    for (unsigned int i=0; i<notices.size(); i++) {
        NOTICE& n = notices[i];
        if (rfp) {
            if (strcmp(rfp->url, n.feed_url)) continue;
        } else {
            if (strlen(n.feed_url)) continue;
        }
        n.write(fout, false);
    }
    fout.printf("</notices>\n");
    fclose(f);
}

// Remove outdated notices
//
void NOTICES::remove_notices(PROJECT* p, int which) {
    deque<NOTICE>::iterator i = notices.begin();
    while (i != notices.end()) {
        NOTICE& n = *i;
        if (p && strcmp(n.project_name, p->get_project_name())) {
            ++i;
            continue;
        }
        bool remove = false;
        switch (which) {
        case REMOVE_NETWORK_MSG:
            remove = !strcmp(n.description.c_str(), NEED_NETWORK_MSG);
            break;
        case REMOVE_SCHEDULER_MSG:
            remove = !strcmp(n.category, "scheduler");
            break;
        case REMOVE_NO_WORK_MSG:
            remove = !strcmp(n.description.c_str(), NO_WORK_MSG);
            break;
        case REMOVE_CONFIG_MSG:
            remove = (strstr(n.description.c_str(), "cc_config.xml") != NULL);
            break;
        case REMOVE_APP_INFO_MSG:
            remove = (strstr(n.description.c_str(), "app_info.xml") != NULL);
            break;
        case REMOVE_APP_CONFIG_MSG:
            remove = (strstr(n.description.c_str(), "app_config.xml") != NULL);
            break;
        }
        if (remove) {
            i = notices.erase(i);
#ifndef SIM
            gstate.gui_rpcs.set_notice_refresh();
#endif
            if (log_flags.notice_debug) {
                msg_printf(p, MSG_INFO, "Removing notices of type %d", which);
            }
        } else {
            ++i;
        }
    }
}

// write notices newer than seqno as XML (for GUI RPC).
// Write them in order of increasing seqno
//
void NOTICES::write(int seqno, GUI_RPC_CONN& grc, bool public_only) {
    size_t i;
    MIOFILE mf;

    if (!net_status.need_physical_connection) {
        remove_notices(NULL, REMOVE_NETWORK_MSG);
    }
    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO, "NOTICES::write: seqno %d, refresh %s, %d notices",
            seqno, grc.get_notice_refresh()?"true":"false", (int)notices.size()
        );
    }
    grc.mfout.printf("<notices>\n");
    if (grc.get_notice_refresh()) {
        grc.clear_notice_refresh();
        NOTICE n;
        n.seqno = -1;
        seqno = -1;
        i = notices.size();
        n.write(grc.mfout, true);
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO, "NOTICES::write: sending -1 seqno notice");
        }
    } else {
        for (i=0; i<notices.size(); i++) {
            NOTICE& n = notices[i];
            if (n.seqno <= seqno) break;
        }
    }
    for (; i>0; i--) {
        NOTICE& n = notices[i-1];
        if (public_only && n.is_private) continue;
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO, "NOTICES::write: sending notice %d", n.seqno);
        }
        n.write(grc.mfout, true);
    }
    grc.mfout.printf("</notices>\n");
}

///////////// RSS_FEED ////////////////

void RSS_FEED::feed_file_name(char* path) {
    char buf[256];
    escape_project_url(url_base, buf);
    sprintf(path, NOTICES_DIR"/%s.xml", buf);
}

void RSS_FEED::archive_file_name(char* path) {
    char buf[256];
    escape_project_url(url_base, buf);
    sprintf(path, NOTICES_DIR"/archive_%s.xml", buf);
}

// read and parse the contents of the archive file;
// insert items in NOTICES
//
int RSS_FEED::read_archive_file() {
    char path[MAXPATHLEN];
    archive_file_name(path);
    return notices.read_archive_file(path, this);
}

// parse a feed descriptor (in scheduler reply or feed list file)
//
int RSS_FEED::parse_desc(XML_PARSER& xp) {
    strcpy(url, "");
    poll_interval = 0;
    next_poll_time = 0;
    while (!xp.get_tag()) {
        if (!xp.is_tag) continue;
        if (xp.match_tag("/rss_feed")) {
            if (!poll_interval || !strlen(url)) {
                if (log_flags.notice_debug) {
                    msg_printf(0, MSG_INFO,
                        "[notice] URL or poll interval missing in sched reply feed"
                    );
                }
                return ERR_XML_PARSE;
            }
            safe_strcpy(url_base, url);
            char* p = strchr(url_base, '?');
            if (p) *p = 0;
            return 0;
        }
        if (xp.parse_str("url", url, sizeof(url))) {
            xml_unescape(url);
        }
        if (xp.parse_double("poll_interval", poll_interval)) continue;
        if (xp.parse_double("next_poll_time", next_poll_time)) continue;
    }
    return ERR_XML_PARSE;
}

void RSS_FEED::write(MIOFILE& fout) {
    char buf[256];
    safe_strcpy(buf, url);
    xml_escape(url, buf, sizeof(buf));
    fout.printf(
        "  <rss_feed>\n"
        "    <url>%s</url>\n"
        "    <poll_interval>%f</poll_interval>\n"
        "    <next_poll_time>%f</next_poll_time>\n"
        "  </rss_feed>\n",
        buf,
        poll_interval,
        next_poll_time
    );
}

static inline bool create_time_asc(NOTICE n1, NOTICE n2) {
    return n1.create_time < n2.create_time;
}

// parse the actual RSS feed.
//
int RSS_FEED::parse_items(XML_PARSER& xp, int& nitems) {
    nitems = 0;
    int ntotal = 0, nerror = 0;
    int retval, func_ret = ERR_XML_PARSE;
    vector<NOTICE> new_notices;

    notices.clear_keep();

    while (!xp.get_tag()) {
        if (!xp.is_tag) continue;
        if (xp.match_tag("/rss")) {
            if (log_flags.notice_debug) {
                msg_printf(0, MSG_INFO,
                    "[notice] parsed RSS feed: total %d error %d added %d",
                    ntotal, nerror, nitems
                );
            }
            func_ret = 0;
            break;
        }
        if (xp.match_tag("item")) {
            NOTICE n;
            ntotal++;
            retval = n.parse_rss(xp);
            if (retval) {
                nerror++;
            } else if (n.create_time < gstate.now - 30*86400) {
                if (log_flags.notice_debug) {
                    msg_printf(0, MSG_INFO,
                        "[notice] item is older than 30 days: %s",
                        n.title
                    );
                }
            } else {
                n.arrival_time = gstate.now;
                n.keep = true;
                safe_strcpy(n.feed_url, url);
                safe_strcpy(n.project_name, project_name);
                new_notices.push_back(n);
            }
            continue;
        }
        if (xp.parse_int("error_num", retval)) {
            if (log_flags.notice_debug) {
                msg_printf(0,MSG_INFO,
                    "[notice] RSS fetch returned error %d (%s)",
                    retval,
                    boincerror(retval)
                );
            }
            return retval;
        }
    }

    //  sort new notices by increasing create time, and append them
    //
    std::sort(new_notices.begin(), new_notices.end(), create_time_asc);
    for (unsigned int i=0; i<new_notices.size(); i++) {
        NOTICE& n = new_notices[i];
        if (notices.append(n)) {
            nitems++;
        }
    }
    notices.unkeep(url);
    return func_ret;
}

void RSS_FEED::delete_files() {
    char path[MAXPATHLEN];
    feed_file_name(path);
    boinc_delete_file(path);
    archive_file_name(path);
    boinc_delete_file(path);
}

///////////// RSS_FEED_OP ////////////////

RSS_FEED_OP::RSS_FEED_OP() {
    error_num = BOINC_SUCCESS;
    gui_http = &gstate.gui_http;
}

// see if time to start new fetch
//
bool RSS_FEED_OP::poll() {
    unsigned int i;
    if (gstate.gui_http.is_busy()) return false;
    if (gstate.network_suspended) return false;
    for (i=0; i<rss_feeds.feeds.size(); i++) {
        RSS_FEED& rf = rss_feeds.feeds[i];
        if (gstate.now > rf.next_poll_time) {
            rf.next_poll_time = gstate.now + rf.poll_interval;
            char filename[256];
            rf.feed_file_name(filename);
            rfp = &rf;
            if (log_flags.notice_debug) {
                msg_printf(0, MSG_INFO,
                    "[notice] start fetch from %s", rf.url
                );
            }
            char url[1024];
            strcpy(url, rf.url);
            gstate.gui_http.do_rpc(this, url, filename, true);
            break;
        }
    }
    return false;
}

// handle a completed RSS feed fetch
//
void RSS_FEED_OP::handle_reply(int http_op_retval) {
    char filename[256];
    int nitems;

    if (!rfp) return;   // op was canceled

    if (http_op_retval) {
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO,
                "[notice] fetch of %s failed: %d", rfp->url, http_op_retval
            );
        }
        return;
    }

    if (log_flags.notice_debug) {
        msg_printf(0, MSG_INFO,
            "[notice] handling reply from %s", rfp->url
        );
    }

    rfp->feed_file_name(filename);
    FILE* f = fopen(filename, "r");
    if (!f) {
        msg_printf(0, MSG_INTERNAL_ERROR,
            "RSS feed file '%s' not found", filename
        );
        return;
    }
    MIOFILE fin;
    fin.init_file(f);
    XML_PARSER xp(&fin);
    int retval = rfp->parse_items(xp, nitems);
    if (retval) {
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO,
                "[notice] RSS parse error: %d", retval
            );
        }
    }
    fclose(f);

    notices.write_archive(rfp);
}

///////////// RSS_FEEDS ////////////////

static void init_proj_am(PROJ_AM* p) {
    FILE* f;
    MIOFILE fin;
    char path[MAXPATHLEN];

    project_feed_list_file_name(p, path);
    f = fopen(path, "r");
    if (f) {
        fin.init_file(f);
        XML_PARSER xp(&fin);
        parse_rss_feed_descs(xp, p->proj_feeds);
        fclose(f);
    }
}

// called on startup.  Get list of feeds.  Read archives.
//
void RSS_FEEDS::init() {
    unsigned int i;

    boinc_mkdir(NOTICES_DIR);

    if (!gstate.acct_mgr_info.get_no_project_notices()) {
        for (i=0; i<gstate.projects.size(); i++) {
            PROJECT* p = gstate.projects[i];
            init_proj_am(p);
        }
    }
    if (gstate.acct_mgr_info.using_am()) {
        init_proj_am(&gstate.acct_mgr_info);
    }

    update_feed_list();

    for (i=0; i<feeds.size(); i++) {
        RSS_FEED& rf = feeds[i];
        if (log_flags.notice_debug) {
            msg_printf(0, MSG_INFO,
                "[notice] feed: %s, %.0f sec",
                rf.url, rf.poll_interval
            );
        }
        rf.read_archive_file();
    }
}

RSS_FEED* RSS_FEEDS::lookup_url(char* url) {
    for (unsigned int i=0; i<feeds.size(); i++) {
        RSS_FEED& rf = feeds[i];
        if (!strcmp(rf.url, url)) {
            return &rf;
        }
    }
    return NULL;
}

// arrange to fetch the project's feeds
//
void RSS_FEEDS::trigger_fetch(PROJ_AM* p) {
    for (unsigned int i=0; i<p->proj_feeds.size(); i++) {
        RSS_FEED& rf = p->proj_feeds[i];
        RSS_FEED* rfp = lookup_url(rf.url);
        if (rfp) {
            rfp->next_poll_time = 0;
        }
    }
}

void RSS_FEEDS::update_proj_am(PROJ_AM* p) {
    unsigned int j;
    for (j=0; j<p->proj_feeds.size(); j++) {
        RSS_FEED& rf = p->proj_feeds[j];
        RSS_FEED* rfp = lookup_url(rf.url);
        if (rfp) {
            rfp->found = true;
        } else {
            rf.found = true;
            safe_strcpy(rf.project_name, p->get_project_name());
            feeds.push_back(rf);
            if (log_flags.notice_debug) {
                msg_printf(0, MSG_INFO,
                    "[notice] adding feed: %s, %.0f sec",
                    rf.url, rf.poll_interval
                );
            }
        }
    }
}

// the set of project feeds has changed.
// update the master list.
//
void RSS_FEEDS::update_feed_list() {
    unsigned int i;
    for (i=0; i<feeds.size(); i++) {
        RSS_FEED& rf = feeds[i];
        rf.found = false;
    }
    if (!gstate.acct_mgr_info.get_no_project_notices()) {
        for (i=0; i<gstate.projects.size(); i++) {
            PROJECT* p = gstate.projects[i];
            update_proj_am(p);
        }
    }
    if (gstate.acct_mgr_info.using_am()) {
        update_proj_am(&gstate.acct_mgr_info);
    }
    vector<RSS_FEED>::iterator iter = feeds.begin();
    while (iter != feeds.end()) {
        RSS_FEED& rf = *iter;
        if (rf.found) {
            iter++;
        } else {
            // cancel op if active
            //
            if (rss_feed_op.rfp == &(*iter)) {
                if (rss_feed_op.gui_http->is_busy()) {
                    gstate.http_ops->remove(&rss_feed_op.gui_http->http_op);
                }
                rss_feed_op.rfp = NULL;
            }
            if (log_flags.notice_debug) {
                msg_printf(0, MSG_INFO,
                    "[notice] removing feed: %s",
                    rf.url
                );
            }
            rf.delete_files();
            iter = feeds.erase(iter);
        }
    }
    write_feed_list();
}

void RSS_FEEDS::write_feed_list() {
    FILE* f = fopen(NOTICES_DIR"/feeds.xml", "w");
    if (!f) return;
    MIOFILE fout;
    fout.init_file(f);
    write_rss_feed_descs(fout, feeds);
    fclose(f);
}

void delete_project_notice_files(PROJECT* p) {
    char path[MAXPATHLEN];
    project_feed_list_file_name(p, path);
    boinc_delete_file(path);
}
