/*
 * This file is part of the KFTPGrabber project
 *
 * Copyright (C) 2003-2006 by the KFTPGrabber developers
 * Copyright (C) 2003-2006 Jernej Kos <kostko@jweb-network.net>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU 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
 * is provided AS IS, WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, and
 * NON-INFRINGEMENT.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Steet, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations
 * including the two.
 *
 * You must obey the GNU General Public License in all respects
 * for all of the code used other than OpenSSL.  If you modify
 * file(s) with this exception, you may extend this exception to your
 * version of the file(s), but you are not obligated to do so.  If you
 * do not wish to do so, delete this exception statement from your
 * version.  If you delete this exception statement from all source
 * files in the program, then also delete it here.
 */
#ifndef KFTPENGINESOCKET_H
#define KFTPENGINESOCKET_H

#include <kurl.h>
#include <kremoteencoding.h>

#include <qptrlist.h>
#include <qguardedptr.h>
#include <qdatetime.h>

#include "commands.h"

namespace KFTPEngine {

class ConnectionRetry;

enum SocketFeatures {
  SF_FXP_TRANSFER = 1,
  SF_RAW_COMMAND  = 2
};

/**
 * The socket class provides an abstract class for all implemented protocols. It
 * provides basic methods and also some remote operations (recursive scan,
 * recursive removal).
 *
 * @author Jernej Kos <kostko@jweb-network.net>
 */
class Socket
{
friend class Thread;
friend class FtpCommandStat;
public:
    /**
     * Constructs a new socket.
     *
     * @param thread The thread that created this socket
     * @param protocol The protocol name
     */
    Socket(Thread *thread, const QString &protocol);
    ~Socket();
    
    /**
     * Set an internal config value.
     *
     * @param key Key
     * @param value Value
     */
    void setConfig(const QString &key, const QString &value) { m_config[key] = value; }
    
    /**
     * Set an internal config value.
     *
     * @param key Key
     * @param value Value
     */
    void setConfig(const QString &key, int value) { m_config[key] = QString::number(value); }
    
    /**
     * Set an internal config value.
     *
     * @param key Key
     * @param value Value
     */
    void setConfig(const QString &key, filesize_t value) { m_config[key] = QString::number(value); }
    
    /**
     * Get an internal config value as string.
     *
     * @param key Key
     * @return The key's value or an empty string if the key doesn't exist
     */
    QString getConfig(const QString &key) { return m_config[key]; }
    
    /**
     * Get an internal config value as an integer.
     *
     * @param key Key
     * @return The key's value or 0 if the key doesn't exist
     */
    int getConfigInt(const QString &key) { return m_config[key].toInt(); }
    
    /**
     * Get an internal config value as filesize.
     *
     * @param key Key
     * @return The key's value or 0 if the key doesn't exist
     */
    filesize_t getConfigFs(const QString &key) { return m_config[key].toULongLong(); }
    
    /**
     * This method should initialize the configuration map.
     */
    virtual void initConfig();
    
    /**
     * This method should trigger the connection process.
     *
     * @param url Remote url to connect to
     */
    virtual void protoConnect(const KURL &url) = 0;
    
    /**
     * This method should disconnect from the remote host.
     */
    virtual void protoDisconnect();
    
    /**
     * This method should abort any ongoing action.
     */
    virtual void protoAbort();
    
    /**
     * This method should download a remote file and save it localy.
     *
     * @param source The source url
     * @param destination The destination url
     */
    virtual void protoGet(const KURL &source, const KURL &destination) = 0;
    
    /**
     * This method should upload a local file and save it remotely.
     *
     * @param source The source url
     * @param destination The destination url
     */
    virtual void protoPut(const KURL &source, const KURL &destination) = 0;
    
    /**
     * Each protocol should implement this method. It should remove just one
     * single entry. A config variable "params.remove.directory" will be set
     * to 1 if the entry to remove is a directory and to 0 if it should expect
     * a file.
     *
     * @warning You should NOT use this method directly! Use @ref protoDelete
     *          instead!
     * @param path The path to the entry to remove
     */
    virtual void protoRemove(const KURL &path) = 0;
    
    /**
     * This method should rename/move a remote file.
     *
     * @param source The source file path
     * @param destination The destination file path
     */
    virtual void protoRename(const KURL &source, const KURL &destination) = 0;
    
    /**
     * This method should change file's mode.
     *
     * * @warning You should NOT use this method directly! Use @ref protoChmod
     *            instead!
     * @param path The file's path
     * @param mode The new file mode
     */
    virtual void protoChmodSingle(const KURL &path, int mode) = 0;
    
    /**
     * This method should create a new remote directory.
     *
     * @param path Path of the newly created remote directory
     */
    virtual void protoMkdir(const KURL &path) = 0;
    
    /**
     * This method should fetch the remote directory listing for a specified
     * directory. Note that this method could be called as a chained command,
     * so it MUST NOT emit an EventDirectoryListing event if isChained returns
     * true! In this case it should save the directory listing to the
     * m_lastDirectoryListing member variable.
     *
     * @param path The path to list
     */
    virtual void protoList(const KURL &path) = 0;
    
    /**
     * This method should fetch the information about the given path. It is
     * usualy called as a chained command.
     *
     * @param path The path to stat
     */
    virtual void protoStat(const KURL &path);
    
    /**
     * This method should send a raw command in case the protocol supports it
     * (the SF_RAW_COMMAND is among features).
     *
     * @param command The command to send
     */
    virtual void protoRaw(const QString&) {}
    
    /**
     * This method should initiate a site to site transfer in case the protocol
     * supports it (the SF_FXP_TRANSFER is among features).
     *
     * @param socket The destination socket
     * @param source The source url
     * @param destination The destination url
     */
    virtual void protoSiteToSite(Socket*, const KURL&, const KURL&) {}
    
    /**
     * Send a packet to keep the connection alive.
     */
    virtual void protoKeepAlive() {}
    
    /**
     * Recursively scan a directory and emit a DirectoryTree that can be used to
     * create new transfers for addition to the queue.
     *
     * @param path The path to recursively scan
     */
    void protoScan(const KURL &path);
    
    /**
     * Identify if the remote path is a file or a directory and recursively remove
     * it if so. The difference between this command and @ref protoRemove is, that
     * protoRemove removes just one entry, and doesn't identify file type.
     *
     * @param path The path to remove
     */
    void protoDelete(const KURL &path);
    
    /**
     * Change file or directory mode. Also supports recursive mode changes.
     *
     * @param path The file's path
     * @param mode The new file mode
     * @param recursive Should the mode be recursively changed
     */
    void protoChmod(const KURL &path, int mode, bool recursive);
    
    /**
     * Returns this socket's parent thread.
     *
     * @return Socket's parent thread
     */
    Thread *thread() { return m_thread; }
    
    /**
     * Returns the protocol name of this socket.
     *
     * @return This socket's protocol name
     */
    QString protocolName() { return m_protocol; }
    
    /**
     * This method should return the socket's features by or-ing the values in
     * SocketFeatures enum.
     *
     * @return Socket's features
     */
    virtual int features() = 0;
    
    /**
     * This method should return true if this socket is connected.
     *
     * @return True if the socket has successfully connected
     */
    virtual bool isConnected() = 0;
    
    /**
     * This method should return true if the connection is encrypted by some method.
     *
     * @return True if the connection is encrypted
     */
    virtual bool isEncrypted() = 0;
    
    /**
     * Returns true if the socket is currently busy performing an action.
     *
     * @return True if the socket is busy
     */
    virtual bool isBusy() { return m_currentCommand != Commands::CmdNone; }

    /**
     * Emit an engine error code.
     *
     * @param code The error code
     * @param param1 Optional string parameter
     */
    void emitError(ErrorCode code, const QString &param1 = 0);
    
    /**
     * Emit an engine event.
     *
     * @param type Event type
     * @param param1 Optional string parameter
     * @param param2 Optional string parameter
     */
    void emitEvent(Event::Type type, const QString &param1 = 0, const QString &param2 = 0);
    
    /**
     * Emit an engine event containing a directory listing.
     *
     * @param type Event type
     * @param param1 The DirectoryListing parameter
     */
    void emitEvent(Event::Type type, DirectoryListing param1);
    
    /**
     * Emit an engine event containing a filesize.
     *
     * @param type Event type
     * @param param1 The filesize parameter
     */
    void emitEvent(Event::Type type, filesize_t param1);
    
    /**
     * Emit an engine event containing a custom pointer.
     *
     * @param type Event type
     * @param param1 The custom pointer parameter
     */
    void emitEvent(Event::Type type, void *param1);
    
    /**
     * This method will set the socket's remote encoding which will be used when
     * converting filenames into UTF-8 and back.
     *
     * @param encoding A valid encoding name
     */
    virtual void changeEncoding(const QString &encoding);
    
    /**
     * Retrieve the KRemoteEncoding object for this socket set to the appropriate
     * encoding.
     *
     * @return The KRemoteEncoding object
     */
    KRemoteEncoding *remoteEncoding() { return m_remoteEncoding; }
    
    /**
     * Sets the current directory path.
     *
     * @param path The current directory path
     */
    void setCurrentDirectory(const QString &path) { m_currentDirectory = path; }
    
    /**
     * Get the current directory path.
     *
     * @return The current directory path.
     */
    virtual QString getCurrentDirectory() { return m_currentDirectory; }
    
    /**
     * Sets the default directory path (like a remote home directory).
     *
     * @param path The default directory path
     */
    void setDefaultDirectory(const QString &path) { m_defaultDirectory = path; }
    
    /**
     * Get the default directory path.
     *
     * @return The default directory path
     */
    virtual QString getDefaultDirectory() { return m_defaultDirectory; }
    
    /**
     * Sets the url this socket is connected to.
     *
     * @param url The url this socket is connected to
     */
    void setCurrentUrl(const KURL &url) { m_currentUrl = url; }
    
    /**
     * Get the url this socket is connected to.
     *
     * @return The url this socket is currently connected to
     */
    KURL getCurrentUrl() { return m_currentUrl; }
    
    /**
     * Sets the command the socket is currently executing.
     *
     * @param type Command type
     */
    void setCurrentCommand(Commands::Type type) { m_currentCommand = type; }
    
    /**
     * Get the current socket command.
     *
     * @return The current socket command
     */
    Commands::Type getCurrentCommand();
    
    /**
     * Get the toplevel socket command in the command chain.
     *
     * @return The toplevel socket command
     */
    Commands::Type getToplevelCommand();
    
    /**
     * Get the command that executed the current command. Note that this
     * is valid only if the current command is chained. Otherwise this
     * method returns Commands::CmdNone.
     *
     * @return The previous command
     */
    Commands::Type getPreviousCommand();
    
    /**
     * Get the last directory listing made by protoList.
     *
     * @return The last directory listing
     */
    DirectoryListing getLastDirectoryListing() { return m_lastDirectoryListing; }
    
    /**
     * Get the last stat response made by protoStat.
     *
     * @return The last stat response
     */
    DirectoryEntry getStatResponse() { return m_lastStatResponse; }
    
    /**
     * Get the number of bytes transfered from the beginning of the transfer.
     *
     * @return The number of bytes transfered
     */
    filesize_t getTransferBytes() { return m_transferBytes; }
    
    /**
     * Get the current transfer speed.
     *
     * @return The current transfer speed.
     */
    filesize_t getTransferSpeed();
    
    /**
     * This method will be called every cycle. It should be usually used to
     * poll the data transfer socket.
     */
    virtual void poll() = 0;
    
    /**
     * Wakeup the last command processor with a specific wakeup event. This
     * is used for async two-way communication between the engine and the
     * GUI (wakeup event is a reply from the GUI).
     *
     * By default this method just passes the event to the currently active
     * command processor.
     *
     * @param event The wakeup event that should be passed to the command class
     */
    virtual void wakeup(WakeupEvent *event);
    
    /**
     * Reset the current command class, possibly invoking the calling chained
     * command class or completing the operation.
     *
     * @param code The result code
     */
    virtual void resetCommandClass(ResetCode code = Ok);
    
    /**
     * Add a command class to the command chain so that it will be executed next.
     *
     * @param cmd The command class to add
     */
    void addToCommandChain(Commands::Base *cmd) { m_commandChain.append(cmd); }
    
    /**
     * Execute the next command.
     */
    void nextCommand();
    
    /**
     * Schedule the execution of the next command in the next thread loop.
     */
    void nextCommandAsync();
    
    /**
     * Returns true if the current command has been chained from another command class.
     *
     * @return True if the current command has been chained
     */
    bool isChained() { return m_commandChain.count() > 0; }
    
    /**
     * Set the error reporting on or off. This variable is then used by some
     * command classes do determine if they should emit errors and reset with
     * failure or if they should just silently ignore the error and reset
     * the command class with an Ok code.
     *
     * @param value Error reporting value
     */
    void setErrorReporting(bool value) { m_errorReporting = value; }
    
    /**
     * Get the current error reporting setting. This only makes sense if the
     * class is chained - otherwise this allways returns true.
     *
     * @return The current error reporting setting
     */
    bool errorReporting() { return m_errorReporting || !isChained(); }
    
    /**
     * Returns true if the current operation should abort. This method should be
     * used when the underlying socket implementation is doing blocking operations.
     *
     * @return True if the operation should be aborted
     */
    bool shouldAbort() { return m_shouldAbort; }
protected:
    /**
     * Call this method when a long wait period has started or ended. If the wait
     * isn't nulled before the timeout is reached the current action will be aborted
     * and the socket will be disconnected.
     *
     * @param start True if the wait period should start, false if it should end
     */
    void timeoutWait(bool start);
    
    /**
     * Reset the timeout counter. Call this once in a while during long wait periods
     * to notify the engine that the socket is still responsive.
     */
    void timeoutPing();
    
    /**
     * Check if we should timeout. This method might cause a disconnect if the timeout
     * value is reached.
     */
    void timeoutCheck();
    
    /**
     * Enable the issue of keepalive packets.
     */
    void keepaliveStart();
    
    /**
     * Check if we should transmit a new keepalive packet.
     */
    void keepaliveCheck();
protected:
    KRemoteEncoding *m_remoteEncoding;
    
    Commands::Base *m_cmdData;
    QPtrList<Commands::Base> m_commandChain;
    
    Thread *m_thread;
    DirectoryListing m_lastDirectoryListing;
    DirectoryEntry m_lastStatResponse;
    
    filesize_t m_transferBytes;
    time_t m_speedLastTime;
    filesize_t m_speedLastBytes;
    
    QTime m_timeoutCounter;
    QTime m_keepaliveCounter;
private:
    QMap<QString, QString> m_config;
    QString m_currentDirectory;
    QString m_defaultDirectory;
    KURL m_currentUrl;
    QString m_protocol;
    Commands::Type m_currentCommand;
    bool m_errorReporting;
    bool m_shouldAbort;
    QGuardedPtr<ConnectionRetry> m_connectionRetry;
};

}

#endif
