package org.limewire.net;

import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.StringTokenizer;

import org.limewire.io.IP;
import org.limewire.io.NetworkUtils;
import org.limewire.io.IOUtils;

/**
 * Represents a WHOIS request. The request value will be
 * sent to the appropriate whois server, and the response
 * will be parsed for name:value pairs.
 * 
 * Some whois servers merely report the authoritative
 * server for a given domain, and we support ONE level of
 * forwarding here.
 * 
 */
public class WhoIsRequestImpl implements WhoIsRequest {
    
    /**
     * Map of whois servers keyed on the tld and "0" (zero)
     * for IP addresses.
     */
    protected Map<String,String> servers;
    
    /**
     * The exact query value.
     */
    protected String name;
    
    /**
     * All parsable values in the reply for this request.
     */
    protected Map<String,String> values;
    
    /**
     * Name of the whois server that was used to fulfill
     * this request (assuming that the request has already
     * been fulfilled. Null until then.
     */
    protected String whoisServer;
    
    /**
     * To prevent us from getting caught in any annoying
     * infinite loops, use this value to make sure that
     * we only follow a whois referral once.
     */
    protected Set<String> referrals;
    
    /**
     * The socket manager, as you may have guessed.
     */
    protected SocketsManager socketsManager;
    
    /**
     * Creates a new request object with a request name.
     * It is this request name that will be handed off to
     * the whois server and the request can be either for
     * a DNS name or an IP address.
     * 
     * @param name Request name (ie, "apple.com" or 
     * "17.149.160.49").
     */
    public WhoIsRequestImpl(String name, SocketsManager socketsManager, Map<String,String> defaultServers) {
        if (name == null)
            throw new NullPointerException();
        
        if (name.length() == 0)
            throw new IllegalArgumentException("Zero-length name is not allowed.");
        
        // if there are no periods in the request name, 
        // then it is definitely not a valid DNS name nor
        // an IP address.
        //
        // if the period is at the end of the request name,
        // then it is similarly invalid.
        //
        {
            int ndx = name.lastIndexOf(".");
            
            if (ndx == -1 || ndx == name.length() - 1)
                throw new IllegalArgumentException("Invalid request name.");
        }
        
        this.name = name;
        this.values = new HashMap<String,String>();
        this.socketsManager = socketsManager;
        this.referrals = new HashSet<String>();
        this.servers = defaultServers;
    }
    
    /**
     * Connects to the whois server, sends the request and
     * then reads and parses the reply. Parsed values are
     * accessible via getValue().
     * 
     * In most circumstances, this should return in well 
     * under one second, but with sockets there is no such
     * guarantee. Don't block in the UI on a call to this.
     * 
     */
    public void doRequest() throws IOException {
        // only parse and find a whois server if there is
        // not already one defined.
        //
        // if this is a second call to doRequest(), based
        // on following a referral from a previous request,
        // then the whois server will have already been
        // defined for us.
        //
        if (this.whoisServer == null)
            this.whoisServer = this.getWhoIsServer();
        
        // no server, no luck
        if (this.whoisServer == null)
            throw new IllegalArgumentException("Unsupported request name, '" + this.name + "'");
        
        // store this whois server so that when following
        // referrals, we do not ever enter a loop.
        //
        this.referrals.add(this.whoisServer);
        
        // there really isn't a standard whois format, 
        // other than to say that there is a rough
        // adherence to a "name:value" format, where the
        // value portion may be on the same line or the
        // next line, or it may be multiple lines.
        //
        // you might say that the value extends until
        // the start of the next "name:value" pair.
        //
        // we will store all names in upper-case.
        //
        // 1. if a value starts on the same line as its
        //    corresponding name, then the value is also
        //    wholly contained on that one line.
        //
        // 2. names are always followed by colons.
        //
        // 3. names are sometimes followed by colon-space.
        //
        // 4. arbitrary whitespace before, after and 
        //    between names and values must be supported.
        //
        // 5. if a name is re-used, only the last value is
        //    retained.
        //
        {
            StringTokenizer st = new StringTokenizer(doRequestGetWhoIsResponse(), "\r\n");
            String name = null;
            StringBuilder value = new StringBuilder(100);
            boolean skipTillRegistrant = false;
            
            // we're splitting on new-lines only.
            while (st.hasMoreTokens()) {
                int index = 0;
                String line = st.nextToken().trim();
                
                // some text is often included with the 
                // whois result, and we want to skip that
                // text. the start of final portion of the
                // data we want to parse appears to always
                // be "Registrant:". look for that point to
                // continue parsing, if we find that we are
                // in a block of text that we don't care 
                // about.
                //
                if (skipTillRegistrant) {
                    if (-1 == line.indexOf("Registrant:"))
                        continue;
                }
                else if (-1 != line.indexOf(">>>") || -1 != line.indexOf("NOTICE:") || -1 != line.indexOf("TERMS OF USE:")) {
                    skipTillRegistrant = true;
                    continue;
                }
                else if (line.startsWith("#")) {
                    continue;
                }
                
                // if we are already parsing a value....
                if (name != null) {
                    // if this is the start of a new name:value pair....
                    if (-1 != (index = line.indexOf(":"))) {
                        this.setValue(name.toUpperCase(Locale.ENGLISH), value.toString());
                        name = line.substring(0, index);
                        value = new StringBuilder(100);
                        
                        // if this is a one-line name:value pair...
                        if ((line = line.substring(index+1).trim()).length() > 0) {
                            if (line.startsWith("//")) {
                                name = null;
                                continue;
                            }
                            
                            this.setValue(name.toUpperCase(Locale.ENGLISH), line);
                            name = null;
                        }
                    }
                    // if this is a continuation of a value....
                    else if (value.length() != 0) {
                        value.append(" ");
                        value.append(line);
                    }
                    else {
                        value.append(line);
                    }
                }
                // if this is the start of a new name:value pair....
                else if (-1 != (index = line.indexOf(":"))) {
                    name = line.substring(0, index);
                    
                    // if this is a one-line name:value pair...
                    if ((line = line.substring(index+1).trim()).length() > 0) {
                        if (line.startsWith("//")) {
                            name = null;
                            continue;
                        }
                        
                        this.setValue(name.toUpperCase(Locale.ENGLISH), line);
                        name = null;
                    }
                }
                // this is garbage text in the reply that we don't care about....
                else {
                }
            }
            
            if (this.name != null)
                this.setValue(this.name.toUpperCase(Locale.ENGLISH), value.toString());
        }
        
        // if this whois response contains the key "WHOIS 
        // SERVER", then we should refer to the target
        // server specified there. only do this once,
        // however.
        //
        // don't follow the referral if the server we are
        // presently talking to is the server mentioned in
        // "WHOIS SERVER".
        //
        {
            String referral = this.values.get("WHOIS SERVER");
            
            if (referral != null && referral.length() != 0 && 
                    !this.referrals.contains(referral)) {
                this.whoisServer = referral;
                this.doRequest();
            }
        }
    }
    
    /**
     * Get an arbitrary value by name.
     * 
     * @param name The name of the value that you want.
     * @return The value associated with the given name.
     */
    public String getValue(String name) {
        return values.get(name);
    }
    
    /**
     * Set a value for the given name.
     * 
     * @param name Key name for reply value
     * @param value Value to store for the key.
     */
    protected void setValue(String name, String value) {
        values.put(name, value);
    }
    
    /**
     * Returns the net range of the ip address, or null if
     * there is none.
     */
    public IP getNetRange () {
        String range = this.getValue("CIDR");
        
        if (range != null)
            return new IP(range);
        
        return null;
    }
    
    /**
     * Contacts the whois server, sends the request, reads
     * and returns the reply.
     * 
     * @return Verbatim reply from the whois server.
     * @throws IOException 
     */
    protected String doRequestGetWhoIsResponse() throws IOException {
        StringBuilder sb = new StringBuilder(1000);
        Socket socket = null;
        
        // connect, send the request, read the reply, close
        // the socket.
        //
        try {
            socket = socketsManager.connect(new InetSocketAddress(this.whoisServer, 43), 0);
            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();
            byte[] bytes = new byte[1000];
            int read = 0;
            
            os.write((this.name+"\r\n").getBytes());
            
            // read until we hit EOF or the socket closes.
            try {
                while (0 < (read = is.read(bytes, 0, 1000))) {
                    if (read > 0)
                        sb.append(new String(bytes, 0, read, "US-ASCII"));
                }
                
            }
            catch (IOException e2) {
                // this can occur when the socket closes.
            }
        }
        catch (IOException e) {
            throw e;
        }
        finally {
            IOUtils.close(socket);
        }
        
        return sb.toString();
    }
    
    /**
     * Returns the appropriate whois server based on the
     * server list for the request name.
     * 
     * @return Appropriate whois server host name.
     * @throws
     */
    public String getWhoIsServer() {
        String server = null;
        
        if (NetworkUtils.isDottedIPV4(this.name))
            server = this.servers.get("0");
        else
            server = this.servers.get(this.name.substring(this.name.lastIndexOf(".")+1).toLowerCase(Locale.ENGLISH));
        
        return server;
    }
    
}
