/*
 * Copyright (c) 2005-2006 Tom N Harris <telliamed@whoopdedo.org>.
 *
 *     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.
 *
 *     A copy of the GNU General Public License can be found at
 *     http://www.gnu.org/
 *
 * Written by Tom N Harris.
 *
 */

#include <algorithm>
#include <cstring>
#include <cctype>

#include "InstallInfo.h"
#include "InstallCfg.h"
#include "globals.h"
#include "dark.h"

using namespace Setup;
using namespace std;


static int
WhichGame(string const& str)
{
	// I really should allow for comma-separated strings
	// not to mention stripping whitespace, et-al...
	const char* s = str.c_str();
	if (! stricmp(s, "all"))
		return DARKGAME_ALL;
	if (! stricmp(s, "thief2"))
		return DARKGAME_T2;
	if (! stricmp(s, "sshock2"))
		return DARKGAME_SS2;
	if (! stricmp(s, "thief1"))
		return DARKGAME_T1;
	return 0;
}

InstallDir::InstallDir(string const& nm)
	: m_name(nm)
{
#ifndef UNICODE
	m_name = Win32::LocalizeString(m_name.to_ucs2());
#endif
	replace(m_name.begin(), m_name.end(), TEXT('/'), TEXT('\\'));
	if (*m_name.end() == TEXT('\\'))
		m_name.erase(m_name.size() - 1);
	size_t pos = 0;
	while (m_name[pos] == TEXT('\\') && ++pos < m_name.size()) ;
	m_name.erase(0, pos);
}

bool 
InstallDir::FileExistsInDest(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');

	DWORD attr;
	size_t lpos = 0;
	size_t rpos = m_name.find(TEXT('\\'));
	while (rpos < m_name.size())
	{
		path.append(m_name, lpos, rpos - lpos);
		attr = ::GetFileAttributes(path.c_str());
		if (attr == INVALID_FILE_ATTRIBUTES)
			return false;
		if (0 == (attr & FILE_ATTRIBUTE_DIRECTORY))
			return true;
		lpos = rpos;
		rpos = m_name.find(TEXT('\\'), lpos + 1);
	}
	path.append(m_name, lpos, rpos - lpos);
	attr = ::GetFileAttributes(path.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES)
	    && (0 == (attr & FILE_ATTRIBUTE_DIRECTORY));
}

bool 
InstallDir::CreateDirectoryInDest(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');

	size_t lpos = 0;
	size_t rpos = m_name.find(TEXT('\\'));
	while (rpos < m_name.size())
	{
		path.append(m_name, lpos, rpos - lpos);
		if (INVALID_FILE_ATTRIBUTES == ::GetFileAttributes(path.c_str()))
		{
			if (! ::CreateDirectory(path.c_str(), NULL))
			{
				return false;
			}
		}
		lpos = rpos;
		rpos = m_name.find(TEXT('\\'), lpos + 1);
	}
	path.append(m_name, lpos, rpos - lpos);
	if (INVALID_FILE_ATTRIBUTES == ::GetFileAttributes(path.c_str()))
		return ::CreateDirectory(path.c_str(), NULL);
	return true;
}

bool
InstallDir::DeleteDirectoryFromDest(String const& dir, FILETIME const* timestamp) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');

	size_t lpos = path.size() - 1;
	path.append(m_name);
	if (0 == (::GetFileAttributes(path.c_str()) & FILE_ATTRIBUTE_DIRECTORY))
	{
		// If the deepest directory exists, then it's fair to assume the
		// whole path is valid... At least in this universe it is.
		return false;
	}

	size_t rpos = path.rfind(TEXT('\\'));
	do 
	{
		if (timestamp)
		{
			// This is an annoying way to do this.
			// Why the fsck doesn't Win32 have "GetFileTime(LPCTSR, ...)" ?
			WIN32_FIND_DATA dirinfo;
			HANDLE dh = ::FindFirstFile(path.c_str(), &dirinfo);
			if (dh != INVALID_HANDLE_VALUE)
			{
				::FindClose(dh);
				if (dirinfo.ftCreationTime < *timestamp)
					return true;
			}
		}
		if (! ::RemoveDirectory(path.c_str()))
			return false;
		path.erase(rpos);
		rpos = path.rfind(TEXT('\\'), rpos - 1);
	}
	while (rpos >= lpos && rpos != String::npos);
	return true;
}

bool 
InstallDir::NeedsInstallCfg() const
{
	return ! m_cfgs.empty();
}

void 
InstallDir::UpdateInstallCfg(InstallCfg& cfgfile, String const& dir) const
{
	if (m_cfgs.empty())
		return;
	
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_name;

	for (vector<t_cfg>::const_iterator cfg = m_cfgs.begin(); 
	     cfg != m_cfgs.end(); ++cfg)
	{
		switch (cfg->second)
		{
		case CfgAdd:
			cfgfile.AddDir(cfg->first, path);
			break;
		case CfgPrefix:
			cfgfile.AddDirFront(cfg->first, path);
			break;
		case CfgPostfix:
			cfgfile.AddDirBack(cfg->first, path);
			break;
		}
	}
}

void
InstallDir::addCfgEntry(string const& cfg, CfgType type)
{
	string s;
	s.reserve(cfg.size());
	for (string::const_iterator p = cfg.begin(); p != cfg.end(); ++p)
		s.push_back(tolower(*p));

	t_cfg t(s, type);
	vector<t_cfg>::iterator i = lower_bound(m_cfgs.begin(), m_cfgs.end(), t);
	if (i == m_cfgs.end() || i->first != cfg)
	{
		m_cfgs.insert(i, t);
	}
}

void
InstallDir::addCfgEntry(InstallDir const& other)
{
	if (other.m_cfgs.empty())
		return;
	if (m_cfgs.empty())
	{
		m_cfgs = other.m_cfgs;
		return;
	}
	if (m_cfgs == other.m_cfgs)
	{
		return;
	}

	// What I really need is an inplace union, but such is not in the STL
	// and I'm too lazy to do it myself.
	size_t sz = m_cfgs.size();
	m_cfgs.insert(m_cfgs.end(), other.m_cfgs.begin(), other.m_cfgs.end());
	inplace_merge(m_cfgs.begin(), m_cfgs.begin() + sz, m_cfgs.end());
	unique(m_cfgs.begin(), m_cfgs.end());
}

InstallFile::InstallFile(string const& src, string const& dst, bool ro)
	: m_source(src), m_dest(dst), m_readonly(ro)
{
#ifndef UNICODE
	m_source = Win32::LocalizeString(m_source.to_ucs2());
	m_dest = Win32::LocalizeString(m_dest.to_ucs2());
#endif
	replace(m_source.begin(), m_source.end(), TEXT('/'), TEXT('\\'));
	if (*m_source.end() == TEXT('\\'))
		m_source.erase(m_source.size() - 1);
	replace(m_dest.begin(), m_dest.end(), TEXT('/'), TEXT('\\'));
	if (*m_dest.end() == TEXT('\\'))
		m_dest.erase(m_dest.size() - 1);
}

InstallFile::InstallFile(string const& src, bool ro)
	: m_source(src), m_readonly(ro)
{
#ifndef UNICODE
	m_source = Win32::LocalizeString(m_source.to_ucs2());
#endif
	replace(m_source.begin(), m_source.end(), TEXT('/'), TEXT('\\'));
	size_t pos = m_source.rfind(TEXT('\\'));
	if (pos == m_source.size() - 1)
	{
		m_source.erase(pos);
		pos = m_source.rfind(TEXT('\\'), pos - 1);
	}
	if (pos < m_source.size())
	{
		++pos;
		m_dest.assign(m_source, pos, m_source.size() - pos);
	}
	else
		m_dest = m_source;
}

bool 
InstallFile::SourceExists() const
{
	DWORD attr = ::GetFileAttributes(m_source.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES)
	    && (0 == (attr & FILE_ATTRIBUTE_DIRECTORY));
}

bool 
InstallFile::FileExistsInDest(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_dest;

	DWORD attr = ::GetFileAttributes(path.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES);
}

bool 
InstallFile::DestIsDirectory(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_dest;

	DWORD attr = ::GetFileAttributes(path.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES)
	    && (attr & FILE_ATTRIBUTE_DIRECTORY);
}

bool 
InstallFile::DestIsReadOnly(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_dest;

	// Only tests the flags, not the ACL... 
	DWORD attr = ::GetFileAttributes(path.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES)
	    && (attr & FILE_ATTRIBUTE_READONLY);
}

bool 
InstallFile::CopyToDest(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_dest;

	DWORD attr = ::GetFileAttributes(path.c_str());
	if (INVALID_FILE_ATTRIBUTES != attr)
	{
		if (0 != (attr & FILE_ATTRIBUTE_DIRECTORY))
			return false;
		::SetFileAttributes(path.c_str(), FILE_ATTRIBUTE_NORMAL);
		::DeleteFile(path.c_str());
	}
	if (::CopyFile(m_source.c_str(), path.c_str(), FALSE))
	{
		if (m_readonly)
			::SetFileAttributes(path.c_str(), 
					::GetFileAttributes(path.c_str()) | FILE_ATTRIBUTE_READONLY);
		return true;
	}
	return false;
}

bool
InstallFile::DeleteFromDest(String const& dir) const
{
	String path = dir;
	replace(path.begin(), path.end(), TEXT('/'), TEXT('\\'));
	if (*path.end() != TEXT('\\'))
		path += TEXT('\\');
	path += m_dest;

	DWORD attr = ::GetFileAttributes(path.c_str());
	if (INVALID_FILE_ATTRIBUTES == attr)
		return true;
	if (0 != (attr & FILE_ATTRIBUTE_DIRECTORY))
		return false;
	::SetFileAttributes(path.c_str(), FILE_ATTRIBUTE_NORMAL);
	return ::DeleteFile(path.c_str());
}

bool 
InstallFile::FileExists(String const& path)
{
	DWORD attr = ::GetFileAttributes(path.c_str());
	return (attr != INVALID_FILE_ATTRIBUTES)
		&& (0 == (attr & FILE_ATTRIBUTE_DIRECTORY));
}


InstallTarget::InstallTarget(InstallSoftware const& soft, string const& id, string const& vers)
	: m_software(&soft), 
	  m_id(id), 
	  m_version(vers), 
	  m_game(0),
	  m_installgames(0)
{
#ifndef UNICODE
	m_version = Win32::LocalizeString(m_version.to_ucs2());
#endif
}

InstallTarget::InstallTarget(InstallSoftware const& soft, string const& id, string const& vers, string const& game)
	: m_software(&soft), 
	  m_id(id), 
	  m_version(vers), 
	  m_game(0),
	  m_installgames(0)
{
#ifndef UNICODE
	m_version = Win32::LocalizeString(m_version.to_ucs2());
#endif
	m_game = WhichGame(game);
}

InstallDir* 
InstallTarget::addDir(string const& dst)
{
	InstallDir dir(dst);
	vector<InstallDir>::iterator i = find(m_dirs.begin(), m_dirs.end(), dir);
	if (i == m_dirs.end())
		i = m_dirs.insert(i, dir);
	return &(*i);
}

void 
InstallTarget::addFile(string const& src, bool ro)
{
	InstallFile file(src, ro);
	vector<InstallFile>::iterator i = find(m_files.begin(), m_files.end(), file);
	if (i == m_files.end())
		m_files.insert(i, file);
}

void 
InstallTarget::addFile(string const& src, string const& dst, bool ro)
{
	InstallFile file(src, dst, ro);
	vector<InstallFile>::iterator i = find(m_files.begin(), m_files.end(), file);
	if (i == m_files.end())
		m_files.insert(i, file);
}

void 
InstallTarget::addDependency(string const& dep)
{
	vector<string>::iterator i = find(m_deps.begin(), m_deps.end(), dep);
	if (i == m_deps.end())
		m_deps.insert(i, dep);
}

void 
InstallTarget::removeDependency(string const& dep)
{
	vector<string>::iterator i = find(m_deps.begin(), m_deps.end(), dep);
	if (i != m_deps.end())
		m_deps.erase(i);
}

void 
InstallTarget::addGame(string const& game)
{
	m_game |= WhichGame(game);
}

void 
InstallTarget::addGame(int game)
{
	m_game |= game;
}


InstallSoftware::InstallSoftware(string const& id, string const& name)
	: m_id(id), m_name(name)
{
#ifndef UNICODE
	m_name = Win32::LocalizeString(m_name.to_ucs2());
#endif
}

void
InstallSoftware::addTarget(string const& targ)
{
	vector<string>::iterator i = find(m_targets.begin(), m_targets.end(), targ);
	if (i == m_targets.end())
		i = m_targets.insert(i, targ);
}

/*
InstallTarget* 
InstallSoftware::addTarget(string const& id, string const& vers)
{
	InstallTarget targ(*this, id, vers);
	vector<InstallTarget>::iterator i = find(m_targets.begin(), m_targets.end(), targ);
	if (i == m_targets.end())
	{
		i = m_targets.insert(i, targ);
	}
	return &(*i);
}

InstallTarget* 
InstallSoftware::addTarget(string const& id, string const& vers, string const& game)
{
	InstallTarget targ(*this, id, vers, game);
	vector<InstallTarget>::iterator i = find(m_targets.begin(), m_targets.end(), targ);
	if (i == m_targets.end())
	{
		i = m_targets.insert(i, targ);
	}
	return &(*i);
}
*/

