/*
 * Copyright (C) 2002, 2006 by the Widelands Development Team
 *
 * 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 WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */
// this originally comes from Return to the Shadows (http://www.rtts.org/)
// files.cc: provides all the OS abstraction to access files

#include "config.h"
#include "constants.h"
#include "disk_filesystem.h"
#include <errno.h>
#include "error.h"
#include "filesystem.h"
#include "layered_filesystem.h"
#include <string>
#include <unistd.h>
#include <vector>
#include "zip_exceptions.h"
#include "zip_filesystem.h"

#ifdef USE_DATAFILE
#include "datafile.h"
#endif

#ifdef _WIN32
  #include <windows.h>
  #include <io.h>
  #define stat _stat
  #include <sys/stat.h>
  #include <types.h>
#else
  #include <glob.h>
  #include <sys/stat.h>
  #include <sys/types.h>
  #include <unistd.h>
#endif

FileSystem::FileSystem()
{
#ifdef __WIN32__
	//TODO: this probably needs to be overwritten later
	m_root="C:\\";
	m_filesep='\\';
#else
	m_root="/";
	m_filesep='/';
#endif
}

/**
 * Append extension (e.g. ".foo") to the filename if it is'nt already there
 * \param filename The filename to possibly be extended
 * \param extension The extension to append, without leading dot (unless you
 * want to consecutive dots, that is)
 */
const std::string FileSystem::AutoExtension(const std::string filename,
													 const std::string extension)
{
	const SSS_T suffix_length=extension.size()+1;
	const SSS_T startpos=filename.size()-suffix_length;

	if (filename.substr(startpos, suffix_length) != "."+extension)
	{
		return filename+"."+extension;
	}

	return filename;
}

/**
 * Strip the extension (if any) from the filename
 */
const char *FileSystem::FS_StripExtension(char * const fname)
{
	char *p;
	char *dot = 0;

	for(p = fname; *p; p++) {
		if (*p == '/' || *p == '\\')
			dot = 0;
		else if (*p == '.')
			dot = p;
	}

	if (dot)
		*dot = 0;

	return fname;
}

/**
 * Translate filename so that it is relative to basefile.
 * Basically concatenates the two strings, but removes the filename part
 * of basefile (if any)
 */
const char *FileSystem::FS_RelativePath(char *buf, const int buflen, const char *basefile, const char *filename)
{
	const char *p;
	int endbase;

	if (*filename == '/' || *filename == '\\') { // it's an absolute filename
		snprintf(buf, buflen, "%s", filename);
		return buf;
	}

	// find the end of the basefile name
	endbase = 0;
	for(p = basefile; *p; p++) {
		if (*p == '/' || *p == '\\')
			endbase = p-basefile+1;
	}

	if (!endbase) { // no path in basefile
		snprintf(buf, buflen, "%s", filename);
		return buf;
	}

	// copy the base path
	snprintf(buf, buflen, "%s%s", basefile, filename);

	return buf;
}

/**
 * \param path A file or directory name
 * \return True if ref path is absolute
 */
const bool FileSystem::pathIsAbsolute(const std::string path) const
{
	if (path.substr(0,m_root.size())!=m_root)
		return false;

	if (path[m_root.size()]!=m_filesep)
		return false;

	return true;
}

/**
 * Prepend FS root and/or working directory (if neccessary) to make path absolute.
 * \param path A path that might or might not be absolute
 * \return An absolute path
 */
const std::string FileSystem::AbsolutePath(const std::string path) const
{
	if (pathIsAbsolute(path))
		return path;

#ifdef __WIN32__
	//For "nearly" absoute paths, just append drive letter
	if (path[0]==m_filesep)
		return m_root+path;
#endif

	return getWorkingDirectory()+m_filesep+path;
}

/**
 * \return The process' current working directory
 */
const std::string FileSystem::getWorkingDirectory() const
{
	char cwd[PATH_MAX+1];
	getcwd(cwd, PATH_MAX);

	return std::string(cwd);
}

/**
 * \return An existing directory where temporary files can be put
 * \todo Is there a temp directory on win32? Where?
 */
const std::string FileSystem::getTempDirectory()
{
	const char *tmpdir;

#ifdef __WIN32__
	tmpdir=".";
#else
	tmpdir="/tmp";
#endif

	if(!FileExists(tmpdir))
		throw FileNotFound_error("FileSystem::getTempDirectory", tmpdir);

	return tmpdir;
}

/// \todo Write homedir detection for non-getenv-systems
std::string FileSystem::GetHomedir()
{
	std::string homedir("");

#ifdef HAS_GETENV
	homedir=getenv("HOME");
#endif

	if (homedir.empty()) {
		printf("\nWARNING: either we can not detect your home directory "
		       "or you don't have one! Please contact the developers.\n\n");

		//TODO: is it really a good idea to set homedir to "." then ??

		printf("Instead of your home directory, '.' will be used.\n\n");
		homedir=".";
	}

	return homedir;
}

/**
 * Split a string into components seperated by a certain character
 *
 * \param path The path to parse
 * \param filesep A character seperating the components
 * \return a list of path components
 *
 * \todo This does not really belong into a filesystem class
 */
const std::vector<std::string> FileSystem::FS_Tokenize(const std::string path) const
{
	std::vector<std::string> components;
	SSS_T pos;  //start of token
	SSS_T pos2; //next filesep character

	//extract the first path component
	if (path.find(m_filesep)==0) //is this an absolute path?
		pos=1;
	else //relative path
		pos=0;
	pos2=path.find(m_filesep, pos);
	//'current' token is now between pos and pos2

	//split path into it's components
	while (pos2!=std::string::npos) {
		components.push_back(path.substr(pos, pos2-pos));
		pos=pos2+1;
		pos2=path.find(m_filesep, pos);
	}

	//extract the last component (most probably a filename)
	components.push_back(path.substr(pos));

	return components;
}

/**
 * Transform any valid, unique pathname into a well-formed absolute path
 *
 * \todo Enable non-Unix paths
 */
const std::string FileSystem::FS_CanonicalizeName(const std::string path) const
{
	std::vector<std::string> components;
	std::vector<std::string>::iterator i;

#ifndef __WIN32__
	bool absolute=pathIsAbsolute(path);

	components=FS_Tokenize(path);

	//tilde expansion
	if(*components.begin()=="~") {
		components.erase(components.begin());

		std::vector<std::string> homecomponents;
		homecomponents=FS_Tokenize(GetHomedir());
		components.insert(components.begin(),
		                  homecomponents.begin(), homecomponents.end());

		absolute=true;
	}

	//make relative paths absolute (so that "../../foo" can work)
	if (!absolute) {
		std::vector<std::string> cwdcomponents;

		if (m_root.empty())
			cwdcomponents=FS_Tokenize(getWorkingDirectory());
		else
			cwdcomponents=FS_Tokenize(m_root);

		components.insert(components.begin(),
		                  cwdcomponents.begin(), cwdcomponents.end());
		absolute=true;
	}

	//clean up the path
	for(i=components.begin(); i!=components.end(); ) {
		bool erase = false;
		bool erase_prev = false;

		//remove empty components ("foo/bar//baz/")
		if (i->empty()) erase = true;

		//remove single dot
		if (*i==".") erase = true;

		//remove double dot and the preceding component (if any)
		if (*i=="..") {
			if(i!=components.begin())
				erase_prev = true;
			erase = true;
		}

		std::vector<std::string>::iterator nexti = i;

		if( erase_prev && erase ) {
			components.erase( i-1, i+1);
			i = components.begin();
			continue;
		}
		if( erase ) {
			components.erase( i );
			i = components.begin();
			continue;
		}

		i++;
	}

	std::string canonpath="";
	if (absolute)
		canonpath="/";
	else
		canonpath="./";

	for(i=components.begin(); i!=components.end(); i++)
		canonpath+=*i+"/";

#else

	/*===============================================
	Windows-filesystems work different than Unix-FS
	so here is a solution, to get it run.

	Every path will be absolute afterwards.

	Only problem:
	Widelands will be started in the directory, the
	the executable is in, even if you run it from
	different directory. (Real problem???)
	===============================================*/

	bool absolutewin32=false;
	components=FS_Tokenize(path);
	if (path[0]=='\\') //Is a backslash in path? If yes, the path is allready absolute.
		absolutewin32=true;

	std::string canonpath="";
	if (absolutewin32==true)
		canonpath=""; //if the path is allready absolute, nothing has to be added.
	else   {
		canonpath=m_root.c_str();   //We simply add the root folder
		canonpath=canonpath+"\\"; //and of course an ending backslash
	}

	for(i=components.begin(); i!=components.end(); i++)
		canonpath+=*i+"\\";

#endif
	canonpath.erase(canonpath.end() - 1); //remove trailing slash
	return canonpath;
}

/**
 * Returns the filename of this path, everything after the last
 * / or \  (or the whole string)
 */
const char *FileSystem::FS_Filename(const char* buf) {
	int i=strlen(buf)-1;
	while(i>=0) {
		if(buf[i]=='/' || buf[i]=='\\') return &buf[i+1];
		--i;
	}
	return buf;
}

/**
 * Create a filesystem from a zipfile or a real directory
 * \todo Catch FileType_error in all users
 * \todo Check for existence before doing anything with the file/dir
 * \todo Catch FileNotFound_error in all users
 * \throw FileNotFound_error if root does not exist, is some kind of special file,
 * loops around (via symlinks) or is too long for the OS/filesystem
 * \throw FileAccessDenied_error if the OS denies access (of course ;-)
 * \throw FileTypeError if root is neither a directory or regular file
 * \todo throw FileTypeError if root is not a zipfile (exception from ZipFilesystem)
 */
FileSystem *FileSystem::Create(std::string root) throw(FileType_error,
      FileNotFound_error,
      FileAccessDenied_error)
{
	struct stat statinfo;

	if (stat(root.c_str(), &statinfo) == -1) {
		if ( errno==EBADF ||
		      errno==ENOENT ||
		      errno==ENOTDIR ||
#ifdef ELOOP
		      errno==ELOOP || //MinGW does not support ELOOP (yet)
#endif
		      errno==ENAMETOOLONG )
		{
			throw FileNotFound_error("FileSystem::Create", root);
		}
		if ( errno==EACCES ) {
			throw FileAccessDenied_error("FileSystem::Create", root);
		}
	}

	if(S_ISDIR(statinfo.st_mode)) {
		return new RealFSImpl(root);
	}
	if(S_ISREG(statinfo.st_mode)) { //TODO: ensure root is a zipfile
		return new ZipFilesystem(root);
	}

	throw FileType_error("FileSystem::Create", root,
	                     "cannot create virtual filesystem from file or directory");
}
