/*
 * By zzz 2008, released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 */
package net.i2p.client.naming;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.StringTokenizer;

import net.i2p.I2PAppContext;
import net.i2p.data.Destination;
import net.i2p.util.EepGet;

/**
 * A network-based naming service using HTTP, with in-memory caching.
 * Fetches from one or more remote (in-i2p) CGI services using HTTP GET.
 *
 * The remote HTTP service takes a CGI parameter and must return (only) the
 * 516-byte Base64 destination, or hostname=dest.
 * A trailing \n or \r\n is acceptable.
 *
 * Should be used from MetaNamingService, after HostsTxtNamingService.
 * Cannot be used as the only NamingService! Be sure any naming service hosts
 * are in hosts.txt.
 * Supports caching, b32, and b64.
 *
 * Sample config to put in configadvanced.jsp (restart required):
 *
 * i2p.naming.impl=net.i2p.client.naming.MetaNamingService
 * i2p.nameservicelist=net.i2p.client.naming.HostsTxtNamingService,net.i2p.client.naming.EepGetNamingService
 * i2p.naming.eepget.list=http://namingservice.i2p/cgi-bin/lkup.cgi?host=,http://i2host.i2p/cgi-bin/i2hostquery?
 *
 */
public class EepGetNamingService extends DummyNamingService {

    private final static String PROP_EEPGET_LIST = "i2p.naming.eepget.list";
    private final static String DEFAULT_EEPGET_LIST = "http://i2host.i2p/cgi-bin/i2hostquery?";

    /** 
     * The naming service should only be constructed and accessed through the 
     * application context.  This constructor should only be used by the 
     * appropriate application context itself.
     *
     */
    public EepGetNamingService(I2PAppContext context) {
        super(context);
    }
    
    private List<String> getURLs() {
        String list = _context.getProperty(PROP_EEPGET_LIST, DEFAULT_EEPGET_LIST);
        StringTokenizer tok = new StringTokenizer(list, ",");
        List<String> rv = new ArrayList<String>(tok.countTokens());
        while (tok.hasMoreTokens())
            rv.add(tok.nextToken());
        return rv;
    }
    
    @Override
    public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
        Destination d = super.lookup(hostname, null, null);
        if (d != null)
            return d;

        hostname = hostname.toLowerCase(Locale.US);
        // Base32 failed?
        if (hostname.length() == BASE32_HASH_LENGTH + 8 && hostname.endsWith(".b32.i2p"))
            return null;

        List<String> URLs = getURLs();
        if (URLs.isEmpty())
            return null;

        // prevent lookup loops - this cannot be the only lookup service
        for (int i = 0; i < URLs.size(); i++) { 
            String url = URLs.get(i);
            if (url.startsWith("http://" + hostname + "/")) {
                _log.error("Lookup loop: " + hostname);
                return null;
            }
        }

        // lookup
        for (int i = 0; i < URLs.size(); i++) { 
            String url = URLs.get(i);
            String key = fetchAddr(url, hostname);	  	
            if (key != null) {
                _log.error("Success: " + url + hostname);
                d = lookupBase64(key);
                putCache(hostname, d);
                return d;
            }
        }
        return null;
    }

    // FIXME allow larger Dests for non-null Certs
    private static final int MAX_RESPONSE = DEST_SIZE + 68 + 10; // allow for hostname= and some trailing stuff
    private String fetchAddr(String url, String hostname) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(MAX_RESPONSE);
        
        try {
            // Do a proxied eepget into our ByteArrayOutputStream with 0 retries
            EepGet get = new EepGet(_context, true, "localhost", 4444, 0, DEST_SIZE, MAX_RESPONSE,
                                    null, baos, url + hostname, false, null, null);
            // 10s header timeout, 15s total timeout, unlimited inactivity timeout
            if (get.fetch(10*1000l, 15*1000l, -1l)) {
                if (baos.size() < DEST_SIZE) {
                    _log.error("Short response: " + url + hostname);
                    return null;
                }
                String key = baos.toString();
                if (key.startsWith(hostname + "="))  // strip hostname=
                    key = key.substring(hostname.length() + 1); 
                key = key.substring(0, DEST_SIZE);   // catch IndexOutOfBounds exception below
                if (!key.endsWith("AA")) {
                    _log.error("Invalid key: " + url + hostname);
                    return null;
                }
                if (key.replaceAll("[a-zA-Z0-9~-]", "").length() != 0) {
                    _log.error("Invalid chars: " + url + hostname);
                    return null;
                }
                return key;
            }
            _log.error("Fetch failed from: " + url + hostname);
            return null;
        } catch (Throwable t) {
            _log.error("Error fetching the addr", t);
        }
        _log.error("Caught from: " + url + hostname);
        return null;
    }
}
