//
// Utility.cc
//
// Utility functions for BuildDistribution.
//
// (C) Copyright 2010 ARM Research Labs, LLC See
// www.armresearch.com for the copyright terms.
//
// $Id$
//

#include <windows.h>
#include <tchar.h>

#include <string>
#include <iostream>
#include <stdexcept>
#include <algorithm>
#include <sstream>
#include <iterator>

#include "Utility.h"

static DWORD LastError = 0;                                                     // Last value from GetLastError().  Needed because
                                                                                // GetLastError() doesn't return the correct value
                                                                                // after an exception is thrown.

void
OutputUsage(std::string DefaultTempPath) {
    std::cout << 
      "\n"
      "Usage:\n"
      "\n"
      "  BuildDistribution -d DistPath -t TempPath\n"
      "\n"
      "  where DistPath is the path of the distribution\n"
      "  (e.g. SNFMultiSDK_Windows_3.1, and TempPath is the name of the\n"
      "  directory to use for temporary storage.  TempPath must exist.\n"
      "  The default value of TempPath is '" << DefaultTempPath << "'.\n"
      ;
}

void
OutputHelp() {
    std::cout <<
      "\n"
      "The program does the following:\n"
      "\n"
      " 1) For each directory DIR that appears in DistPath and also in\n"
      "    DistPath\\..:\n"
      "\n"
      "    a) Delete the contents of DistPath/DIR, except for .svn.\n"
      "\n"
      "    b) Copy the contents of DistPath\\..\\DIR to DistPath\\DIR, except\n"
      "       for .svn.\n"
      "\n"
      " 2) Create a temporary directory TempPath/DistName, where DistName is\n"
      "    the directory name of DistPath (e.g. if DistName is\n"
      "    c:\\dir1\\dir2\\SNFMultiSDK_Windows_3.1, then DistName is\n"
      "    SNFMultiSDK_Windows_3.1).\n"
      "\n"
      " 3) For each directory DIR that appears in DistPath and also in\n"
      "    DistPath\\..:\n"
      "\n"
      "    a) Copy all files from DistPath/DIR to TempPath/DistName, except\n"
      "       for .svn.\n\n"
      ;

}

void
OutputErrorMessage(std::string Message) {
    LPVOID lpMsgBuf;

    if (LastError != 0) {

        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
                      FORMAT_MESSAGE_FROM_SYSTEM |
                      FORMAT_MESSAGE_IGNORE_INSERTS,
                      NULL,
                      LastError,
                      MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                      (LPTSTR) &lpMsgBuf,
                      0,
                      NULL);
        std::cerr << Message << ":  " << (LPCSTR) lpMsgBuf;

    } else {

        std::cerr << Message << "\n";

    }

}

/// Case-insensitive search for a specified string in a container of strings.
//
// \param[in] Needle is the string to search for.
//
// \param[in] Haystack is the container of strings to search.
//
// \returns true if Needle is in Haystack (case-insensitive
// comparison), false otherwise.
//
bool IsInContainer(std::string Needle, StringContainer Haystack);

/// Get a list of directories in a specified path.
//
// This function returns a list of directories in the specified path,
// excluding a specified list of names.
//
// \param[in] DistPath is the path to search.
//
// \param[in] ExcludeName contains the names to exclude.
//
// \returns a list of directory names in the specified path, excluding
// names in ExcludeNames.
//
StringContainer GetDirectoryList(std::string DistPath,
				 StringContainer ExcludeName);

/// Get a list of files in a specified path.
//
// This function returns a list of files in the specified path,
// excluding a specified list of names.
//
// \param[in] DistPath is the path to search.
//
// \param[in] ExcludeName contains the names to exclude.
//
// \returns a list of file names in the specified path,
// excluding names in ExcludeNames.
//
StringContainer GetFileList(std::string DistPath,
			    StringContainer ExcludeName);

/// Update the files in one directory.
//
// \param[in] SourceDir is the name of the directory that contains the
// files to copy from.
//
// \param[in] DestDir is the name of the directory to clear and copy
// files to.
//
// \param[in] ExcludeName contains the names of the files to not
// delete or copy.
//
// This function:
//
//  1) Deletes the files in DestDir, except for ExcludeName and directories.
//
//  2) If a directory in exists in both SourceDir and DestDir, then
//  update that directory by invoking UpdateCommonDirectories().
//
//  3) If a directory exists in DestDir but not in SourceDir, delete
//  that directory and all it's contents.
//
//  4) Copy the non-directory files SourceDir to DestDir, except for
//  ExcludeName.
//
void UpdateOneDirectory(std::string SourceDir,
			std::string DestDir,
			StringContainer ExcludeName);

/// Delete a directory and all it's contents.
//
// This function recursively deletes the contents of a directory, and
// then deletes the directory.
//
// \param[in] Path is the path to delete.
//
void DeletePath(std::string Path);

void
GetDirectoryNames(std::string &InputDistPath,
		  std::string &DistSourcePath,
		  std::string &DistParentPath,
		  std::string &DistName) {

    DWORD RetVal = 0;
    TCHAR PathPart[MAX_PATH];
    LPTSTR FilePart = NULL;

    RetVal = GetFullPathName(InputDistPath.c_str(),                             // Get the path name for the source.
                             MAX_PATH,
                             PathPart,
                             &FilePart);
    LastError = GetLastError();
    if (0 == RetVal) {

        throw(std::invalid_argument("Error determining directory names"));

    }
    DistSourcePath = PathPart;

    if ( (NULL != FilePart) && (0 != *FilePart) ) {                             // Extract the distribution name
        // (last component of the source path.


        DistName = FilePart;

    } else {

        throw(std::invalid_argument("Unable to determine distribution name"));

    }

    std::string::size_type DistNameIndex;                                       // Get the parent directory of the
                                                                                // source directory.


    DistNameIndex = DistSourcePath.rfind("\\");
    if (std::string::npos == DistNameIndex) {

        throw(std::invalid_argument("Unable to determine the name of the "
                                    "distribution parent directory"));

    }

    DistParentPath = DistSourcePath.substr(0, DistNameIndex);

}

bool
IsInContainer(std::string Needle, StringContainer Haystack) {

    std::transform(Needle.begin(), Needle.end(), Needle.begin(), ::toupper);

    StringContainer::iterator iFile;

    for (iFile = Haystack.begin(); iFile != Haystack.end(); iFile++) {          // Check each file in the container.

        std::transform(iFile->begin(), iFile->end(), iFile->begin(), ::toupper);
        if (*iFile == Needle) {

            return true;

        }
    }

    return false;

}


StringContainer
GetDirectoryList(std::string DistPath, StringContainer ExcludeName) {

    DistPath += "\\*.*";                                                        // Create filter string.

    WIN32_FIND_DATA FileData;
    HANDLE ListHandle;
    std::string FileName;

    ListHandle = FindFirstFile(DistPath.c_str(),
                               &FileData);

    StringContainer DirName;                                                    // Container to hold directory
                                                                                // names of the distribution.

    if ( (FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&              // Is this a directory...

         (! IsInContainer(FileData.cFileName, ExcludeName)) ) {                 // ...and not an excluded file?

        DirName.push_back(FileData.cFileName);                                  // Yes.  Save the name.

    }

    while (true) {

        if (FindNextFile(ListHandle, &FileData)) {

            if ( (FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&      // Is this a directory...

                 (! IsInContainer(FileData.cFileName, ExcludeName)) ) {         // ...and not an excluded file?

                DirName.push_back(FileData.cFileName);                          // Yes.  Save the name.

            }

        } else {

            LastError = GetLastError();
            if (ERROR_NO_MORE_FILES == LastError) {

                break;

            } else {
                std::ostringstream temp;

                temp << "Error determining directory list of '"
                     << DistPath << "'";
                throw(std::runtime_error(temp.str()));

            }
        }
    }

    if (FindClose(ListHandle) == 0) {
        std::ostringstream Temp;

        LastError = GetLastError();
        Temp << "***Error closing handle for " << DistPath;
        throw(std::runtime_error(Temp.str()));

    }

    return DirName;
}

StringContainer
GetFileList(std::string DirPath, StringContainer ExcludeName) {

    std::string DirPathFilter;

    DirPathFilter = DirPath + "\\*.*";                                          // Create filter string.

    WIN32_FIND_DATA FileData;
    HANDLE ListHandle;

    ListHandle = FindFirstFile(DirPathFilter.c_str(),
                               &FileData);

    StringContainer FileNameList;                                               // Container to hold file names.
    std::string FileName;

    if ( !(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&             // Is this not a directory...

         (! IsInContainer(FileData.cFileName, ExcludeName)) ) {                 // ...and not an excluded file?

        FileNameList.push_back(FileData.cFileName);                             // Yes.  Save the name.

    }

    while (true) {

        if (FindNextFile(ListHandle, &FileData)) {

            if ( !(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&     // Is this not a directory...

                 (! IsInContainer(FileData.cFileName, ExcludeName)) ) {         // ...and not an excluded file?

                FileNameList.push_back(FileData.cFileName);                     // Yes.  Save the name.

            }

        } else {

            LastError = GetLastError();
            if (ERROR_NO_MORE_FILES == LastError) {

                break;

            } else {
                std::ostringstream temp;

                temp << "Error determining directory list of '"
                     << DirPath << "'";
                throw(std::runtime_error(temp.str()));

            }
        }
    }

    if (FindClose(ListHandle) == 0) {
        std::ostringstream Temp;

        LastError = GetLastError();
        Temp << "***Error closing handle for " << DirPath;
        throw(std::runtime_error(Temp.str()));

    }

    return FileNameList;
}

void
UpdateOneDirectory(std::string SourceDir,
		   std::string DestDir,
		   StringContainer ExcludeName) {

    StringContainer DestDirFileList;                                            // Files in DestDir.


    DestDirFileList = GetFileList(DestDir, ExcludeName);

    StringContainer::iterator iFile;
    std::string FileToDelete;

    for (iFile = DestDirFileList.begin();                                       // Remove the files.
         iFile != DestDirFileList.end();
         iFile++) {

        if (! IsInContainer(*iFile, ExcludeName)) {                             // Check if this file is in ExcludeName.

            FileToDelete = DestDir + "\\";                                      // It is not.  Delete it.
            FileToDelete += *iFile;
            if (DeleteFile(FileToDelete.c_str()) == 0) {
                std::ostringstream Temp;

                LastError = GetLastError();
                Temp << "***Error deleting " << FileToDelete;
                throw(std::runtime_error(Temp.str()));

            }
        }
    }

    StringContainer SourceDirFileList;                                          // Files in SourceDir.
    std::string SourceFile, DestFile;

    SourceDirFileList = GetFileList(SourceDir, ExcludeName);
    for (iFile = SourceDirFileList.begin();                                     // Copy the files from SourceDir.
         iFile != SourceDirFileList.end();
         iFile++) {

        if (! IsInContainer(*iFile, ExcludeName)) {                             // Check if this file is in ExcludeName.

            SourceFile = SourceDir + "\\";                                      // It is not.  Copy it.
            SourceFile += *iFile;
            DestFile = DestDir + "\\";
            DestFile += *iFile;
            if (CopyFile(SourceFile.c_str(), DestFile.c_str(), true) == 0) {
                std::ostringstream Temp;

                LastError = GetLastError();
                Temp << "Error copying " << SourceFile << " to " << DestFile;
                throw(std::runtime_error(Temp.str()));

            }
        }
    }

    StringContainer SourceDirDirList, DestDirDirList;
    std::string SourceUpdateDir, DestUpdateDir;
    StringContainer SourceDirExtraDirList, DestDirExtraDirList;

    SourceDirDirList = GetDirectoryList(SourceDir, ExcludeName);
    DestDirDirList = GetDirectoryList(DestDir, ExcludeName);

    for (iFile = DestDirDirList.begin();                                        // Check that all directories in
         iFile != DestDirDirList.end();                                         // the destination directory are
         iFile++) {                                                             // also in the source directory.

        if (! IsInContainer(*iFile, SourceDirDirList)) {                        // Not in source directory?

            DestDirExtraDirList.push_back(*iFile);                              // It is not.  This is an error.

        }

    }

    for (iFile = SourceDirDirList.begin();                                      // Check that all directories in
         iFile != SourceDirDirList.end();                                       // the source directory are also
         iFile++) {                                                             // in the destination directory.

        if (! IsInContainer(*iFile, DestDirDirList)) {                          // Not in source directory?

            SourceDirExtraDirList.push_back(*iFile);                            // It is not.  This is an error.

        }

    }


    if (!SourceDirExtraDirList.empty() || !DestDirExtraDirList.empty()) {       // Check for error.
        std::ostringstream Temp;

        if (!SourceDirExtraDirList.empty()) {                                   // Extra directories in source.

            Temp << "***Error--There were directories in " << SourceDir
                 << " that are not in " << DestDir << ":\n\t";

            std::copy(SourceDirExtraDirList.begin(),
                      SourceDirExtraDirList.end(),
                      std::ostream_iterator<std::string>(Temp, "\n\t"));

        }

        if (!DestDirExtraDirList.empty()) {                                     // Extra directories in destination.

            Temp << "***Error--There were directories in " << DestDir
                 << " that are not in " << SourceDir << ":\n\t";
            std::copy(DestDirExtraDirList.begin(),
                      DestDirExtraDirList.end(),
                      std::ostream_iterator<std::string>(Temp, "\n\t"));

        }

        throw(std::runtime_error(Temp.str()));
    }

    for (iFile = DestDirDirList.begin();                                        // Update each directory.
         iFile != DestDirDirList.end();
         iFile++) {

        SourceUpdateDir = SourceDir + "\\";
        SourceUpdateDir += *iFile;
        DestUpdateDir = DestDir + "\\";
        DestUpdateDir += *iFile;
        UpdateOneDirectory(SourceUpdateDir, DestUpdateDir, ExcludeName);
        
    }

}


void
UpdateCommonDirectories(std::string DistPath, std::string DistParentPath,
			StringContainer ExcludeName) {

    StringContainer DistDirList, DistParentDirList;

    DistDirList = GetDirectoryList(DistPath, ExcludeName);                      // Distribution directory names.
    DistParentDirList = GetDirectoryList(DistParentPath, ExcludeName);          // Parent of distribution directory names.

#if 0
    // DEBUG.
    std::cout << "Directory list of distribution--\n";
    std::copy(DistDirList.begin(),
              DistDirList.end(),
              std::ostream_iterator<std::string>(std::cout, "\n"));

    std::cout << "\nDirectory list of distribution parent--\n";
    std::copy(DistParentDirList.begin(),
              DistParentDirList.end(),
              std::ostream_iterator<std::string>(std::cout, "\n"));
    // END OF DEBUG.
#endif

    StringContainer::iterator iDistDir;                                         // Update direcotries with the same name.

    for (iDistDir = DistDirList.begin();
         iDistDir != DistDirList.end();
         iDistDir++) {
        std::string SourceDir, DestDir;

        if (IsInContainer(*iDistDir, DistParentDirList)) {                      // Check if this is a common directory.

            SourceDir = DistParentPath + "\\";                                  // This is a common directory.
            SourceDir += *iDistDir;
            DestDir = DistPath + "\\";
            DestDir += *iDistDir;
            UpdateOneDirectory(SourceDir, DestDir, ExcludeName);
        }

    }

    return;

}

void
PrepareTempDir(std::string TempPath) {

    WIN32_FIND_DATA FileData;
    DWORD LastError;
    HANDLE DirHandle;
    std::ostringstream Temp;

    DirHandle = FindFirstFile(TempPath.c_str(), &FileData);                     // Check whether the directory exists.
    if (INVALID_HANDLE_VALUE == DirHandle) {                                    // Directory doesn't exist.

        if (!CreateDirectory(TempPath.c_str(), NULL)) {

            LastError = GetLastError();
            Temp << "***Error creating the temporary directory " << TempPath;
            throw(std::runtime_error(Temp.str()));

        }

        return;

    }

    // If the path is a file, delete it and create a new directory.
    if ( !(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {

        if (CloseHandle(DirHandle) != 0) {

            LastError = GetLastError();
            Temp << "***Error closing handle for " << TempPath;
            throw(std::runtime_error(Temp.str()));

        }

        if (DeleteFile(TempPath.c_str()) != 0) {

            LastError = GetLastError();
            Temp << "***Error deleting " << TempPath;
            throw(std::runtime_error(Temp.str()));

        }

        PrepareTempDir(TempPath);
        return;

    }

    if (CloseHandle(DirHandle) != 0) {

        LastError = GetLastError();
        Temp << "***Error closing handle for " << TempPath;
        throw(std::runtime_error(Temp.str()));

    }

    DeletePath(TempPath);                                                       // TempPath is a directory.  Delete it.
    PrepareTempDir(TempPath);                                                   // Create an empty directory.

}

void
DeletePath(std::string Path) {

    StringContainer DirList, FileList, ExcludeName;
    StringContainer::iterator iName;
    std::string NameToDelete;

    ExcludeName.push_back(".");
    ExcludeName.push_back("..");

    DirList = GetDirectoryList(Path, ExcludeName);
    FileList = GetFileList(Path, ExcludeName);

    for (iName = FileList.begin();                                              // Delete each file.
         iName != FileList.end();
         iName++) {

        NameToDelete = Path + "\\";
        NameToDelete += *iName;
        if (DeleteFile(NameToDelete.c_str()) == 0) {
            std::ostringstream Temp;

            LastError = GetLastError();
            Temp << "***Error deleting " << NameToDelete << " in " << Path;
            throw(std::runtime_error(Temp.str()));

        }

    }

    for (iName = DirList.begin();                                               // Delete each directory.
         iName != DirList.end();
         iName++) {

        NameToDelete = Path + "\\";
        NameToDelete += *iName;
        DeletePath(NameToDelete);

    }

    if (RemoveDirectory(Path.c_str()) == 0) {
        std::ostringstream Temp;

        LastError = GetLastError();
        Temp << "***Error removing directory " << Path;
        throw(std::runtime_error(Temp.str()));

    }

}

void
CopyDirectory(std::string SourcePath, std::string DestPath, StringContainer ExcludeName) {

    StringContainer DirList, FileList;
    StringContainer::iterator iName;
    std::string SourceName, DestName;
    BOOL FailIfExists = true;

    DirList = GetDirectoryList(SourcePath, ExcludeName);
    FileList = GetFileList(SourcePath, ExcludeName);

    for (iName = FileList.begin();                                              // Copy each file.
         iName != FileList.end();
         iName++) {

        SourceName = SourcePath + "\\";
        SourceName += *iName;
        DestName = DestPath + "\\";
        DestName += *iName;
        if (!CopyFile(SourceName.c_str(), DestName.c_str(), FailIfExists)) {
            std::ostringstream Temp;

            LastError = GetLastError();
            Temp << "Error copying " << *iName << " to " << DestPath;
            throw(std::runtime_error(Temp.str()));

        }

    }

    for (iName = DirList.begin();                                               // Copy each directory.
         iName != DirList.end();
         iName++) {

        SourceName = SourcePath + "\\";
        SourceName += *iName;
        DestName = DestPath + "\\";
        DestName += *iName;
        
        if (!CreateDirectory(DestName.c_str(), NULL)) {                         // Create the directory.
            std::ostringstream Temp;

            LastError = GetLastError();
            Temp << "***Error creating the temporary directory " << DestName;
            throw(std::runtime_error(Temp.str()));

        }

        CopyDirectory(SourceName, DestName, ExcludeName);

    }

}
