package net.i2p.router.peermanager;
/*
 * 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 net.i2p.data.Hash;
import net.i2p.router.ProfileManager;
import net.i2p.router.RouterContext;
import net.i2p.util.Log;

/**
 *  Methods to update profiles.
 *  Unless otherwise noted, methods are blocking on the reorganize lock.
 */
public class ProfileManagerImpl implements ProfileManager {
    private final Log _log;
    private final RouterContext _context;
    
    public ProfileManagerImpl(RouterContext context) {
        _context = context;
        _log = _context.logManager().getLog(ProfileManagerImpl.class);
    }
    
    /**
     * Note that it took msToSend to send a message of size bytesSent to the peer over the transport.
     * This should only be called if the transport considered the send successful.
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void messageSent(Hash peer, String transport, long msToSend, long bytesSent) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastSendSuccessful(_context.clock().now());
        //data.getSendSuccessSize().addData(bytesSent, msToSend);
    }
    
    /**
     * Note that the router failed to send a message to the peer over the transport specified.
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void messageFailed(Hash peer, String transport) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastSendFailed(_context.clock().now());
    }
    
    /**
     * Note that the router failed to send a message to the peer over any transport.
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void messageFailed(Hash peer) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastSendFailed(_context.clock().now());
    }
    
    /**
     * Note that there was some sort of communication error talking with the peer
     *
     */
    public void commErrorOccurred(Hash peer) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Comm error occurred for peer " + peer.toBase64(), new Exception("Comm error"));
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        data.setLastSendFailed(_context.clock().now());
    }
    
    /**
     * Note that the router agreed to participate in a tunnel
     *
     */
    public void tunnelJoined(Hash peer, long responseTimeMs) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        data.getTunnelCreateResponseTime().addData(responseTimeMs, responseTimeMs);
        data.setLastHeardFrom(_context.clock().now());
        data.getTunnelHistory().incrementAgreedTo();
    }
    
    /**
     * Note that a router explicitly rejected joining a tunnel.  
     *
     * @param responseTimeMs ignored
     * @param severity how much the peer doesnt want to participate in the 
     *                 tunnel (large == more severe)
     */
    public void tunnelRejected(Hash peer, long responseTimeMs, int severity) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        data.getTunnelHistory().incrementRejected(severity);
    }
    
    /**
     * Note that a router did not respond to a tunnel join. 
     *
     * Since TunnelHistory doesn't have a timeout stat, pretend we were
     * rejected for bandwidth reasons.
     */
    public void tunnelTimedOut(Hash peer) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        data.getTunnelHistory().incrementRejected(TunnelHistory.TUNNEL_REJECT_BANDWIDTH);
    }
    
    /**
     * Note that a tunnel that the router is participating in
     * was successfully tested with the given round trip latency
     *
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    @SuppressWarnings("deprecation")
    public void tunnelTestSucceeded(Hash peer, long responseTimeMs) {
        if (PeerProfile.ENABLE_TUNNEL_TEST_RESPONSE_TIME) {
            PeerProfile data = getProfileNonblocking(peer);
            if (data == null) return;
            data.updateTunnelTestTimeAverage(responseTimeMs);
            data.getTunnelTestResponseTime().addData(responseTimeMs, responseTimeMs);
        }
    }
    
    /**
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void tunnelDataPushed(Hash peer, long rtt, int size) {
        if (_context.routerHash().equals(peer))
            return;
        PeerProfile data = getProfileNonblocking(peer);
        if (data != null)
            data.dataPushed(size); // ignore rtt, as we are averaging over a minute
    }

    /**
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void tunnelDataPushed1m(Hash peer, int size) {
        if (_context.routerHash().equals(peer))
            return;
        PeerProfile data = getProfileNonblocking(peer);
        if (data != null)
            data.dataPushed1m(size);
    }

    
    /**
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void tunnelLifetimePushed(Hash peer, long lifetime, long size) {
        if (_context.routerHash().equals(peer))
            return;
        PeerProfile data = getProfileNonblocking(peer);
        if (data != null)
            data.tunnelDataTransferred(size);
    }
    
    
    /**
     * Note that the peer participated in a tunnel that failed.  Its failure may not have
     * been the peer's fault however.
     * Blame the peer with a probability of pct/100.
     *
     */
    public void tunnelFailed(Hash peer, int pct) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        data.getTunnelHistory().incrementFailed(pct);
    }
    
    /**
     * Note that the peer was able to return the valid data for a db lookup
     *
     * This will force creation of DB stats
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void dbLookupSuccessful(Hash peer, long responseTimeMs) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        if (!data.getIsExpandedDB())
            data.expandDBProfile();
        data.getDbResponseTime().addData(responseTimeMs, responseTimeMs);
        DBHistory hist = data.getDBHistory();
        hist.lookupSuccessful();
    }
    
    /**
     * Note that the peer was unable to reply to a db lookup - either with data or with
     * a lookupReply redirecting the user elsewhere
     *
     * This will force creation of DB stats
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void dbLookupFailed(Hash peer) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        if (!data.getIsExpandedDB())
            data.expandDBProfile();
        DBHistory hist = data.getDBHistory();
        hist.lookupFailed();
    }
    
    /**
     * Note that the peer replied to a db lookup with a redirect to other routers, where
     * the list of redirected users included newPeers routers that the local router didn't
     * know about, oldPeers routers that the local router already knew about, the given invalid
     * routers that were invalid in some way, and the duplicate number of routers that we explicitly
     * asked them not to send us, but they did anyway
     *
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void dbLookupReply(Hash peer, int newPeers, int oldPeers, int invalid, int duplicate, long responseTimeMs) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        if (!data.getIsExpandedDB())
            return;
        data.getDbResponseTime().addData(responseTimeMs, responseTimeMs);
        data.getDbIntroduction().addData(newPeers, responseTimeMs);
        DBHistory hist = data.getDBHistory();
        hist.lookupReply(newPeers, oldPeers, invalid, duplicate);
    }
    
    /**
     * Note that the local router received a db lookup from the given peer
     *
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void dbLookupReceived(Hash peer) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        if (!data.getIsExpandedDB())
            return;
        //DBHistory hist = data.getDBHistory();
        //hist.lookupReceived();
    }
    
    /**
     * Note that the local router received an unprompted db store from the given peer
     *
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void dbStoreReceived(Hash peer, boolean wasNewKey) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        if (!data.getIsExpandedDB())
            return;
        DBHistory hist = data.getDBHistory();
        hist.unpromptedStoreReceived(wasNewKey);
    }
    
    /**
     * Note that we've confirmed a successful send of db data to the peer (though we haven't
     * necessarily requested it again from them, so they /might/ be lying)
     *
     * This is not really interesting, since they could be lying, so we do not
     * increment any DB stats at all. On verify, call dbStoreSuccessful().
     *
     * @param responseTimeMs ignored
     */
    public void dbStoreSent(Hash peer, long responseTimeMs) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        long now = _context.clock().now();
        data.setLastHeardFrom(now);
        data.setLastSendSuccessful(now);
        //if (!data.getIsExpandedDB())
        //    data.expandDBProfile();
        //DBHistory hist = data.getDBHistory();
        //hist.storeSuccessful();
    }
    
    /**
     * Note that we've verified a successful send of db data to the floodfill peer
     * by querying another floodfill.
     *
     * This will force creation of DB stats
     */
    public void dbStoreSuccessful(Hash peer) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        long now = _context.clock().now();
        data.setLastHeardFrom(now);
        data.setLastSendSuccessful(now);
        if (!data.getIsExpandedDB())
            data.expandDBProfile();
        DBHistory hist = data.getDBHistory();
        hist.storeSuccessful();
    }
    
    /**
     * Note that we were unable to confirm a successful send of db data to
     * the peer, at least not within our timeout period
     *
     * This will force creation of DB stats
     */
    public void dbStoreFailed(Hash peer) {
        PeerProfile data = getProfile(peer);
        //if (data == null) return;
        if (!data.getIsExpandedDB())
            data.expandDBProfile();
        DBHistory hist = data.getDBHistory();
        hist.storeFailed();
        // we could do things like update some sort of "how many successful stores we've
        // failed to send them"...
    }
    
    /**
     * Note that the local router received a reference to the given peer, either
     * through an explicit dbStore or in a dbLookupReply
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void heardAbout(Hash peer) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardAbout(_context.clock().now());
    }

    /**
     * Note that the local router received a reference to the given peer
     * at a certain time. Only update the time if newer.
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void heardAbout(Hash peer, long when) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardAbout(when);
    }
    
    /**
     * Note that the router received a message from the given peer on the specified
     * transport.  Messages received without any "from" information aren't recorded
     * through this metric.  If msToReceive is negative, there was no timing information
     * available.
     * Non-blocking. Will not update the profile if we can't get the lock.
     */
    public void messageReceived(Hash peer, String style, long msToReceive, int bytesRead) {
        PeerProfile data = getProfileNonblocking(peer);
        if (data == null) return;
        data.setLastHeardFrom(_context.clock().now());
        //data.getReceiveSize().addData(bytesRead, msToReceive);
    }
    
    /**
     *   Blocking.
     *   Creates a new profile if it didn't exist.
     *   @return non-null
     */
    private PeerProfile getProfile(Hash peer) {
        PeerProfile prof = _context.profileOrganizer().getProfile(peer);
        if (prof == null) {
            prof = new PeerProfile(_context, peer);
            _context.profileOrganizer().addProfile(prof);
        }
        return prof;
    }
    
    /**
     *  Non-blocking.
     *  Creates a new profile if it didn't exist.
     *  @return null if the fetch or create would have blocked
     *  @since 0.8.12
     */
    private PeerProfile getProfileNonblocking(Hash peer) {
        return _context.profileOrganizer().getOrCreateProfileNonblocking(peer);
    }
}
