/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.harmony.luni.internal.net.www.protocol.ftp;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketPermission;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.harmony.luni.internal.net.www.MimeTable;
import org.apache.harmony.luni.net.NetUtil;
import org.apache.harmony.luni.util.Msg;

public class FtpURLConnection extends URLConnection {

    private static final int FTP_PORT = 21;

    // FTP Reply Constants
    private static final int FTP_DATAOPEN = 125;

    private static final int FTP_OPENDATA = 150;

    private static final int FTP_OK = 200;

    private static final int FTP_USERREADY = 220;

    private static final int FTP_TRANSFEROK = 226;

    // private static final int FTP_PASV = 227;

    private static final int FTP_LOGGEDIN = 230;

    private static final int FTP_FILEOK = 250;

    private static final int FTP_PASWD = 331;

    // private static final int FTP_DATAERROR = 451;

    // private static final int FTP_ERROR = 500;

    private static final int FTP_NOTFOUND = 550;

    private Socket controlSocket;

    private Socket dataSocket;

    private ServerSocket acceptSocket;

    private InputStream ctrlInput;

    private InputStream inputStream;

    private OutputStream ctrlOutput;

    private int dataPort;

    private String username = "anonymous"; //$NON-NLS-1$

    private String password = ""; //$NON-NLS-1$

    private String replyCode;

    private String hostName;

    private Proxy proxy;

    private Proxy currentProxy;

    private URI uri;

    /**
     * FtpURLConnection constructor comment.
     * 
     * @param url
     */
    protected FtpURLConnection(URL url) {
        super(url);
        hostName = url.getHost();
        String parse = url.getUserInfo();
        if (parse != null) {
            int split = parse.indexOf(':');
            if (split >= 0) {
                username = parse.substring(0, split);
                password = parse.substring(split + 1);
            } else {
                username = parse;
            }
        }
        uri = null;
        try {
            uri = url.toURI();
        } catch (URISyntaxException e) {
            // do nothing.
        }
    }

    /**
     * FtpURLConnection constructor.
     * 
     * @param url
     * @param proxy
     */
    protected FtpURLConnection(URL url, Proxy proxy) {
        this(url);
        this.proxy = proxy;
    }

    /**
     * Change the server directory to that specified in the URL
     */
    private void cd() throws IOException {
        int idx = url.getFile().lastIndexOf('/');

        if (idx > 0) {
            String dir = url.getFile().substring(0, idx);
            write("CWD " + dir + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
            int reply = getReply();
            if (reply != FTP_FILEOK && dir.length() > 0 && dir.charAt(0) == '/') {
                write("CWD " + dir.substring(1) + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
                reply = getReply();
            }
            if (reply != FTP_FILEOK) {
                throw new IOException(Msg.getString("K0094")); //$NON-NLS-1$
            }
        }
    }

    /**
     * Establishes the connection to the resource specified by this
     * <code>URL</code>
     * 
     * @see #connected
     * @see java.io.IOException
     * @see URLStreamHandler
     */
    @Override
    public void connect() throws IOException {
        // Use system-wide ProxySelect to select proxy list,
        // then try to connect via elements in the proxy list.
        List<Proxy> proxyList = null;
        if (null != proxy) {
            proxyList = new ArrayList<Proxy>(1);
            proxyList.add(proxy);
        } else {
            proxyList = NetUtil.getProxyList(uri);
        }
        if (null == proxyList) {
            currentProxy = null;
            connectInternal();
        } else {
            ProxySelector selector = ProxySelector.getDefault();
            Iterator<Proxy> iter = proxyList.iterator();
            boolean connectOK = false;
            while (iter.hasNext() && !connectOK) {
                currentProxy = iter.next();
                try {
                    connectInternal();
                    connectOK = true;
                } catch (IOException ioe) {
                    // If connect failed, callback "connectFailed"
                    // should be invoked.
                    if (null != selector && Proxy.NO_PROXY != currentProxy) {
                        selector
                                .connectFailed(uri, currentProxy.address(), ioe);
                    }
                }
            }
            if (!connectOK) {
                throw new IOException(Msg.getString("K0097")); //$NON-NLS-1$
            }
        }
    }

    private void connectInternal() throws IOException {
        int port = url.getPort();
        int connectTimeout = getConnectTimeout();
        if (port <= 0) {
            port = FTP_PORT;
        }
        if (null == currentProxy || Proxy.Type.HTTP == currentProxy.type()) {
            controlSocket = new Socket();
        } else {
            controlSocket = new Socket(currentProxy);
        }
        InetSocketAddress addr = new InetSocketAddress(hostName, port);
        controlSocket.connect(addr, connectTimeout);
        connected = true;
        ctrlOutput = controlSocket.getOutputStream();
        ctrlInput = controlSocket.getInputStream();
        login();
        setType();
        if (!getDoInput()) {
            cd();
        }

        try {
            acceptSocket = new ServerSocket(0);
            dataPort = acceptSocket.getLocalPort();
            /* Cannot set REUSEADDR so we need to send a PORT command */
            port();
            if (connectTimeout == 0) {
                // set timeout rather than zero as before
                connectTimeout = 3000;
            }
            acceptSocket.setSoTimeout(getConnectTimeout());
            if (getDoInput()) {
                getFile();
            } else {
                sendFile();
            }
            dataSocket = acceptSocket.accept();
            dataSocket.setSoTimeout(getReadTimeout());
            acceptSocket.close();
        } catch (InterruptedIOException e) {
            throw new IOException(Msg.getString("K0095")); //$NON-NLS-1$
        }
        if (getDoInput()) {
            // BEGIN android-modified
            inputStream = new FtpURLInputStream(
                    new BufferedInputStream(dataSocket.getInputStream(), 8192),
                    controlSocket);
            // END android-modified
        }

    }

    /**
     * Returns the content type of the resource. Just takes a guess based on the
     * name.
     */
    @Override
    public String getContentType() {
        String result = guessContentTypeFromName(url.getFile());
        if (result == null) {
            return MimeTable.UNKNOWN;
        }
        return result;
    }

    private void getFile() throws IOException {
        int reply;
        String file = url.getFile();
        write("RETR " + file + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
        reply = getReply();
        if (reply == FTP_NOTFOUND && file.length() > 0 && file.charAt(0) == '/') {
            write("RETR " + file.substring(1) + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
            reply = getReply();
        }
        if (!(reply == FTP_OPENDATA || reply == FTP_TRANSFEROK)) {
            throw new FileNotFoundException(Msg.getString("K0096", reply)); //$NON-NLS-1$
        }
    }

    /**
     * Creates a input stream for writing to this URL Connection.
     * 
     * @return The input stream to write to
     * @throws IOException
     *             Cannot read from URL or error creating InputStream
     * 
     * @see #getContent()
     * @see #getOutputStream()
     * @see java.io.InputStream
     * @see java.io.IOException
     * 
     */
    @Override
    public InputStream getInputStream() throws IOException {
        if (!connected) {
            connect();
        }
        return inputStream;
    }

    /**
     * Returns the permission object (in this case, SocketPermission) with the
     * host and the port number as the target name and "resolve, connect" as the
     * action list.
     * 
     * @return the permission object required for this connection
     * @throws IOException
     *             thrown when an IO exception occurs during the creation of the
     *             permission object.
     */
    @Override
    public Permission getPermission() throws IOException {
        int port = url.getPort();
        if (port <= 0) {
            port = FTP_PORT;
        }
        return new SocketPermission(hostName + ":" + port, "connect, resolve"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Creates a output stream for writing to this URL Connection.
     * 
     * @return The output stream to write to
     * @throws IOException
     *             when the OutputStream could not be created
     * 
     * @see #getContent()
     * @see #getInputStream()
     * @see java.io.IOException
     * 
     */

    @Override
    public OutputStream getOutputStream() throws IOException {
        if (!connected) {
            connect();
        }
        return dataSocket.getOutputStream();
    }

    private int getReply() throws IOException {
        byte[] code = new byte[3];
        ctrlInput.read(code, 0, code.length);
        replyCode = new String(code, "ISO8859_1"); //$NON-NLS-1$
        boolean multiline = false;
        if (ctrlInput.read() == '-') {
            multiline = true;
        }
        readLine(); /* Skip the rest of the first line */
        if (multiline) {
            while (readMultiLine()) {/* Read all of a multiline reply */
            }
        }
        return Integer.parseInt(new String(code, "ISO8859_1")); //$NON-NLS-1$
    }

    private void login() throws IOException {
        int reply;
        reply = getReply();
        if (reply == FTP_USERREADY) {
        } else {
            throw new IOException(Msg.getString("K0097", url.getHost())); //$NON-NLS-1$
        }
        write("USER " + username + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
        reply = getReply();
        if (reply == FTP_PASWD || reply == FTP_LOGGEDIN) {
        } else {
            throw new IOException(Msg.getString("K0098", url.getHost())); //$NON-NLS-1$
        }
        if (reply == FTP_PASWD) {
            write("PASS " + password + "\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
            reply = getReply();
            if (!(reply == FTP_OK || reply == FTP_USERREADY || reply == FTP_LOGGEDIN)) {
                throw new IOException(Msg.getString("K0098", url.getHost())); //$NON-NLS-1$
            }
        }
    }

    private void port() throws IOException {
        write("PORT " //$NON-NLS-1$
                + controlSocket.getLocalAddress().getHostAddress().replace('.',
                        ',') + ',' + (dataPort >> 8) + ','
                + (dataPort & 255)
                + "\r\n"); //$NON-NLS-1$
        if (getReply() != FTP_OK) {
            throw new IOException(Msg.getString("K0099")); //$NON-NLS-1$
        }
    }

    /**
     * Read a line of text and return it for possible parsing
     */
    private String readLine() throws IOException {
        StringBuffer sb = new StringBuffer();
        int c;
        while ((c = ctrlInput.read()) != '\n') {
            sb.append((char) c);
        }
        return sb.toString();
    }

    private boolean readMultiLine() throws IOException {
        String line = readLine();
        if (line.length() < 4) {
            return true;
        }
        if (line.substring(0, 3).equals(replyCode)
                && (line.charAt(3) == (char) 32)) {
            return false;
        }
        return true;
    }

    /**
     * Issue the STOR command to the server with the file as the parameter
     */
    private void sendFile() throws IOException {
        int reply;
        write("STOR " //$NON-NLS-1$
                + url.getFile().substring(url.getFile().lastIndexOf('/') + 1,
                        url.getFile().length()) + "\r\n"); //$NON-NLS-1$
        reply = getReply();
        if (!(reply == FTP_OPENDATA || reply == FTP_OK || reply == FTP_DATAOPEN)) {
            throw new IOException(Msg.getString("K009a")); //$NON-NLS-1$
        }
    }

    /**
     * Set the flag if this <code>URLConnection</code> supports input (read).
     * It cannot be set after the connection is made. FtpURLConnections cannot
     * support both input and output
     * 
     * @param newValue *
     * @throws IllegalAccessError
     *             when this method attempts to change the flag after connected
     * 
     * @see #doInput
     * @see #getDoInput()
     * @see java.lang.IllegalAccessError
     * @see #setDoInput(boolean)
     */
    @Override
    public void setDoInput(boolean newValue) {
        if (connected) {
            throw new IllegalAccessError();
        }
        this.doInput = newValue;
        this.doOutput = !newValue;
    }

    /**
     * Set the flag if this <code>URLConnection</code> supports output(read).
     * It cannot be set after the connection is made.\ FtpURLConnections cannot
     * support both input and output.
     * 
     * @param newValue
     * 
     * @throws IllegalAccessError
     *             when this method attempts to change the flag after connected
     * 
     * @see #doOutput
     * @see java.lang.IllegalAccessError
     * @see #setDoOutput(boolean)
     */
    @Override
    public void setDoOutput(boolean newValue) {
        if (connected) {
            throw new IllegalAccessError();
        }
        this.doOutput = newValue;
        this.doInput = !newValue;
    }

    /**
     * Set the type of the file transfer. Only Image is supported
     */
    private void setType() throws IOException {
        write("TYPE I\r\n"); //$NON-NLS-1$
        if (getReply() != FTP_OK) {
            throw new IOException(Msg.getString("K009b")); //$NON-NLS-1$
        }
    }

    private void write(String command) throws IOException {
        ctrlOutput.write(command.getBytes("ISO8859_1")); //$NON-NLS-1$
    }
}
