package net.i2p.client.streaming;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.data.Base32;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.EepGet;
import net.i2p.util.SocketTimeout;

/**
 *  Fetch a URL using a socket from the supplied I2PSocketManager.
 *  Hostname must resolve to an i2p destination - no routing to an outproxy.
 *  Does not support response gzip decompression (unlike I2PTunnelHTTPProxy) (yet),
 *  but of course there is still gzip at the I2CP layer.
 *
 *  This is designed for Java apps such as bittorrent clients that wish to
 *  do HTTP fetches and use other protocols on a single set of tunnels.
 *  This may provide anonymity benefits over using the shared clients HTTP proxy,
 *  preventing inadvertent outproxy usage, reduce resource usage by eliminating
 *  a second set of tunnels, and eliminate the requirement to
 *  to separately configure the proxy host and port.
 *
 *  For additional documentation see the superclass.
 *
 *  Supports http://example.i2p/blah
 *  Supports http://B32KEY.b32.i2p/blah
 *  Supports http://i2p/B64KEY/blah for compatibility with the eepproxy
 *  Supports http://B64KEY/blah for as of 0.9.42
 *  Supports http://B64KEY.i2p/blah as of 0.9.42
 *  Warning - does not support /eepproxy/blah, address helpers, http://B64KEY.i2p/blah,
 *  or other odd things that may be found in the HTTP proxy.
 *
 *  @author zzz
 */
public class I2PSocketEepGet extends EepGet {
    private final I2PSocketManager _socketManager;
    /** this replaces _proxy in the superclass. Sadly, I2PSocket does not extend Socket. */
    private I2PSocket _socket;
    
    /** from ConnectionOptions */
    private static final String PROP_CONNECT_DELAY = "i2p.streaming.connectDelay";
    private static final String CONNECT_DELAY = "500";

    public I2PSocketEepGet(I2PAppContext ctx, I2PSocketManager mgr, int numRetries, String outputFile, String url) {
        this(ctx, mgr, numRetries, -1, -1, outputFile, null, url);
    }

    public I2PSocketEepGet(I2PAppContext ctx, I2PSocketManager mgr, int numRetries, long minSize, long maxSize,
                           String outputFile, OutputStream outputStream, String url) {
        // we're using this constructor:
        // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) {
        super(ctx, false, null, -1, numRetries, minSize, maxSize, outputFile, outputStream, url, true, null, null);
        _socketManager = mgr;
    }
   
    /**
     *  We have to override this to close _socket, since we can't use _proxy in super as the I2PSocket.
     */
    @Override
    public boolean fetch(long fetchHeaderTimeout, long totalTimeout, long inactivityTimeout) {
        boolean rv = super.fetch(fetchHeaderTimeout, totalTimeout, inactivityTimeout);
        if (_socket != null) {
            try { 
                _socket.close(); 
                _socket = null;
            } catch (IOException ioe) {}
        }
        return rv;
    }

    /**
     *  Overridden to disable inline gunzipping
     *  @since 0.8.10
     */
    @Override
    protected void readHeaders() throws IOException {
        try {
            super.readHeaders();
        } finally {
            _isGzippedResponse = false;
        }
    }

    /**
     *  Look up the address, get a socket from the I2PSocketManager supplied in the constructor,
     *  and send the request.
     *
     *  @param timeout ignored 
     */
    @Override
    protected void sendRequest(SocketTimeout timeout) throws IOException {
        if (_outputStream == null) {
            File outFile = new File(_outputFile);
            if (outFile.exists())
                _alreadyTransferred = outFile.length();
        }

        if (_proxyIn != null) try { _proxyIn.close(); } catch (IOException ioe) {}
        if (_proxyOut != null) try { _proxyOut.close(); } catch (IOException ioe) {}
        if (_socket != null) try { _socket.close(); } catch (IOException ioe) {}

        try {
            URI url = new URI(_actualURL);
            if ("http".equals(url.getScheme())) {
                Destination dest = null;
                String host = url.getHost();
                if (host == null) {
                    String ann = _actualURL;
                    // URI can't handle b64dest.i2p if it contains '~'
                    // but it doesn't throw an exception, just returns a null host
                    if (ann.startsWith("http://") && ann.length() >= 7 + 516 && ann.contains("~")) {
                        ann = ann.substring(7);
                        int slash = ann.indexOf('/');
                        if (slash >= 516) {
                            ann = ann.substring(0, slash);
                            if (ann.endsWith(".i2p"))
                                ann = ann.substring(0, ann.length() - 4);
                            try {
                                dest = new Destination(ann);
                            } catch (DataFormatException dfe) {}
                        }
                    }
                    if (dest == null)
                        throw new MalformedURLException("no hostname: " + _actualURL);
                    // won't pick up the port either, but the path will be OK
                }
                int port = url.getPort();
                if (port <= 0 || port > 65535)
                    port = 80;

                // HTTP Proxy compatibility http://i2p/B64KEY/blah
                // Rewrite the url to strip out the /i2p/,
                // as the naming service accepts B64KEY (but not B64KEY.i2p atm)
                if ("i2p".equals(host)) {
                    String file = url.getRawPath();
                    try {
                        int slash = 1 + file.substring(1).indexOf('/');
                        host = file.substring(1, slash);
                        _actualURL = "http://" + host + file.substring(slash);
                        String query = url.getRawQuery();
                        if (query != null)
                            _actualURL = _actualURL + '?' + query;
                    } catch (IndexOutOfBoundsException ioobe) {
                        throw new MalformedURLException("Bad /i2p/ format: " + _actualURL);
                    }
                }

                if (dest == null) {
                    // Use existing I2PSession for lookups.
                    // This is much more efficient than using the naming service
                    I2PSession sess = _socketManager.getSession();
                    if (sess != null && !sess.isClosed()) {
                        try {
                            if (host.length() == 60 && host.endsWith(".b32.i2p")) {
                                byte[] b = Base32.decode(host.substring(0, 52));
                                if (b != null) {
                                    Hash h = Hash.create(b);
                                    dest = sess.lookupDest(h, 20*1000);
                                } else {
                                    dest = null;
                                }
                            } else {
                                dest = sess.lookupDest(host, 20*1000);
                            }
                        } catch (I2PSessionException ise) {
                            dest = null;
                        }
                    } else {
                        dest = _context.namingService().lookup(host);
                    }
                    if (dest == null)
                        throw new UnknownHostException("Unknown or non-i2p host: " + host);
                }

                // Set the timeouts, using the other existing options in the socket manager
                // This currently duplicates what SocketTimeout is doing in EepGet,
                // but when that's ripped out of EepGet to use setsotimeout, we'll need this.
                Properties props = new Properties();
                props.setProperty(I2PSocketOptions.PROP_CONNECT_TIMEOUT, Integer.toString(CONNECT_TIMEOUT));
                props.setProperty(I2PSocketOptions.PROP_READ_TIMEOUT, Integer.toString(INACTIVITY_TIMEOUT));
                // This is important - even if the underlying socket doesn't have a connect delay,
                // we want to set it for this connection, so the request headers will go out
                // in the SYN packet, saving one RTT.
                props.setProperty(PROP_CONNECT_DELAY, CONNECT_DELAY);
                I2PSocketOptions opts = _socketManager.buildOptions(props);
                opts.setPort(port);
                _socket = _socketManager.connect(dest, opts);
            } else {
                throw new MalformedURLException("Unsupported protocol: " + _actualURL);
            }
        } catch (URISyntaxException use) {
            IOException ioe = new MalformedURLException("Bad URL");
            ioe.initCause(use);
            throw ioe;
        } catch (I2PException ie) {
            throw new IOException("I2P error", ie);
        }

        _proxyIn = _socket.getInputStream();
        _proxyOut = _socket.getOutputStream();
        
        // SocketTimeout doesn't take an I2PSocket, but no matter, because we
        // always close our socket in fetch() above.
        //timeout.setSocket(_socket);
        
        String req = getRequest();
        _proxyOut.write(DataHelper.getUTF8(req));
        _proxyOut.flush();
    }

    /**
     *  Guess we have to override this since
     *  super doesn't strip the http://host from the GET line
     *  which hoses some servers (opentracker)
     *  HTTP proxy was kind enough to do this for us
     */
    @Override
    protected String getRequest() throws IOException {
        StringBuilder buf = new StringBuilder(2048);
        URI url;
        try {
            url = new URI(_actualURL);
        } catch (URISyntaxException use) {
            IOException ioe = new MalformedURLException("Bad URL");
            ioe.initCause(use);
            throw ioe;
        }
        //String host = url.getHost();
        String path = url.getRawPath();
        String query = url.getRawQuery();
        if (query != null)
            path = path + '?' + query;
        if (!path.startsWith("/"))
	    path = '/' + path;
        buf.append("GET ").append(path).append(" HTTP/1.1\r\n" +
                   "Host: ").append(url.getHost()).append("\r\n");
        if (_alreadyTransferred > 0) {
            buf.append("Range: bytes=");
            buf.append(_alreadyTransferred);
            buf.append("-\r\n");
        }
        buf.append("Accept-Encoding: \r\n" +
                   "Cache-Control: no-cache\r\n" +
                   "Pragma: no-cache\r\n" +
                   "Connection: close\r\n");
        boolean uaOverridden = false;
        if (_extraHeaders != null) {
            for (String hdr : _extraHeaders) {
                if (hdr.toLowerCase(Locale.US).startsWith("user-agent: "))
                    uaOverridden = true;
                buf.append(hdr).append("\r\n");
            }
        }
        if(!uaOverridden)
            buf.append("User-Agent: " + USER_AGENT + "\r\n");
        buf.append("\r\n");
        if (_log.shouldDebug())
            _log.debug("Request: [" + buf.toString() + "]");
        return buf.toString();
    }

    /**
     * I2PSocketEepGet [-n #retries] [-t timeout] url
     * Uses I2CP at localhost:7654 with a single 1-hop tunnel each direction.
     * Tunnel build time not included in the timeout.
     *
     * This is just for testing.
     * Real command line apps should use EepGet.main(),
     * which has more options, and you don't have to wait for tunnels to be built.
     */ 
/****
    public static void main(String args[]) {
        int numRetries = 0;
        long inactivityTimeout = INACTIVITY_TIMEOUT;
        String url = null;
        try {
            for (int i = 0; i < args.length; i++) {
                if (args[i].equals("-n")) {
                    numRetries = Integer.parseInt(args[i+1]);
                    i++;
                } else if (args[i].equals("-t")) {
                    inactivityTimeout = 1000 * Integer.parseInt(args[i+1]);
                    i++;
                } else if (args[i].startsWith("-")) {
                    usage();
                    return;
                } else {
                    url = args[i];
                }
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
            usage();
            return;
        }
        
        if (url == null) {
            usage();
            return;
        }

        Properties opts = new Properties();
        opts.setProperty("i2cp.dontPublishLeaseSet", "true");
        opts.setProperty("inbound.quantity", "1");
        opts.setProperty("outbound.quantity", "1");
        opts.setProperty("inbound.length", "1");
        opts.setProperty("outbound.length", "1");
        opts.setProperty("inbound.nickname", "I2PSocketEepGet");
        I2PSocketManager mgr = I2PSocketManagerFactory.createManager(opts);
        if (mgr == null) {
            System.err.println("Error creating the socket manager");
            return;
        }
        I2PSocketEepGet get = new I2PSocketEepGet(I2PAppContext.getGlobalContext(),
                                                  mgr, numRetries, suggestName(url), url);
        get.addStatusListener(get.new CLIStatusListener(1024, 40));
        get.fetch(inactivityTimeout, -1, inactivityTimeout);
        mgr.destroySocketManager();
    }
    
    private static void usage() {
        System.err.println("I2PSocketEepGet [-n #retries] [-t timeout] url");
    }
****/
}
