/*
 *  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.EOFException;
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.nio.charset.Charsets;
import java.security.Permission;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

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";

    private String password = "";

    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");
            int reply = getReply();
            if (reply != FTP_FILEOK && dir.length() > 0 && dir.charAt(0) == '/') {
                write("CWD " + dir.substring(1) + "\r\n");
                reply = getReply();
            }
            if (reply != FTP_FILEOK) {
                throw new IOException("Unable to change directories");
            }
        }
    }

    /**
     * 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 {
            ProxySelector selector = ProxySelector.getDefault();
            if (selector != null) {
                proxyList = selector.select(uri);
            }
        }
        if (null == proxyList) {
            currentProxy = null;
            connectInternal();
        } else {
            ProxySelector selector = ProxySelector.getDefault();
            Iterator<Proxy> iter = proxyList.iterator();
            boolean connectOK = false;
            String failureReason = "";
            while (iter.hasNext() && !connectOK) {
                currentProxy = iter.next();
                try {
                    connectInternal();
                    connectOK = true;
                } catch (IOException ioe) {
                    failureReason = ioe.getLocalizedMessage();
                    // 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("Unable to connect to server: " + failureReason);
            }
        }
    }

    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("Could not establish data connection");
        }
        if (getDoInput()) {
            inputStream = new FtpURLInputStream(
                    new BufferedInputStream(dataSocket.getInputStream()), controlSocket);
        }
    }

    /**
     * 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 "content/unknown";
        }
        return result;
    }

    private void getFile() throws IOException {
        int reply;
        String file = url.getFile();
        write("RETR " + file + "\r\n");
        reply = getReply();
        if (reply == FTP_NOTFOUND && file.length() > 0 && file.charAt(0) == '/') {
            write("RETR " + file.substring(1) + "\r\n");
            reply = getReply();
        }
        if (!(reply == FTP_OPENDATA || reply == FTP_TRANSFEROK)) {
            throw new FileNotFoundException("Unable to retrieve file: " + reply);
        }
    }

    /**
     * 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");
    }

    /**
     * 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];
        for (int i = 0; i < code.length; i++) {
            final int tmp = ctrlInput.read();
            if (tmp == -1) {
                throw new EOFException();
            }
            code[i] = (byte) tmp;
        }
        replyCode = new String(code, 0, code.length, Charsets.ISO_8859_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 */
            }
        }

        try {
            return Integer.parseInt(replyCode);
        } catch (NumberFormatException e) {
            throw (IOException)(new IOException("reply code is invalid").initCause(e));
        }
    }

    private void login() throws IOException {
        int reply;
        reply = getReply();
        if (reply == FTP_USERREADY) {
        } else {
            throw new IOException("Unable to connect to server: " + url.getHost());
        }
        write("USER " + username + "\r\n");
        reply = getReply();
        if (reply == FTP_PASWD || reply == FTP_LOGGEDIN) {
        } else {
            throw new IOException("Unable to log in to server (USER): " + url.getHost());
        }
        if (reply == FTP_PASWD) {
            write("PASS " + password + "\r\n");
            reply = getReply();
            if (!(reply == FTP_OK || reply == FTP_USERREADY || reply == FTP_LOGGEDIN)) {
                throw new IOException("Unable to log in to server (PASS): " + url.getHost());
            }
        }
    }

    private void port() throws IOException {
        write("PORT "
                + controlSocket.getLocalAddress().getHostAddress().replace('.',
                        ',') + ',' + (dataPort >> 8) + ','
                + (dataPort & 255)
                + "\r\n");
        if (getReply() != FTP_OK) {
            throw new IOException("Unable to configure data port");
        }
    }

    /**
     * Read a line of text and return it for possible parsing
     */
    private String readLine() throws IOException {
        StringBuilder sb = new StringBuilder();
        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 "
                + url.getFile().substring(url.getFile().lastIndexOf('/') + 1,
                        url.getFile().length()) + "\r\n");
        reply = getReply();
        if (!(reply == FTP_OPENDATA || reply == FTP_OK || reply == FTP_DATAOPEN)) {
            throw new IOException("Unable to store file");
        }
    }

    /**
     * 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");
        if (getReply() != FTP_OK) {
            throw new IOException("Unable to set transfer type");
        }
    }

    private void write(String command) throws IOException {
        ctrlOutput.write(command.getBytes(Charsets.ISO_8859_1));
    }
}
