//----------------------------------------------------------------------------
//
// Module:      FTPModel.java      
//
// Description: FTP model object - take care of connection level actions
//
// FESI Copyright (c) Jean-Marc Lugrin, 2000
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//----------------------------------------------------------------------------

import java.awt.*;
import java.awt.event.*;
import java.io.*;

import com.oroinc.net.ftp.*;
import FESI.jslib.*;

/**
 * Model class (in the sense of the Model-View-Controller) for
 * FesiFTP. Subclasses by the controller class. Implements the
 * GUI independent commands, and is a logical proxy to the FTP 
 * and LocalContext classes.
 */
public class FTPModel {
    
    private String eol = System.getProperty("line.separator", "\n");

    // The current server, username and password
    private String server = null;
    private String username = null;
    private String password = null;
       
    // FTP context
    private FTPClient ftp =  new FTPClient();
    // Tracks the current transfer mode to minimize changes
    private int currentMode = FTP.ASCII_FILE_TYPE;  // default
    
    // Local file system context
    private LocalContext localContext;

    // Make false if detail directory cannot be accessed via FTP
    // Currently not modifiable dynamically.
    private boolean directoryByName = true; // false if directory cannot be parsed
    
    // Keeps track of last error and last reply from the server
    protected String lastError = null;
    protected String lastReply = null;
    
    // Exception mode - if true throws an exception instead of returning a status
    // in case of error
    private boolean exceptionMode = false;
    

    /**
     * Change the exception mode, returning the previous one.
     * <P>For the GUI.
     *
     * @param   mode the new mode 
     * @return the previous mode
     */
    public boolean changeExceptionMode(boolean mode) {
        boolean old = exceptionMode;
        exceptionMode = mode;
        return old;
    }
        
    /**
     * Set the exception mode
     *
     * @param   mode the new mode 
     */
    public void setExceptionMode(boolean mode) {
        exceptionMode = mode;
    }
    
    /**
     * get the exception mode
     *
     * @return the mode
     */
    protected boolean getExceptionMode() {
        return exceptionMode;
    }
    
    
    /**
     * Return connection state
     *
     * @return     
     */
    public boolean isConnected () {
        return ftp != null & ftp.isConnected();
    }
    

    /**
     * Return the last reply to EcmaScript
     *
     * @return  the last reply   
     */
    public String getLastReply() {
        return lastReply==null ? "" : lastReply;  // for EcmaScript
    }


    /**
     * Return the last error text to EcmaScript
     *
     * @return  the last error text   
     */
    public String getLastError() {
        return lastError;                           // null if none
    }
    

    /**
     * Return the server name of the last connection
     *
     * @return  The server name   
     */
    public String getServer() {
        return server;
    }

    /**
     * Return the username of the last connection
     *
     * @return  the username   
     */
    public String getUsername() {
        return username;
    }

    /**
     * Return the password of the last connection
     *
     * @return  the password   
     */
    public String getPassword() {
        return password;
    }        
        

    /**
     * Create a new FTPModel
     *
     */
    public FTPModel() {
        super();
                
        try {
            localContext = new LocalContext();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }

    }
    

    /**
     * Append the FTP reply string to the lastReply
     *
     * @param   message  
     */
   private void appendReplyStrings(String message) {
        if (lastReply == null) lastReply = new String();
        if (message!= null && message.length()>0) lastReply += message + eol;
        String[] replies = ftp.getReplyStrings();
        for (int i = 0; i<replies.length; i++) {
            lastReply += replies[i] + eol;
        }
    }

    

    // -----------------------------------------------------------------
    // Remote commands
    // -----------------------------------------------------------------
    

    /**
     * connect and login to FTP server
     *
     * @param   server  servername
     * @param   username  username
     * @param   password  password
     * @return  true if successful  
     */
    public boolean connect(String server, 
                           String username, 
                           String password) 
                      throws JSException {
        
        lastError = null;
        lastReply = null;
        
        if (isConnected()) {
            return returnErrorFalse("Already connect");
        } else {
            this.server = server.trim();
            this.username = username.trim();
            this.password = password.trim();
            // textArea.append("Connecting to '" + server + "'");
            try {
              int reply;
              ftp.connect(this.server);
              appendReplyStrings("Connection message:");
        
              // After connection attempt, you should check the reply code to verify
              // success.
              reply = ftp.getReplyCode();
        
              if(!FTPReply.isPositiveCompletion(reply)) {
                ftp.disconnect();
                return returnErrorFalse("FTP server refused connection:" + ftp.getReplyString()) ;
              }
              
              if (isConnected()) {
                   //  textArea.append("Login user '" + username + "'" + eol);
                    if(ftp.login(this.username, this.password)) {
                         //textArea.append("login OK: ");
                         appendReplyStrings("Login '" + this.username + "' ok to '" + this.server + "':");
                   } else {
                         //textArea.append("login Error: ");
                         String err = "Login error:" + ftp.getReplyString();
                         ftp.disconnect();
                         return returnErrorFalse(err);
                     }
                }


            } catch(IOException e) {
                if(ftp.isConnected()) {
                    try {
                      ftp.disconnect();
                    } catch(IOException ignored) {
                      // do nothing
                    }
                }
                return returnErrorFalse(e);
            } // catch
        }
        return true; // success        
    }
    

    /**
     * disconnect from the server
     *
     * @return true if successful    
     */
    public boolean disconnect() throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           //textArea.append(">>Disconnecting ... " + eol);
           if(ftp.isConnected()) {
                try {
                    ftp.quit();
                    appendReplyStrings("Disconnected:");
                    ftp.disconnect();
                } catch(IOException ignored) {
                    // do nothing
                }
           }
           //textArea.append(eol + "Disconnection done" + eol);
           return true;
       } else {
           return returnErrorFalse( "Not connected");
       }
    }
     

    /**
     * get remote working directory
     *
     * @return  the directory or null in case of error   
     */
    public String remoteGetWorkingDirectory() throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               String newDir = ftp.printWorkingDirectory();
               if (newDir == null) {
                   return returnErrorNull("Cannot get remote working directory: " + ftp.getReplyString());
               }
               return newDir;
           } catch (IOException e) {
               return returnErrorNull("Error when getting current remote directory: " + eol + e.toString() +eol);
           }
       } else {
           return returnErrorNull("Not connected");
       }
    }
   

    /**
     * change remote working directory
     *
     * @param   newDirectory  
     * @return  false in case of error  
     */
    public boolean remoteChangeWorkingDirectory(String newDirectory) throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.changeWorkingDirectory(newDirectory);
               if (success) {
                   return true;
               } else {
                   return returnErrorFalse("Remote change working directory to '" + newDirectory + 
                                               "', error: " + ftp.getReplyString());
               }
           } catch (IOException e) {
               return returnErrorFalse("Error changing remote working directory: " + eol + e.toString() +eol);
           }
       } else {
           return returnErrorFalse("Not connected");
       }
    }    
    

    /**
     * change remote directory to parent
     *
     * @return  the new directory or null in case of error   
     */
    public boolean remoteChangeToParentDirectory() throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.changeToParentDirectory();
               if (success) {
                   return true;
               } else {
                   return returnErrorFalse("Remote change remote to parent directory error: " + ftp.getReplyString());
               }
           } catch (IOException e) {
               return returnErrorFalse("Error changing to parent directory: " + eol + e.toString() + eol);
           }
       } else {
           return returnErrorFalse("Not connected");
       }
    }
    

    /**
     * get the remote directory listing
     *
     * @return  an array of FTPFile or null in case of error   
     */
    public FTPFile[] remoteGetFileList() throws JSException {
        lastError = null;
        lastReply = null;
        FTPFile[] ftpFiles = null;
        if (isConnected()) {
           try {
              if (directoryByName) {
                  ftpFiles = ftp.listFiles();
                  if (ftpFiles == null) {
                      ftpFiles = new FTPFile[0]; // no file
                  }
              } else {
                  String [] names = ftp.listNames();
                  if (names == null) {
                      ftpFiles = new FTPFile[0]; // no file
                  } else {
                      ftpFiles = new FTPFile[names.length];
                      for (int i = 0; i<names.length; i++) {
                          FTPFile fileInfo = new FTPFile();
                          fileInfo.setName(names[i]);
                          fileInfo.setRawListing(names[i]);
                          ftpFiles[i] = fileInfo;
                      }
                      //textArea.append("  " + names.length + " file(s)" + eol);
                  }
              }
           } catch(IOException e) {
                   lastError = "Error listing files:" + eol + e.toString() + eol;
           }
       } else {
           lastError = "Not connected";
       }
       if (ftpFiles == null) checkExceptionMode();
       return ftpFiles;
    }


    /**
     * Make a remote directory
     *
     * @param   newDirectory  the directory to create
     * @return  true if successful 
     */
    public boolean remoteMakeDirectory(String newDirectory) throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.makeDirectory(newDirectory);
               if (success) {
                   return true;
               } else {
                   lastError = "Make remote directory '" + newDirectory + "' rejected: " +
                                   ftp.getReplyString();
               }
           } catch (IOException e) {
               lastError = "Error making remote directory: " + eol + e.toString() +eol;
           }
       } else {
           lastError = "Not connected";
       }
       return returnErrorFalse();
    }    


    /**
     * rename a remote file
     *
     * @param   srcFileName  the original file name
     * @param   dstFileName  the remote file name
     * @return  true if successful   
     */
    public boolean remoteRenameFile(String srcFileName, String dstFileName) throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.rename(srcFileName, dstFileName);
               if (success) {
                   return true;
               } else {
                   lastError = "Rename remote file '" + srcFileName + 
                                   "' to '" + dstFileName + "' rejected: " +
                                   ftp.getReplyString();
               }
           } catch (IOException e) {
               lastError = "Error renaming remote file: " + eol + e.toString() +eol;
           }
       } else {
           lastError = "Not connected";
       }
       return returnErrorFalse();
    }    


    /**
     * delete a remote file
     *
     * @param   fileName  the file to delete
     * @return  true in case of success
     */
    public boolean remoteDeleteFile(String fileName) throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.deleteFile(fileName);
               if (success) {
                   return true;
               } else {
                   lastError = "Delete remote file '" + fileName + "' rejected: " +
                                   ftp.getReplyString();
               }
           } catch (IOException e) {
               lastError = "Error deleting remote file: " + eol + e.toString() +eol;
           }
       } else {
           lastError = "Not connected";
       }
       return returnErrorFalse();
    }    


    /**
     * remove a remote directory
     *
     * @param   directoryName the directory to remove 
     * @return   true in case of success  
     */
    public boolean remoteRemoveDirectory(String directoryName) throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
           try {
               boolean success = ftp.removeDirectory(directoryName);
               if (success) {
                   return true;
               } else {
                   lastError = "Remove remote directory '" + directoryName + "' rejected: " +
                                   ftp.getReplyString();
               }
           } catch (IOException e) {
               lastError = "Error removing remote directory: " + eol + e.toString() +eol;
           }
       } else {
           lastError = "Not connected";
       }
       return returnErrorFalse();
    }    


    // -----------------------------------------------------------------
    // Local commands
    // -----------------------------------------------------------------
    
    /**
     * Get local directory
     *
     * @return the directory or null in case of error    
     */
    public String localGetWorkingDirectory() throws JSException {
        lastError = null;
        lastReply = null;
        return localContext.getCurrentWorkingDirectory();
    }

    
   

    /**
     * Change local direcotry
     *
     * @param   newDirectory the directory to change to 
     * @return  the new directory or null in case of error 
     */
    public String localChangeWorkingDirectory(String newDirectory) throws JSException {
        lastError = null;
        lastReply = null;
        try {
           boolean success = localContext.changeWorkingDirectory(newDirectory);
           if (success) {
               return localContext.getCurrentWorkingDirectory();
           } else {
               return returnErrorNull("Change to local directory '" + newDirectory + "' rejected");
           }
       } catch (IOException e) {
           return returnErrorNull("Error chaning local woring directory: " + eol + e.toString() + eol);
       }
    }    
    

    /**
     * Change local directory to parent
     *
     * @return  the new directory or null in case of error   
     */
    public String localChangeToParentDirectory() throws JSException {
        lastError = null;
        lastReply = null;
        try {
           boolean success = localContext.changeToParentDirectory();
           if (success) {
               return localContext.getCurrentWorkingDirectory();
           } else {
               return returnErrorNull("Change to local parent directory rejected");
           }
        } catch (IOException e) {
           return returnErrorNull("Error changing to local parent directory: " + eol + e.toString() +eol);
        }
    }


    /**
     * get the local directory listing
     *
     * @return  an array of FTPfiles or null in case of error   
     */
    public FTPFile[] localGetFileList() throws JSException {
        lastError = null;
        lastReply = null;
        FTPFile[] ftpFiles = null;
        try {
           ftpFiles = localContext.listFiles();
           if (ftpFiles == null) {
              ftpFiles = new FTPFile[0]; // no files
           }
       } catch(IOException e) {
           lastError = "Error listing local files: " + eol + e.toString() + eol;
       }
       if (ftpFiles == null) checkExceptionMode();
       return ftpFiles;
    }


    /**
     * make a local directoy
     *
     * @param   newDirectory  the directory to create
     * @return   true if successful  
     */
    public boolean localMakeDirectory(String newDirectory) throws JSException {
        lastError = null;
        lastReply = null;
       try {
           boolean success = localContext.makeDirectory(newDirectory);
           if (success) {
               return true;
           } else {
               lastError = "Make local directory '" + newDirectory + "' rejected";
           }
       } catch (IOException e) {
           lastError = "Error making local directory: " + eol + e.toString() +eol;
       }
       return returnErrorFalse();
    }    


    /**
     * rename a local file
     *
     * @param   srcFileName  the file to rename
     * @param   dstFileName  the new name
     * @return  true if successsful  
     */
    public boolean localRenameFile(String srcFileName, String dstFileName) throws JSException {
       lastError = null;
       lastReply = null;
       try {
           boolean success = localContext.renameFile(srcFileName, dstFileName);
           if (success) {
               return true;
           } else {
               lastError = "Rename local file '" + srcFileName + "' to ' " + 
                               dstFileName + "' rejected";
           }
       } catch (IOException e) {
           lastError = "Error renaming local file: " + eol + e.toString() +eol;
       }
       return returnErrorFalse();
    }    


    /**
     * delete a local file
     *
     * @param   fileName the file to delete 
     * @return  true if successful   
     */
    public boolean localDeleteFile(String fileName) throws JSException {
       lastError = null;
       lastReply = null;
       try {
           boolean success = localContext.deleteFile(fileName);
           if (success) {
               return true;
           } else {
               lastError = "Delete local file '" + fileName + "' rejected";
           }
       } catch (IOException e) {
           lastError = "Error deleting local file: " + eol + e.toString() +eol;
       }
       return returnErrorFalse();
    }    



    /**
     * remove local directory
     *
     * @param   directoryName  the directory to remvoe
     * @return  true if successful   
     */
    public boolean localRemoveDirectory(String directoryName) throws JSException {
       lastError = null;
       lastReply = null;
       try {
           boolean success = localContext.removeDirectory(directoryName);
           if (success) {
               return true;
           } else {
               lastError = "Remove local directory '" + directoryName + "' rejected";
           }
       } catch (IOException e) {
           lastError = "Removing deleting local directory: " + eol + e.toString() +eol;
       }
       return returnErrorFalse();
    }    


    // -----------------------------------------------------------------
    // Transfer commands
    // -----------------------------------------------------------------

    /**
     * Get a text file
     *
     * @param   fileName  the file to get
     * @return  true if successful   
     */
    public boolean getTextFile(String fileName) throws JSException {
         return getFile(FTP.ASCII_FILE_TYPE, fileName, fileName);
    }

    /**
     * Get a text file
     *
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    public boolean getTextFile(String localFileName, 
                               String remoteFileName) 
                        throws JSException {
         return getFile(FTP.ASCII_FILE_TYPE, localFileName, remoteFileName);
    }
    

    /**
     * Get a binary file
     *
     * @param   fileName  the file to get
     * @return  true if successful   
     */
    public boolean getBinaryFile(String fileName) throws JSException {
         return getFile(FTP.BINARY_FILE_TYPE, fileName, fileName);
    }

    /**
     * Get a binary file
     *
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    public boolean getBinaryFile(String localFileName, 
                                 String remoteFileName) 
                           throws JSException {
         return getFile(FTP.BINARY_FILE_TYPE, localFileName, remoteFileName);
    }
     

    /**
     * Get a file
     *
     * @param   mode  FTP.ASCII_FILE_TYPE or FTP.BINARY_FILE_TYPE
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    protected boolean getFile(int mode, 
                              String localFileName, 
                              String remoteFileName) 
                         throws JSException {
        if (isConnected()) {
            if (!checkMode(mode)) {
               return returnErrorFalse();
            }
            try {
                OutputStream output = new FileOutputStream(
                            new File(localContext.getCurrentWorkingDirectory(), localFileName));
                boolean status = ftp.retrieveFile(remoteFileName, output);
                output.close();  
                if (!status) {
                    // Should delete the created output file !
                    return returnErrorFalse("Cannot get file: " + ftp.getReplyString());
                }
            } catch (IOException e) {
                lastError = "Error geting file: " + eol + e.toString() + eol;
            }
        } else {
           return returnErrorFalse("Not connected");
        }
        return true;
    }
    
    
    

    /**
     * Send a text file
     *
     * @param   fileName  file to send
     * @return  true if successful   
     */
    public boolean sendTextFile(String fileName) throws JSException {
         return sendFile(FTP.ASCII_FILE_TYPE, fileName, fileName);
    }

    /**
     * Send a text file
     *
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    public boolean sendTextFile(String localFileName, 
                                String remoteFileName) 
                            throws JSException {
         return sendFile(FTP.ASCII_FILE_TYPE, localFileName, remoteFileName);
    }
    

    /**
     * Send a binary file
     *
     * @param   fileName  file to send
     * @return  true if successful
     */
    public boolean sendBinaryFile(String fileName) throws JSException {
         return sendFile(FTP.BINARY_FILE_TYPE, fileName, fileName);
    }

    /**
     * Send a binary file
     *
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    public boolean sendBinaryFile(String localFileName, 
                                  String remoteFileName)
                           throws JSException {
         return sendFile(FTP.BINARY_FILE_TYPE, localFileName, remoteFileName);
    }
    

    /**
     * Send a file
     *
     * @param   mode  FTP.ASCII_FILE_TYPE or FTP.BINARY_FILE_TYPE
     * @param   localFileName  local file name
     * @param   remoteFileName  remote file name
     * @return  true if successful   
     */
    protected boolean sendFile(int mode, 
                               String localFileName, 
                               String remoteFileName)
                         throws JSException {
        lastError = null;
        lastReply = null;
        if (isConnected()) {
            if (!checkMode(mode)) {
                return returnErrorFalse();
            }
           
            try {
                File localFile = 
                            new File(localContext.getCurrentWorkingDirectory(), localFileName);
                if (!localFile.exists()) {
                    return returnErrorFalse("Local file '" + localFileName + "' does not exist");
                }
                if (!localFile.isFile()) {
                    return returnErrorFalse("Local file " + localFileName + " is not a data file");
                }
                if (!localFile.canRead()) {
                    return returnErrorFalse("Local file '" + localFileName + "' cannot be read");
                }
                InputStream input = new FileInputStream(localFile);
                boolean status = ftp.storeFile(remoteFileName, input);
                input.close();
                if (!status) {
                    return returnErrorFalse("Cannot get file: " + ftp.getReplyString());
                }
   
            } catch (IOException e) {
                lastError = "Error storing file: " + eol + e.toString() + eol;
            }
           
        } else {
           returnErrorFalse("Not connected");
        }
        return true;
    }
    



    // -----------------------------------------------------------------
    // Utility routines
    // -----------------------------------------------------------------
    

    /**
     * Throw an exception if the exceptionMode is true
     *
     * @exception   JSException  Thrown if exceptionMode is true
     */
    protected void checkExceptionMode() throws JSException {
        if (exceptionMode) throw new JSException(lastError);
    }
    

    /**
     * Return false or throw an exception depending on exception mode.
     * lastError must have been loaded with the error text.
     *
     * @return  false 
     * @exception   JSException  Thrown if exceptionMode is true
     */
    protected boolean returnErrorFalse() throws JSException {
        checkExceptionMode();
        return false;
    } 
       

    /**
     * Return false or throw an exception depending on exception mode
     * Set last error to the error string
     *
     * @param   error  The error string
     * @return     
     * @exception   JSException  Thrown if exceptionMode is true
     */
    protected boolean returnErrorFalse(String error) throws JSException {
        lastError = error;
        checkExceptionMode();
        return false;
    } 
       

    /**
     * Return null or throw an exception depending on exception mode.
     * Set last error to the error string
     *
     * @param   error  The error string
     * @return  null   
     * @exception   JSException  Thrown if exceptionMode is true
     */
    protected String returnErrorNull(String error) throws JSException {
        lastError = error;
        checkExceptionMode();
        return null;
    }    


    /**
     * Return false or throw an exception depending on exception mode
     * Set lastError to a text describing the exception
     *
     * @param   e  The original exception
     * @return  false   
     * @exception   JSException  Thrown if exceptionMode is true
     */
    protected boolean returnErrorFalse(Exception e) throws JSException {
        lastError = e.toString();
        checkExceptionMode();
        return false;
    }    


    /**
     * Ensure that the transfer mode (binary or ascii) is as expected
     * 
     * @param   mode  FTP.ASCII_FILE_TYPE or FTP.BINARY_FILE_TYPE
     * @return  true if successful  
     */
    private boolean checkMode(int mode) {
        // Assumes connected
        if (currentMode != mode) {
            try {
                boolean status = ftp.setFileType(mode);
                if (status) {
                    currentMode = mode;
                } else {
                    return false;
                }
            } catch (IOException e) {
                lastError = "Cannot change mode, error: " + eol + e + eol;
                return false;
            }
        }
        return true;
    }
    
}