package edu.unl.consystlab.ftp;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Properties;

public class FTPClient
{

	/**
	 *  Revision control id
	 */

	/**
	 *  Socket responsible for controlling
	 *  the connection
	 */
	private FTPControlSocket control = null;

	/**
	 *  Socket responsible for transferring 
	 *  the data
	 */
	private Socket data = null;

	public short tmode = -1; // Data transfer mode. 0=GET 1=PUT -1=none
	public long tbytes = 0; // Total bytes to transfer
	public long sbytes = 0; // Number of bytes transferred
	/**
	 *  Record of the transfer type
	 */
	private FTPTransferType transferType = null;

	/**
	 *  Constructor. Creates the control 
	 *  socket
	 *
	 *  @param   remoteHost  the remote hostname
	 */
	public FTPClient(String remoteHost) throws IOException, FTPException
	{

		control = new FTPControlSocket(remoteHost);
	}

	/**
	 *  Constructor. Creates the control 
	 *  socket
	 *
	 *  @param   remoteHost  the remote hostname
	 *  @param   controlPort  port for control stream
	 */
	public FTPClient(String remoteHost, int controlPort)
		throws IOException, FTPException
	{

		control = new FTPControlSocket(remoteHost, controlPort);
	}

	/**
	 *  Constructor. Creates the control 
	 *  socket
	 *
	 *  @param   remoteAddr  the address of the
	 *                       remote host
	 */
	public FTPClient(InetAddress remoteAddr) throws IOException, FTPException
	{

		control = new FTPControlSocket(remoteAddr);
	}

	/**
	 *  Constructor. Creates the control 
	 *  socket. Allows setting of control port (normally
	 *  set by default to 21).
	 *
	 *  @param   remoteAddr  the address of the
	 *                       remote host
	 *  @param   controlPort  port for control stream
	 */
	public FTPClient(InetAddress remoteAddr, int controlPort)
		throws IOException, FTPException
	{

		control = new FTPControlSocket(remoteAddr, controlPort);
	}

	/**
	 *  Login into an account on the FTP server. This
	 *  call completes the entire login process
	 *
	 *  @param   user       user name
	 *  @param   password   user's password
	 */
	public void login(String user, String password)
		throws IOException, FTPException
	{

		String response = control.sendCommand("USER " + user);
		control.validateReply(response, "331");
		response = control.sendCommand("PASS " + password);
		control.validateReply(response, "230");
	}

	/**
	 *  Supply the user name to log into an account 
	 *  on the FTP server. Must be followed by the
	 *  password() method - but we allow for 
	 *
	 *  @param   user       user name
	 *  @param   password   user's password
	 */
	public void user(String user) throws IOException, FTPException
	{

		String reply = control.sendCommand("USER " + user);

		// we allow for a site with no password - 230 response
		String[] validCodes = { "230", "331" };
		control.validateReply(reply, validCodes);
	}

	/**
	 *  Supplies the password for a previously supplied
	 *  username to log into the FTP server. Must be
	 *  preceeded by the user() method
	 *
	 *  @param   user       user name
	 *  @param   password   user's password
	 */
	public void password(String password) throws IOException, FTPException
	{

		String reply = control.sendCommand("PASS " + password);

		// we allow for a site with no passwords (202)
		String[] validCodes = { "230", "202" };
		control.validateReply(reply, validCodes);
	}

	/**
	 *  Set up SOCKS v4 proxy settings. This can be used if there
	 *  is a SOCKS proxy server in place that must be connected thru.
	 *
	 *  @param  port  SOCKS proxy port
	 *  @param  host  SOCKS proxy hostname
	 */
	public void initSOCKS(String port, String host)
	{

		Properties props = System.getProperties();
		props.put("socksProxyPort", port);
		props.put("socksProxyHost", host);
		System.setProperties(props);
	}

	/**
	 *  Get the name of the remote host
	 *
	 *  @return  remote host name
	 */
	String getRemoteHostName()
	{
		return control.getRemoteHostName();
	}

	/**
	 *  Issue arbitrary ftp commands to the FTP server.
	 *     
	 *  @param command     ftp command to be sent to server
	 *  @param validCodes  valid return codes for this command
	 */
	public void quote(String command, String[] validCodes)
		throws IOException, FTPException
	{

		String reply = control.sendCommand(command);

		// allow for no validation to be supplied
		if (validCodes != null && validCodes.length > 0)
			control.validateReply(reply, validCodes);
	}

	/**
	 *  Put data onto the FTP server. It
	 *  is placed in the current directory.
	 *
	 *  @param  data        array of bytes
	 *  @param  remoteFile  name of remote file in
	 *                      current directory
	 */
	public void put(InputStream in, String remoteFile)
		throws IOException, FTPException
	{

		put(in, remoteFile, false);
	}

	/**
	 *  Put data onto the FTP server. It
	 *  is placed in the current directory. Allows
	 *  appending if current file exists
	 *
	 *  @param  data        array of bytes
	 *  @param  remoteFile  name of remote file in
	 *                      current directory
	 *  @param  append      true if appending, false otherwise
	 */
	public void put(InputStream in, String remoteFile, boolean append)
		throws IOException, FTPException
	{

		// get an output stream
		data = control.createDataSocket();
		DataOutputStream out = new DataOutputStream(data.getOutputStream());

		// send the command to store
		String cmd = append ? "APPE " : "STOR ";
		String reply = control.sendCommand(cmd + remoteFile);

		// Can get a 125 or a 150 
		String[] validCodes1 = { "125", "150" };
		control.validateReply(reply, validCodes1);
		byte[] buf = new byte[4096];
		int len;
		tmode = 1;
		tbytes = in.available();
		sbytes = 0;
		// write stream
		len = in.read(buf);
		while (len > -1)
		{
			out.write(buf, 0, len);
			sbytes += len;
			len = in.read(buf);
		}

		// flush and clean up
		out.flush();
		out.close();

		// and close the data socket
		try
		{
			data.close();
		}
		catch (IOException ignore)
		{
		}

		tmode = -1;
		// check the control response  
		String[] validCodes2 = { "226", "250" };
		reply = control.readReply();
		control.validateReply(reply, validCodes2);
	}

	public BufferedWriter putStart(String remoteFile)
		throws IOException, FTPException
	{
		return putStart(remoteFile, false);
	}

	//OutputStreamWriter
	public BufferedWriter putStart(String remoteFile, boolean append)
		throws IOException, FTPException
	{

		// get an output stream
		data = control.createDataSocket();
		DataOutputStream dos = new DataOutputStream(data.getOutputStream());
		OutputStreamWriter osw = new OutputStreamWriter(dos);
		BufferedWriter bw = new BufferedWriter(osw);

		// send the command to store
		String cmd = append ? "APPE " : "STOR ";
		String reply = control.sendCommand(cmd + remoteFile);

		// Can get a 125 or a 150 
		String[] validCodes1 = { "125", "150" };
		control.validateReply(reply, validCodes1);
		return bw;
	}

	public void putEnd(BufferedWriter bw) throws IOException, FTPException
	{
		// flush and clean up
		bw.flush();
		bw.close();

		// and close the data socket
		try
		{
			data.close();
		}
		catch (IOException ignore)
		{
		}

		tmode = -1;
		// check the control response  
		String[] validCodes2 = { "226", "250" };
		String reply = control.readReply();
		control.validateReply(reply, validCodes2);
	}

	/**
	 *  Get data from the FTP server. Uses the currently
	 *  set transfer mode. Retrieve as a byte array. Note
	 *  that we may experience memory limitations as the
	 *  entire file must be held in memory at one time.
	 *
	 *  @param  remoteFile  name of remote file in
	 *                      current directory
	 */
	public byte[] get(String remoteFile) throws IOException, FTPException
	{

		// get an input stream to read data from
		data = control.createDataSocket();
		DataInputStream in = new DataInputStream(data.getInputStream());
		BufferedInputStream bIn = new BufferedInputStream(in);

		// send the retrieve command
		String reply = control.sendCommand("RETR " + remoteFile);

		// Can get a 125 or a 150 
		String[] validCodes1 = { "125", "150" };
		control.validateReply(reply, validCodes1);

		// do the retrieving
		int chunksize = 4096;
		byte[] chunk = new byte[chunksize]; // read chunks into
		byte[] resultBuf = new byte[chunksize]; // where we place chunks
		byte[] temp = null; // temp swap buffer
		int count; // size of chunk read
		int bufsize = 0; // size of resultBuf

		// read from socket & write to file
		while ((count = bIn.read(chunk, 0, chunksize)) >= 0)
		{

			// new buffer to hold current buf + new chunk
			temp = new byte[bufsize + count];

			// copy current buf to temp
			System.arraycopy(resultBuf, 0, temp, 0, bufsize);

			// copy new chunk onto end of temp
			System.arraycopy(chunk, 0, temp, bufsize, count);

			// re-assign temp buffer to buf
			resultBuf = temp;

			// update size of buffer
			bufsize += count;
		}

		// close streams
		try
		{
			bIn.close();
			data.close();
		}
		catch (IOException ignore)
		{
		}

		// check the control response  
		String[] validCodes2 = { "226", "250" };
		reply = control.readReply();
		control.validateReply(reply, validCodes2);

		return resultBuf;
	}

	public BufferedReader getStart(String remoteFile)
		throws IOException, FTPException
	{
		// get an input stream to read data from
		data = control.createDataSocket();
		DataInputStream din = new DataInputStream(data.getInputStream());
		BufferedReader br = new BufferedReader(
		    new InputStreamReader(din));

		// send the retrieve command
		String reply = control.sendCommand("RETR " + remoteFile);

		// Can get a 125 or a 150 
		String[] validCodes1 = { "125", "150" };
		control.validateReply(reply, validCodes1);
		
		return br;
	}
	
	public void getEnd(BufferedReader br) throws IOException, FTPException
	{
		// close streams
		try
		{
			br.close();
			data.close();
		}
		catch (IOException ignore)
		{
		}

		// check the control response  
		String[] validCodes2 = { "226", "250" };
		String reply = control.readReply();
		control.validateReply(reply, validCodes2);		
	}

	/**
	 *  Run a site-specific command on the
	 *  server. Support for commands is dependent
	 *  on the server
	 *
	 *  @param  command   the site command to run    
	 *  @return true if command ok, false if
	 *          command not implemented
	 */
	public boolean site(String command) throws IOException, FTPException
	{

		// send the retrieve command
		String reply = control.sendCommand("SITE " + command);

		// Can  a 200 (ok) or 202 (not impl). Some
		// FTP servers return 502 (not impl)
		String[] validCodes = { "200", "202", "502" };
		control.validateReply(reply, validCodes);

		// return true or false? 200 is ok, 202/502 not
		// implemented
		if (reply.substring(0, 3).equals("200"))
			return true;
		else
			return false;
	}

	/**
	 *  List a directory's contents
	 *
	 *  @param  mask  the file mask to use
	 */
	public String list(String mask) throws IOException, FTPException
	{

		return list(mask, false);
	}

	/**
	 *  List a directory's contents
	 *
	 *  @param  mask  the file mask to use
	 *  @param  full  true if detailed listing required
	 *                false otherwise
	 */
	public String list(String mask, boolean full)
		throws IOException, FTPException
	{

		// first set type to ascii if binary
		boolean isBinary = false;
		if (transferType.equals(FTPTransferType.BINARY))
		{
			isBinary = true;
			setType(FTPTransferType.ASCII);
		}

		// get an input stream to read data from
		data = control.createDataSocket();
		InputStreamReader in = new InputStreamReader(data.getInputStream());
		BufferedReader bIn = new BufferedReader(in);

		// send the retrieve command
		String command = full ? "LIST " : "NLST ";
		String reply = control.sendCommand(command + mask);

		// Can get a 125 or a 150 
		String[] validCodes1 = { "125", "150" };
		control.validateReply(reply, validCodes1);

		// get the listing ... this comes on the data channel
		// do the retrieving
		int chunksize = 4096;
		char[] chunk = new char[chunksize]; // read chunks into
		char[] resultBuf = new char[chunksize]; // where we place chunks
		char[] temp = null; // temp swap buffer
		int count; // size of chunk read
		int bufsize = 0; // size of resultBuf

		// read from socket & write to file
		while ((count = bIn.read(chunk, 0, chunksize)) >= 0)
		{

			// new buffer to hold current buf + new chunk
			temp = new char[bufsize + count];

			// copy current buf to temp
			System.arraycopy(resultBuf, 0, temp, 0, bufsize);

			// copy new chunk onto end of temp
			System.arraycopy(chunk, 0, temp, bufsize, count);

			// re-assign temp buffer to buf
			resultBuf = temp;

			// update size of buffer
			bufsize += count;
		}

		// close streams. NOTE! For NLST we close our end before
		// reading the reply! wu-ftpd requires this.
		try
		{
			bIn.close();
			data.close();
		}
		catch (IOException ignore)
		{
		}

		// check the control response  
		String[] validCodes2 = { "226", "250" };
		reply = control.readReply();
		control.validateReply(reply, validCodes2);

		// reset type
		if (isBinary)
			setType(FTPTransferType.BINARY);

		String result = null;
		if (resultBuf.length > 0)
			result = new String(resultBuf);
		return result; // this is the String with the listing in it
	}

	/**
	 *  Switch debug of responses on or off
	 *
	 *  @param  on  true if you wish to have responses to 
	 *              stdout, false otherwise
	 */
	public void debugResponses(boolean on)
	{

		control.debugResponses(on);
	}

	/**
	 *  Get the current transfer type
	 *
	 *  @return  the current type of the transfer,
	 *           i.e. BINARY or ASCII  
	 */
	public FTPTransferType getType()
	{

		return transferType;
	}

	/**
	 *  Set the transfer type
	 *
	 *  @param  type  the transfer type to
	 *                set the server to
	 */
	public void setType(FTPTransferType type) throws IOException, FTPException
	{

		// determine the character to send
		String typeStr = FTPTransferType.ASCII_CHAR;
		if (type.equals(FTPTransferType.BINARY))
			typeStr = FTPTransferType.BINARY_CHAR;

		// send the command
		String reply = control.sendCommand("TYPE " + typeStr);
		control.validateReply(reply, "200");

		// record the type
		transferType = type;
	}

	/**
	 *  Delete the specified remote file
	 *
	 *  @param  remoteFile  name of remote file to
	 *                      delete
	 */
	public void delete(String remoteFile) throws IOException, FTPException
	{

		String reply = control.sendCommand("DELE " + remoteFile);
		control.validateReply(reply, "250");
	}

	/**
	 *  Rename a file or directory
	 *
	 * @param from  name of file or directory to rename
	 * @param to    intended name
	 */
	public void rename(String from, String to) throws IOException, FTPException
	{

		String reply = control.sendCommand("RNFR " + from);
		control.validateReply(reply, "350");

		reply = control.sendCommand("RNTO " + to);
		control.validateReply(reply, "250");
	}

	/**
	 *  Delete the specified remote working directory 
	 *
	 *  @param  dir  name of remote directory to
	 *               delete
	 */
	public void rmdir(String dir) throws IOException, FTPException
	{

		String reply = control.sendCommand("RMD " + dir);
		control.validateReply(reply, "250");
	}

	/**
	 *  Create the specified remote working directory 
	 *
	 *  @param  dir  name of remote directory to
	 *               create
	 */
	public void mkdir(String dir) throws IOException, FTPException
	{

		String reply = control.sendCommand("MKD " + dir);
		control.validateReply(reply, "257");
	}

	/**
	 *  Change the remote working directory to
	 *  that supplied
	 *
	 *  @param  dir  name of remote directory to
	 *               change to
	 */
	public void chdir(String dir) throws IOException, FTPException
	{

		String reply = control.sendCommand("CWD " + dir);
		control.validateReply(reply, "250");
	}

	/**
	 *  Get the current remote working directory 
	 *
	 *  @return   the current working directory
	 */
	public String pwd() throws IOException, FTPException
	{

		String reply = control.sendCommand("PWD");
		control.validateReply(reply, "257");
		return reply;
	}

	/**
	 *  Get the type of the OS at the server
	 *
	 *  @return   the type of server OS
	 */
	public String system() throws IOException, FTPException
	{

		String reply = control.sendCommand("SYST");
		control.validateReply(reply, "215");
		return reply;
	}

	/**
	 *  Quit the FTP session
	 *      
	 */
	public void quit() throws IOException, FTPException
	{

		String reply = control.sendCommand("QUIT" + FTPControlSocket.EOL);
		control.validateReply(reply, "221");

		control.logout();
		control = null;
	}

}
