/* The original version of this program can be found at http://damb.dk */
#include <iostream>
#include <windows.h>
#include <wininet.h>
#include <string>
#include <sstream>
#include <fstream>

// --- This section could be put into some header file.

// A handy helper function
template <typename T>
std::string ToString(T aValue)
{
   std::stringstream ss;
   ss << aValue;
   return ss.str();
}

// Any error is reported by throw'ing a FtpExceptionClass exception
class FtpExceptionClass
{
public:
   FtpExceptionClass(const char *aExc);
   const std::string &What() const { return Message; }
private:
   std::string Message;
};

class FtpInternetConnectionClass;

// This class is a file object, it's created with OpenFile in the connection
class FtpFileClass
{
public:
   ~FtpFileClass();
   void Write(const char *aStr);
   bool Read(std::string &aStr); // Read a string until '\n'
private:
   FtpFileClass(); // Not to be implemented
   friend class FtpConnectionClass;
   // Only FtpConnectionClass can create me
   FtpFileClass(FtpConnectionClass *aConnection, const char *aFileName, DWORD aAccess, DWORD aFlags);
   HINTERNET HFile;
   FtpConnectionClass *Connection;
};

// A directory listing class, created by calling CreateDirList in the connection
class FtpDirListClass
{
public:
   ~FtpDirListClass();
   std::string GetFileName() { return FindData.cFileName; }
   bool Next() { return InternetFindNextFile(Handle, &FindData); }
private:
   friend class FtpConnectionClass;
   FtpDirListClass(); // Not to be implemented
   // Only FtpConnectionClass can create me
   FtpDirListClass(FtpConnectionClass *aConnection, const char *aPath);
   HINTERNET Handle;
   WIN32_FIND_DATA FindData;
   FtpConnectionClass *Connection;
};

// The Connection create one to have a ftp connection.
class FtpConnectionClass
{
public:
   FtpConnectionClass() : HConnection(0), Busy(false)
   {}
   // Call Connect to open a connection.
   bool Connect(const char *aServerName, const char *aUserName = 0, const char *aPassWord = 0);

   enum FileAccessType
   {
      FileAccessRead = 1,
      FileAccessWrite = 2
   };
   enum FileModeType
   {
      FileModeBinary = 4,
      FileModeAcsii  = 8
   };
   // Note: Only one operation can be pending at a time, due to limitations in the ftp protocol,
   // if you need more than one operation at a time, create another connection.

   // Call OpenFile to open a file
   FtpFileClass *OpenFile(const char *aFileName, FileAccessType aFileAccess, FileModeType aFileMode);

   // Call CreateDirList to be able to get a dir list.
   FtpDirListClass *CreateDirList(const char *aFile);

   // Get the current directory on the ftp server
   std::string GetCurrentDirectory();
   // Change dir on the ftp server
   void SetCurrentDirectory(const char *aDir);

   // Get a file from the ftp server
   void GetFile(const char *aFileName);

   // Transfer a file to the ftp server
   void PutFile(const char *aFileName);
private:
   HINTERNET GetHandle() { return HConnection; }
   friend class FtpFileClass;
   friend class FtpDirListClass;
   void Done() { Busy = false; }
   HINTERNET HConnection;
   bool Busy;
   static HINTERNET HInternet;
};

// --- A test application
int main()
{
   try
   {
      FtpConnectionClass FtpConnection; // The Connection
      FtpConnection.Connect("ftp.microsoft.com", 0, 0); // And connect
      // List all files on the server
      FtpDirListClass *DirList = FtpConnection.CreateDirList("*");
      do
      {
         std::cout << DirList->GetFileName() << std::endl;
      }
      while(DirList->Next());
      delete DirList;

      // Create a file to write
      FtpFileClass *File = FtpConnection.OpenFile("abc.txt", FtpConnectionClass::FileAccessWrite, FtpConnectionClass::FileModeBinary);
      // Write a string to it
      File->Write("Hello Worldrn");
      // Close the file again
      delete File;
      // Now create a file to read
      File = FtpConnection.OpenFile("abc.txt", FtpConnectionClass::FileAccessRead, FtpConnectionClass::FileModeBinary);
      std::string S;
      File->Read(S);
      std::cout << "Read: " << S << std::endl;
      delete File;
      // Get the file we just wrote
      FtpConnection.GetFile("abc.txt");
      // Create a local file
      std::ofstream of("abc2.txt");
      of << "Some text" << std::endl;
      of.close();
      // And upload it to the ftp server
      FtpConnection.PutFile("abc2.txt");

      // Get and set directory
      std::cout << "Current Dir: " << FtpConnection.GetCurrentDirectory() << std::endl;
      FtpConnection.SetCurrentDirectory("new");
      std::cout << "Current Dir: " << FtpConnection.GetCurrentDirectory() << std::endl;
   }

   catch (FtpExceptionClass &FtpException)
   {  // In case something went wrong
      std::cerr << FtpException.What() << std::endl;
   }
}

// --- The rest is implementation of the ftp class'es, should be put into some .cpp file
HINTERNET FtpConnectionClass::HInternet;

FtpFileClass::FtpFileClass(FtpConnectionClass *aConnection, const char *aFileName, DWORD aAccess, DWORD aFlags)
{
   Connection = aConnection;
   HFile = FtpOpenFile(Connection->GetHandle(), aFileName, aAccess, aFlags, 0);
   if(!HFile)
   {
      throw FtpExceptionClass("Failed to open file");
   }
}

FtpFileClass::~FtpFileClass()
{
   InternetCloseHandle(HFile);
   Connection->Done();
}

FtpDirListClass::~FtpDirListClass()
{
   InternetCloseHandle(Handle);
   Connection->Done();
}

FtpDirListClass::FtpDirListClass(FtpConnectionClass *aConnection, const char *aPath)
{
   Connection = aConnection;
   Handle = FtpFindFirstFile(Connection->GetHandle(), aPath, &FindData, INTERNET_FLAG_RELOAD, 0);
   if(!Handle)
   {
      throw FtpExceptionClass("Failed to start dir list");
   }
}

void FtpFileClass::Write(const char *aStr)
{
   DWORD Dummy;
   if(!InternetWriteFile(HFile, aStr, strlen(aStr), &Dummy))
   {
      throw FtpExceptionClass("Failed to write");
   }
}

bool FtpFileClass::Read(std::string &aStr)
{
   aStr.clear();
   char Data[2];
   Data[1] = 0;
   DWORD Dummy;
   while(InternetReadFile(HFile, Data, 1, &Dummy) && Dummy)
   {
      aStr += Data;
      if(Data[0] == '\n')
         return true;
   }
   return false;
}

FtpExceptionClass::FtpExceptionClass(const char *aExc)
{
   char *MsgBuf = 0;
   FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER,
                 0,
                 GetLastError(),
                 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                 (char *)&MsgBuf, 0, 0);
   Message = "Ftp exception: ";
   Message += aExc;
   Message += "n";
   if(MsgBuf)
   {
      Message += MsgBuf;
      LocalFree(MsgBuf);
   }
   else
   {
      Message += "Unknown Error: ";
      Message += ToString(GetLastError());
   }
}

bool FtpConnectionClass::Connect(const char *aServerName, const char *aUserName, const char *aPassWord)
{
   if(!HInternet)
   {
      HInternet = InternetOpen("MyApp", INTERNET_OPEN_TYPE_PROXY, 0, 0, 0);
      if(!HInternet)
      {
         throw(FtpExceptionClass("Unable to open internet"));
      }
   }
   if(HConnection)
   {
      InternetCloseHandle(HConnection);
   }
   HConnection = InternetConnect(HInternet, aServerName, INTERNET_DEFAULT_FTP_PORT, aUserName, aPassWord, INTERNET_SERVICE_FTP, 0, 1);
   if(!HConnection)
   {
      throw FtpExceptionClass("Failed to open Connection");
   }
   return true;
}

FtpFileClass *FtpConnectionClass:: OpenFile(const char *aFileName, FileAccessType aFileAccess, FileModeType aFileMode)
{
   if(!HConnection)
   {
      throw FtpExceptionClass("Connection not opened");
   }
   if(Busy)
   {
      throw FtpExceptionClass("Only one action at a time");
   }
   FtpFileClass *FtpFile = new FtpFileClass(this,
                                            aFileName,
                                            aFileAccess == FileAccessRead ? GENERIC_READ : GENERIC_WRITE,
                                            aFileMode == FileModeBinary ? FTP_TRANSFER_TYPE_BINARY  : FTP_TRANSFER_TYPE_ASCII);
   Busy = true;
   return FtpFile;
}

FtpDirListClass *FtpConnectionClass::CreateDirList(const char *aFile)
{
   if(!HConnection)
   {
      throw FtpExceptionClass("Connection not opened");
   }
   if(Busy)
   {
      throw FtpExceptionClass("Only one action at a time");
   }
   FtpDirListClass *FtpDirList = new FtpDirListClass(this, aFile);
   Busy = true;
   return FtpDirList;
}

std::string FtpConnectionClass::GetCurrentDirectory()
{
   std::string Path;
   Path.resize(MAX_PATH + 1);
   DWORD Size = MAX_PATH;
   if(!FtpGetCurrentDirectory(HConnection, (char *)Path.c_str(), &Size))
   {
      throw FtpExceptionClass("Failed to get current dir");
   }
   return Path;
}

void FtpConnectionClass::SetCurrentDirectory(const char *aDir)
{
   if(!FtpSetCurrentDirectory(HConnection, aDir))
   {
      throw FtpExceptionClass("Failed to set current dir");
   }
}

void FtpConnectionClass::GetFile(const char *aFileName)
{
   if(!FtpGetFile(HConnection, aFileName, aFileName, false, FILE_ATTRIBUTE_NORMAL, FTP_TRANSFER_TYPE_BINARY, 0))
   {
      throw FtpExceptionClass("Failed to get file");
   }
}

void FtpConnectionClass::PutFile(const char *aFileName)
{
   if(!FtpPutFile(HConnection, aFileName, aFileName, FTP_TRANSFER_TYPE_BINARY, 0))
   {
      throw FtpExceptionClass("Failed to put file");
   }

}

// --- EOF