// -*- C++ -*-
/**
 * \file package.C
 * This file is part of LyX, the document processor.
 * Licence details can be found in the file COPYING.
 *
 * \author Angus Leeming
 *
 * Full author contact details are available in file CREDITS.
 *
 * Warning! This file is autogenerated from package.C.in.
 * All changes to this file will be lost.
 */

#include <config.h>

#include "support/package.h"

#include "debug.h"
#include "gettext.h"

#include "support/environment.h"
#include "support/filetools.h"
#include "support/lstrings.h"
#include "support/os.h"

#include <boost/assert.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/tuple/tuple.hpp>

#include <list>
#include <utility>

#if !defined (USE_WINDOWS_PACKAGING) && \
    !defined (USE_MACOSX_PACKAGING) && \
    !defined (USE_POSIX_PACKAGING)
#error USE_FOO_PACKAGING must be defined for FOO = WINDOWS, MACOSX or POSIX.
#endif

#if defined (USE_WINDOWS_PACKAGING)

/*
 * MinGW's version of winver.h contains this comment:
 *
 * If you need Win32 API features newer the Win95 and WinNT then you must
 * define WINVER before including windows.h or any other method of including
 * the windef.h header.
 *
 * GetLongPathNameA requires WINVER == 0x0500.
 *
 * It doesn't matter if the Windows version is older than this because the
 * function will compile but will fail at run time. See
 * http://msdn.microsoft.com/library/en-us/mslu/winprog/microsoft_layer_for_unicode_apis_with_limited_support.asp
 */
# if defined(__MINGW32__)
#  define WINVER 0x0500
# endif

# include <windows.h>
# include <shlobj.h>  // SHGetFolderPath

  // Needed for MinGW:
# ifndef SHGFP_TYPE_CURRENT
#  define SHGFP_TYPE_CURRENT 0
# endif

#elif defined (USE_MACOSX_PACKAGING)
# include <CoreServices/CoreServices.h> // FSFindFolder, FSRefMakePath
#endif

using std::string;

namespace fs = boost::filesystem;

namespace lyx {
namespace support {

namespace {

Package package_;
bool initialised_ = false;

} // namespace anon


void init_package(string const & command_line_arg0,
		  string const & command_line_system_support_dir,
		  string const & command_line_user_support_dir,
		  exe_build_dir_to_top_build_dir top_build_dir_location)
{
	// Can do so only once.
	if (initialised_)
		return;

	package_ = Package(command_line_arg0,
			   command_line_system_support_dir,
			   command_line_user_support_dir,
			   top_build_dir_location);
	initialised_ = true;
}


Package const & package()
{
	// Commented out because package().locale_dir() can be called
	// from the message translation code in messages.C before
	// init_package() is called. Lars is on the case...
	// BOOST_ASSERT(initialised_);
	return package_;
}


namespace {

string const abs_path_from_binary_name(string const & exe);

std::pair<string, string> const
get_build_dirs(string const & abs_binary,
	       exe_build_dir_to_top_build_dir top_build_dir_location);

string const get_document_dir(string const & home_dir);

string const get_home_dir();

string const get_locale_dir(string const & system_support_dir);

string const get_system_support_dir(string const & abs_binary,
				    string const & command_line_system_support_dir);

string const get_temp_dir();

string const get_default_user_support_dir(string const & home_dir);

std::pair<string, bool> const
get_user_support_dir(string const & default_user_support_dir,
		     string const & command_line_user_support_dir);

} // namespace anon


Package::Package(string const & command_line_arg0,
		 string const & command_line_system_support_dir,
		 string const & command_line_user_support_dir,
		 exe_build_dir_to_top_build_dir top_build_dir_location)
	: explicit_user_support_dir_(false)
{
	home_dir_ = get_home_dir();
	temp_dir_ = get_temp_dir();
	document_dir_ = get_document_dir(home_dir_);

	string const abs_binary = abs_path_from_binary_name(command_line_arg0);
	binary_dir_ = OnlyPath(abs_binary);

	// Is LyX being run in-place from the build tree?
	boost::tie(build_support_dir_, system_support_dir_) =
		get_build_dirs(abs_binary, top_build_dir_location);

	if (build_support_dir_.empty())
		system_support_dir_ =
			get_system_support_dir(abs_binary,
					       command_line_system_support_dir);

	locale_dir_ = get_locale_dir(system_support_dir_);

	string const default_user_support_dir =
		get_default_user_support_dir(home_dir_);
	boost::tie(user_support_dir_, explicit_user_support_dir_) =
		get_user_support_dir(default_user_support_dir,
				     command_line_user_support_dir);

	lyxerr[Debug::INIT]
		<< "<package>\n"
		<< "\tbinary_dir " << binary_dir() << '\n'
		<< "\tsystem_support " << system_support() << '\n'
		<< "\tbuild_support " << build_support() << '\n'
		<< "\tuser_support " << user_support() << '\n'
		<< "\tlocale_dir " << locale_dir() << '\n'
		<< "\tdocument_dir " << document_dir() << '\n'
		<< "\ttemp_dir " << temp_dir() << '\n'
		<< "\thome_dir " << home_dir() << '\n'
		<< "</package>\n" << std::endl;
}


namespace {

// These next three functions contain the stuff that is substituted at
// configuration-time.
string const top_srcdir()
{
	static string const dir("%TOP_SRCDIR%");
	return dir;
}


string const hardcoded_localedir()
{
	return string("%LOCALEDIR%");
}


string const hardcoded_system_support_dir()
{
	return string("../../lib/");
}

} // namespace anon


string const & Package::top_srcdir() const
{
	static string const dir("%TOP_SRCDIR%");
	return dir;
}


namespace {

bool check_command_line_dir(string const & dir,
			    string const & file,
			    string const & command_line_switch);

string const extract_env_var_dir(string const & env_var);

bool check_env_var_dir(string const & dir,
		       string const & env_var);

bool check_env_var_dir(string const & dir,
		       string const & file,
		       string const & env_var);

string const relative_locale_dir();

string const relative_system_support_dir();


#if defined (USE_WINDOWS_PACKAGING)
// Given a folder ID, returns the folder name (in unix-style format).
// Eg CSIDL_PERSONAL -> "C:/Documents and Settings/USERNAME/My Documents"
string const win32_folder_path(int folder_id)
{
	char folder_path[MAX_PATH + 1];
	if (SUCCEEDED(SHGetFolderPath(0, folder_id, 0,
				      SHGFP_TYPE_CURRENT, folder_path)))
		return os::internal_path(folder_path);
	return string();
}
#endif


std::string const
get_build_support_dir(std::string const & binary_dir,
		      exe_build_dir_to_top_build_dir top_build_dir_location)
{
	string indirection;
	switch (top_build_dir_location) {
	case top_build_dir_is_one_level_up:
		indirection = "../lib";
		break;
	case top_build_dir_is_two_levels_up:
		indirection = "../../lib";
		break;
	}

	return NormalizePath(AddPath(binary_dir, indirection));
}


std::pair<string, string> const
get_build_dirs(string const & abs_binary,
	       exe_build_dir_to_top_build_dir top_build_dir_location)
{
	string const check_text = "Checking whether LyX is run in place...";

	// We're looking for "lyxrc.defaults" in a directory
	//   binary_dir/../lib
	// We're also looking for "chkconfig.ltx" in a directory
	//   binary_dir/top_srcdir()/lib
	// If both are found, then we're running LyX in-place.

	// Note that the name of the lyx binary may be a symbolic link.
	// If that is the case, then we follow the links too.
	string binary = abs_binary;
	while (true) {
		// Try and find "lyxrc.defaults".
		string const binary_dir = OnlyPath(binary);
		string const build_support_dir =
			get_build_support_dir(binary_dir, top_build_dir_location);

		if (!FileSearch(build_support_dir, "lyxrc.defaults").empty()) {
			// Try and find "chkconfig.ltx".
			string const system_support_dir =
				MakeAbsPath(AddPath(top_srcdir(), "lib"),
					    AddPath(binary_dir, "support"));

			if (!FileSearch(system_support_dir, "chkconfig.ltx").empty()) {
				lyxerr[Debug::INIT] << check_text << " yes"
						    << std::endl;
				return std::make_pair(build_support_dir, system_support_dir);
			}
		}

		// Check whether binary is a symbolic link.
		// If so, resolve it and repeat the exercise.
		if (!fs::symbolic_link_exists(binary))
			break;

		string link;
		if (LyXReadLink(binary, link, true)) {
			binary = link;
		} else {
			// Unable to resolve the link.
			break;
		}
	}

	lyxerr[Debug::INIT] << check_text << " no" << std::endl;
	return std::make_pair(string(), string());
}


// Specification of document_dir_ may be reset by LyXRC,
// but the default is fixed for a given OS.
string const get_document_dir(string const & home_dir)
{
#if defined (USE_WINDOWS_PACKAGING)
	(void)home_dir; // Silence warning about unused variable.
	return win32_folder_path(CSIDL_PERSONAL);
#else // Posix-like.
	return home_dir;
#endif
}


// The specification of home_dir_ is fixed for a given OS.
// A typical example on Windows: "C:/Documents and Settings/USERNAME"
// and on a Posix-like machine: "/home/USERNAME".
string const get_home_dir()
{
#if defined (USE_WINDOWS_PACKAGING)
	string const home_dir = getEnv("USERPROFILE");
#else // Posix-like.
	string const home_dir = getEnv("HOME");
#endif

	return os::internal_path(home_dir);
}


// Several sources are probed to ascertain the locale directory.
// The only requirement is that the result is indeed a directory.
string const get_locale_dir(string const & system_support_dir)
{
	// 1. Use the "LYX_LOCALEDIR" environment variable.
	string path = extract_env_var_dir("LYX_LOCALEDIR");
	if (!path.empty() && check_env_var_dir(path, "LYX_LOCALEDIR"))
		return path;

	// 2. Search for system_support_dir / <relative locale dir>
	// The <relative locale dir> is OS-dependent. (On Unix, it will
	// be "../locale/".)
	path = NormalizePath(AddPath(system_support_dir, relative_locale_dir()));

	if (fs::exists(path) && fs::is_directory(path))
		return path;

	// 3. Fall back to the hard-coded LOCALEDIR.
	path = hardcoded_localedir();
	if (fs::exists(path) && fs::is_directory(path))
		return path;

	return string();
}


// Specification of temp_dir_ may be reset by LyXRC,
// but the default is fixed for a given OS.
string const get_temp_dir()
{
#if defined (USE_WINDOWS_PACKAGING)
	// Typical example: C:/TEMP/.
	char path[MAX_PATH + 1];
	GetTempPath(MAX_PATH, path);
	GetLongPathName(path, path, MAX_PATH + 1);
	return os::internal_path(path);
#else // Posix-like.
	return "/tmp";
#endif
}


// If we use a 'lazy' lyxerr in the hope of setting the locale before
// printing any messages, then we should ensure that it is flushed first.
void bail_out()
{
#ifndef CXX_GLOBAL_CSTD
	using std::exit;
#endif
	exit(1);
}


// Extracts the absolute path from the foo of "-sysdir foo" or "-userdir foo"
string const abs_path_from_command_line(string const & command_line)
{
	if (command_line.empty())
		return string();

	string const path = os::internal_path(command_line);
	return os::is_absolute_path(path) ? path : MakeAbsPath(path);
}


// Does the grunt work for abs_path_from_binary_name()
string const get_binary_path(string const & exe)
{
	// The executable may have been invoked either with or
	// without the .exe extension.
	// Ensure that it is present.
	string const as_internal_path = os::internal_path(exe);
	string const exe_path = suffixIs(as_internal_path, ".exe") ?
		as_internal_path : as_internal_path + ".exe";
	if (os::is_absolute_path(exe_path))
		return exe_path;

	// Two possibilities present themselves.
	// 1. The binary is relative to the CWD.
	string const abs_exe_path = MakeAbsPath(exe_path);
	if (fs::exists(abs_exe_path))
		return abs_exe_path;

	// 2. exe must be the name of the binary only and it
	// can be found on the PATH.
	string const exe_name = OnlyFilename(exe_path);
	if (exe_name != exe_path)
		return string();

	std::vector<string> const path = getEnvPath("PATH");
	std::vector<string>::const_iterator it = path.begin();
	std::vector<string>::const_iterator const end = path.end();
	for (; it != end; ++it) {
		// This will do nothing if *it is already absolute.
		string const exe_dir = MakeAbsPath(*it);

		string const exe_path = AddName(exe_dir, exe_name);
		if (fs::exists(exe_path))
			return exe_path;
	}

	// Didn't find anything.
	return string();
}


// Extracts the absolute path to the binary name received as argv[0].
string const abs_path_from_binary_name(string const & exe)
{
	string const abs_binary = get_binary_path(exe);
	if (abs_binary.empty()) {
		lyxerr << bformat(_("Unable to determine the path to the "
				    "LyX binary from the command line %1$s"),
				  exe)
		       << std::endl;
		bail_out();
	}
	return abs_binary;
}


// A plethora of directories is searched to ascertain the system
// lyxdir which is defined as the first directory to contain
// "chkconfig.ltx".
string const
get_system_support_dir(string const & abs_binary,
		  string const & command_line_system_support_dir)
{
	string const chkconfig_ltx = "chkconfig.ltx";

	// searched_dirs is used for diagnostic purposes only in the case
	// that "chkconfig.ltx" is not found.
	std::list<string> searched_dirs;

	// 1. Use the -sysdir command line parameter.
	string path = abs_path_from_command_line(command_line_system_support_dir);
	if (!path.empty()) {
		searched_dirs.push_back(path);
		if (check_command_line_dir(path, chkconfig_ltx, "-sysdir"))
			return path;
	}

	// 2. Use the "LYX_DIR_13x" environment variable.
	path = extract_env_var_dir("LYX_DIR_13x");
	if (!path.empty()) {
		searched_dirs.push_back(path);
		if (check_env_var_dir(path, chkconfig_ltx, "LYX_DIR_13x"))
			return path;
	}

	// 3. Search relative to the lyx binary.
	// We're looking for "chkconfig.ltx" in a directory
	//   OnlyPath(abs_binary) / <relative dir> / PACKAGE /
	// PACKAGE is hardcoded in config.h. Eg "lyx" or "lyx-1.3.6cvs".
	// <relative dir> is OS-dependent; on Unix, it will be "../share/".
	string const relative_lyxdir = relative_system_support_dir();

	// One subtlety to be aware of. The name of the lyx binary may be
	// a symbolic link. If that is the case, then we follow the links too.
	string binary = abs_binary;
	while (true) {
		// Try and find "chkconfig.ltx".
		string const binary_dir = OnlyPath(binary);

		string const lyxdir =
			NormalizePath(AddPath(binary_dir, relative_lyxdir));
		searched_dirs.push_back(lyxdir);

		if (!FileSearch(lyxdir, chkconfig_ltx).empty()) {
			// Success! "chkconfig.ltx" has been found.
			return lyxdir;
		}

		// Check whether binary is a symbolic link.
		// If so, resolve it and repeat the exercise.
		if (!fs::symbolic_link_exists(binary))
			break;

		string link;
		if (LyXReadLink(binary, link, true)) {
			binary = link;
		} else {
			// Unable to resolve the link.
			break;
		}
	}

	// 4. Repeat the exercise on the directory itself.
	string binary_dir = OnlyPath(abs_binary);
	while (true) {
		// This time test whether the directory is a symbolic link
		// *before* looking for "chkconfig.ltx".
		// (We've looked relative to the original already.)
		if (!fs::symbolic_link_exists(binary))
			break;

		string link;
		if (LyXReadLink(binary_dir, link, true)) {
			binary_dir = link;
		} else {
			// Unable to resolve the link.
			break;
		}

		// Try and find "chkconfig.ltx".
		string const lyxdir =
			NormalizePath(AddPath(binary_dir, relative_lyxdir));
		searched_dirs.push_back(lyxdir);

		if (!FileSearch(lyxdir, chkconfig_ltx).empty()) {
			// Success! "chkconfig.ltx" has been found.
			return lyxdir;
		}
	}

	// 5. In desparation, try the hard-coded system support dir.
	path = hardcoded_system_support_dir();
	if (!FileSearch(path, chkconfig_ltx).empty())
		return path;

	// Everything has failed :-(
	// So inform the user and exit.
	string searched_dirs_str;
	typedef std::list<string>::const_iterator iterator;
	iterator const begin = searched_dirs.begin();
	iterator const end = searched_dirs.end();
	for (iterator it = begin; it != end; ++it) {
		if (it != begin)
			searched_dirs_str += "\n\t";
		searched_dirs_str += *it;
	}

	lyxerr << bformat(_("Unable to determine the system directory "
			    "having searched\n"
			    "\t%1$s\n"
			    "Try the '-sysdir' command line parameter or "
			    "set the environment variable LYX_DIR_13x to "
			    "the LyX system directory containing the file "
			    "`chkconfig.ltx'."),
			  searched_dirs_str)
	       << std::endl;

	bail_out();
	// Keep the compiler happy.
	return string();
}


// Returns the absolute path to the user lyxdir, together with a flag
// indicating whether this directory was specified explicitly (as -userdir
// or through an environment variable) or whether it was deduced.
std::pair<string, bool> const
get_user_support_dir(string const & default_user_support_dir,
		     string const & command_line_user_support_dir)
{
	bool explicit_userdir = true;

	// 1. Use the -userdir command line parameter.
	string path =
		abs_path_from_command_line(command_line_user_support_dir);
	if (!path.empty())
		return std::make_pair(path, explicit_userdir);

	// 2. Use the LYX_USERDIR_13x environment variable.
	path = extract_env_var_dir("LYX_USERDIR_13x");
	if (!path.empty())
		return std::make_pair(path, explicit_userdir);

	// 3. Use the OS-dependent default_user_support_dir
	explicit_userdir = false;
	return std::make_pair(default_user_support_dir, explicit_userdir);
}


// $HOME/.lyx on all platforms but Win32 where it will be something like
// "C:/Documents and Settings/USERNAME/Application Data/lyx"
string const get_default_user_support_dir(string const & home_dir)
{
#if defined (USE_WINDOWS_PACKAGING)
	(void)home_dir; // Silence warning about unused variable.

	return AddPath(win32_folder_path(CSIDL_APPDATA), PACKAGE);

#elif defined (USE_MACOSX_PACKAGING)
	(void)home_dir; // Silence warning about unused variable.

	FSRef fsref;
	OSErr const error_code =
		FSFindFolder(kUserDomain, kApplicationSupportFolderType,
			     kDontCreateFolder, &fsref);
	if (error_code != 0)
		return string();

	char store[PATH_MAX + 1];
	OSStatus const status_code =
		FSRefMakePath(&fsref,
			      reinterpret_cast<UInt8*>(store), PATH_MAX);
	if (status_code != 0)
		return string();

	return AddPath(reinterpret_cast<char const *>(store), PACKAGE);

#else // USE_POSIX_PACKAGING
	return AddPath(home_dir, string(".") + PACKAGE);
#endif
}


// Check that directory @c dir contains @c file.
// Else emit a warning about an invalid @c command_line_switch.
bool check_command_line_dir(string const & dir,
			    string const & file,
			    string const & command_line_switch)
{
	string const abs_path = FileSearch(dir, file);
	if (abs_path.empty()) {
		lyxerr << bformat(_("Invalid %1$s switch.\n"
				    "Directory %2$s does not contain %3$s."),
				  command_line_switch, dir, file)
		       << std::endl;
	}

	return !abs_path.empty();
}


// The environment variable @c env_var expands to a (single) file path.
string const extract_env_var_dir(string const & env_var)
{
	string const dir = os::internal_path(getEnv(env_var));
	return dir.empty() ? dir : MakeAbsPath(dir);
}


// Check that directory @c dir contains @c file.
// Else emit a warning about an invalid @c env_var.
bool check_env_var_dir(string const & dir,
		       string const & file,
		       string const & env_var)
{
	string const abs_path = FileSearch(dir, file);
	if (abs_path.empty()) {
		lyxerr << bformat(_("Invalid %1$s environment variable.\n"
				    "Directory %2$s does not contain %3$s."),
			          env_var, dir, file)
		       << std::endl;
	}

	return !abs_path.empty();
}


// Check that directory @c dir is indeed a directory.
// Else emit a warning about an invalid @c env_var.
bool check_env_var_dir(string const & dir,
		       string const & env_var)
{
	bool const success = (fs::exists(dir) && fs::is_directory(dir));

	if (!success) {
		// Put this string on a single line so that the gettext
		// search mechanism in po/Makefile.in.in will register
		// package.C.in as a file containing strings that need
		// translation.
		string const fmt =
		_("Invalid %1$s environment variable.\n%2$s is not a directory.");

		lyxerr << bformat(fmt, env_var, dir)
		       << std::endl;
	}

	return success;
}


// The locale directory relative to the LyX system directory.
string const relative_locale_dir()
{
	return "../locale/";
}


// The system lyxdir is relative to the directory containing the LyX binary.
string const relative_system_support_dir()
{
	string result;

#if defined (USE_WINDOWS_PACKAGING) || defined (USE_MACOSX_PACKAGING)
	result = AddPath("../Resources/", PACKAGE);
#else // Posix-like.
	result = AddPath("../share/", PACKAGE);
#endif

	return result;
}

} // namespace anon

} // namespace support
} // namespace lyx
