package net.i2p.router.networkdb;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import net.i2p.data.DataFormatException;
import net.i2p.data.router.RouterAddress;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.SigningPrivateKey;
import net.i2p.router.JobImpl;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;

/**
 * Publish the local router's RouterInfo periodically.
 * NOTE - this also creates and signs the RI.
 * This is run immediately at startup... but doesn't really
 * send to the floodfills until the second time it runs.
 */
public class PublishLocalRouterInfoJob extends JobImpl {
    private final Log _log;

    /**
     *  Don't store if somebody else stored it recently.
     *  Must be less than PUBLISH_DELAY * 3 / 16 (see getDelay())
     */
    private static final long MIN_PUBLISH_DELAY = 9*60*1000;

    /**
     *  Too short and the network puts a big connection load on the
     *  floodfills since we store directly.
     *  Too long and the floodfill will drop us - timeout is 60 minutes.
     */
    private static final long PUBLISH_DELAY = 43*60*1000;

    /** this needs to be long enough to give us time to start up,
     *  but less than 20m (when we start accepting tunnels and could be a IBGW)
     *  Actually no, we need this soon if we are a new router or
     *  other routers have forgotten about us, else
     *  we can't build IB exploratory tunnels.
     *
     *  First publish after netdb ready is now done via state machine
     *  in Router.setNetDbReady(), so we probably don't need this anymore
     */
    private static final long FIRST_TIME_DELAY = 90*1000;
    private volatile boolean _notFirstTime;
    private final AtomicInteger _runCount = new AtomicInteger();
    
    public PublishLocalRouterInfoJob(RouterContext ctx) {
        super(ctx);
        _log = ctx.logManager().getLog(PublishLocalRouterInfoJob.class);
    }
    
    public String getName() { return "Publish Local Router Info"; }

    public void runJob() {
        if (!getContext().commSystem().isRunning()) {
            // Avoid deadlock in the transports through here via FNDF.publish() at startup
            if (_log.shouldWarn())
                _log.warn("PLRIJ before comm system started");
            requeue(100);
            return;
        }
        long last = getContext().netDb().getLastRouterInfoPublishTime();
        long now = getContext().clock().now();
        if (last + MIN_PUBLISH_DELAY > now) {
            long delay = getDelay();
            requeue(last + delay - now);
            return;
        }
        RouterInfo oldRI = getContext().router().getRouterInfo();
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Old routerInfo contains " + oldRI.getAddresses().size() 
                       + " addresses and " + oldRI.getOptionsMap().size() + " options");
        try {
            List<RouterAddress> oldAddrs = new ArrayList<RouterAddress>(oldRI.getAddresses());
            List<RouterAddress> newAddrs = getContext().commSystem().createAddresses();
            int count = _runCount.incrementAndGet();
            RouterInfo ri = new RouterInfo(oldRI);
            if (_notFirstTime && (count % 4) != 0 && oldAddrs.size() == newAddrs.size()) {
                // 3 times out of 4, we don't republish if everything is the same...
                // If something changed, including the cost, then publish,
                // otherwise don't.
                String newcaps = getContext().router().getCapabilities();
                boolean different = !oldRI.getCapabilities().equals(newcaps);
                if (!different) {
                    Comparator<RouterAddress> comp = new AddrComparator();
                    Collections.sort(oldAddrs, comp);
                    Collections.sort(newAddrs, comp);
                    for (int i = 0; i < oldAddrs.size(); i++) {
                        // deepEquals() includes cost
                        if (!oldAddrs.get(i).deepEquals(newAddrs.get(i))) {
                            different = true;
                            break;
                        }
                    }
                    if (!different) {
                        if (_log.shouldLog(Log.INFO))
                            _log.info("Not republishing early because costs and caps and addresses are the same");
                        requeue(getDelay());
                        return;
                    }
                }
                if (_log.shouldLog(Log.INFO))
                    _log.info("Republishing early because addresses or costs or caps have changed -" +
                              " oldCaps: " + oldRI.getCapabilities() + " newCaps: " + newcaps +
                              " old:\n" +
                              oldAddrs + "\nnew:\n" + newAddrs);
            }
            ri.setPublished(getContext().clock().now());
            Properties stats = getContext().statPublisher().publishStatistics();
            ri.setOptions(stats);
            ri.setAddresses(newAddrs);

            SigningPrivateKey key = getContext().keyManager().getSigningPrivateKey();
            if (key == null) {
                _log.log(Log.CRIT, "Internal error - signing private key not known?  rescheduling publish for 30s");
                requeue(30*1000);
                return;
            }
            ri.sign(key);
            getContext().router().setRouterInfo(ri);
            if (_log.shouldLog(Log.INFO))
                _log.info("Newly updated routerInfo is published with " + stats.size() 
                          + "/" + ri.getOptionsMap().size() + " options on " 
                          + new Date(ri.getPublished()));
            try {
                // This won't really publish until the netdb is initialized.
                getContext().netDb().publish(ri);
            } catch (IllegalArgumentException iae) {
                _log.log(Log.CRIT, "Error publishing our identity - corrupt? Restart required", iae);
                getContext().router().rebuildNewIdentity();
            }
        } catch (DataFormatException dfe) {
            _log.error("Error signing the updated local router info!", dfe);
        }
        if (_notFirstTime) {
            requeue(getDelay());
        } else {
            // First publish after netdb ready is now done via state machine
            // in Router.setNetDbReady(), so we probably don't need this anymore
            // but leave it in for now, a router may have trouble publishing right away
            requeue(FIRST_TIME_DELAY);
            _notFirstTime = true;
        }
    }

    private long getDelay() {
        long rv = (PUBLISH_DELAY * 3 / 4) + getContext().random().nextLong(PUBLISH_DELAY / 4);
        // run 4x as often as usual publish time (see above)
        rv /= 4;
        return rv;
    }

    /**
     *  Arbitrary sort so we can attempt to compare costs between two RIs to see if they have changed
     *
     *  @since 0.9.18
     */
    private static class AddrComparator implements Comparator<RouterAddress>, Serializable {
        public int compare(RouterAddress l, RouterAddress r) {
            int c = l.getTransportStyle().compareTo(r.getTransportStyle());
            if (c != 0)
                return c;
            String lh = l.getHost();
            String rh = r.getHost();
            if (lh == null)
                return rh == null ? 0 : -1;
            if (rh == null)
                return 1;
            return lh.compareTo(rh);
        }
    }
}
