/*************************************************************************
*    UrBackup - Client/Server backup system
*    Copyright (C) 2011-2016 Martin Raiber
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU Affero General Public License as published by
*    the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
*    You should have received a copy of the GNU Affero General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
**************************************************************************/

#include "file_metadata.h"
#include "../Interface/Server.h"
#include "../Interface/File.h"
#include "../urbackupcommon/os_functions.h"
#include "../stringtools.h"
#include "../common/data.h"
#include "../fileservplugin/chunk_settings.h"
#include "../urbackupcommon/fileclient/FileClientChunked.h"
#include <assert.h>
#include <algorithm>
#include <memory>
#include "chunk_hasher.h"

namespace
{
	const char ID_GRANT_ACCESS=0;
	const char ID_DENY_ACCESS=1;
	
	const unsigned int METADATA_MAGIC=0xA4F04E41;

	

	bool write_metadata(IFile* out, INotEnoughSpaceCallback *cb, const FileMetadata& metadata, int64& written)
	{
		CWData data;
		data.addUInt(METADATA_MAGIC);
		data.addChar(0);
		metadata.serialize(data);
		written=0;

		_u32 metadata_size = static_cast<_u32>(data.getDataSize());

		metadata_size = little_endian(metadata_size);

		if(!writeRepeatFreeSpace(out, reinterpret_cast<char*>(&metadata_size), sizeof(metadata_size), cb))
		{
			Server->Log("Error writing file metadata to file \""+out->getFilename()+"\" -1", LL_ERROR);
			return false;
		}
		written+=sizeof(metadata_size);

		if(!writeRepeatFreeSpace(out, data.getDataPtr(), data.getDataSize(), cb))
		{
			Server->Log("Error writing file metadata to file \""+out->getFilename()+"\"", LL_ERROR);
			return false;
		}

		written+=data.getDataSize();

		return true;
	}

	_u32 get_metadata_size(IFile* in)
	{
		_u32 metadata_size_and_magic[2];
		if(in->Read(reinterpret_cast<char*>(&metadata_size_and_magic), sizeof(metadata_size_and_magic))!=sizeof(metadata_size_and_magic) ||
			metadata_size_and_magic[0]==0)
		{
			Server->Log("Error reading file metadata hashfilesize from \""+in->getFilename()+"\" -2", LL_DEBUG);
			return 0;
		}

		if(little_endian(metadata_size_and_magic[1])!=METADATA_MAGIC)
		{
			Server->Log("Metadata magic wrong in file \""+in->getFilename(), LL_DEBUG);
			return 0;
		}

		return little_endian(metadata_size_and_magic[0]);
	}

	bool read_metadata_values(IFile* in, FileMetadata& metadata)
	{
		_u32 metadata_size_and_magic[2];
		if(in->Read(reinterpret_cast<char*>(&metadata_size_and_magic), sizeof(metadata_size_and_magic))!=sizeof(metadata_size_and_magic) ||
			metadata_size_and_magic[0]==0)
		{
			Server->Log("Error reading file metadata hashfilesize from \""+in->getFilename()+"\" -2", LL_DEBUG);
			return false;
		}
		
		if(little_endian(metadata_size_and_magic[1])!=METADATA_MAGIC)
		{
			Server->Log("Metadata magic wrong in file \""+in->getFilename(), LL_DEBUG);
			return false;
		}

		metadata_size_and_magic[0] = little_endian(metadata_size_and_magic[0]);

		std::vector<char> buffer;
		buffer.resize(metadata_size_and_magic[0]);

		if(in->Read(&buffer[0], metadata_size_and_magic[0]-sizeof(unsigned int))!=metadata_size_and_magic[0]-sizeof(unsigned int))
		{
			Server->Log("Error reading file metadata hashfilesize from \""+in->getFilename()+"\" -3", LL_ERROR);
			return false;
		}

		CRData data(&buffer[0], buffer.size());

		bool ok=true;
		char version = 0;
		ok &= data.getChar(&version);
		ok &= metadata.read(data);

		if(version!=0)
		{
			Server->Log("Unknown metadata version at \""+in->getFilename()+"\"", LL_ERROR);
			return false;
		}

		if(!ok)
		{
			Server->Log("Malformed metadata at \""+in->getFilename()+"\"", LL_ERROR);
			return false;
		}

		return true;
	}	
}

bool write_file_metadata(IFile* out, INotEnoughSpaceCallback *cb, const FileMetadata& metadata, bool overwrite_existing, int64& truncate_to_bytes)
{
	int64 hashfilesize;
	int64 hashdata_size;
	bool needs_truncate=false;
	truncate_to_bytes=-1;

	if(out->Size()==0)
	{
		hashfilesize=little_endian((int64)-1);
		out->Seek(0);

		if(!writeRepeatFreeSpace(out, reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize), cb))
		{
			Server->Log("Error writing file metadata to file \""+out->getFilename()+"\"", LL_ERROR);
			return false;
		}

		hashdata_size = get_hashdata_size(hashfilesize);
	}
	else
	{
		if(!out->Seek(0))
		{
			Server->Log("Error seeking in metadata file \""+out->getFilename()+"\"", LL_ERROR);
			return false;
		}

		if(out->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize))!=sizeof(hashfilesize))
		{
			Server->Log("Error reading hashfilesize from metadata file \""+out->getFilename()+"\"", LL_ERROR);
			return false;
		}

		hashfilesize=little_endian(hashfilesize);

		hashdata_size = get_hashdata_size(hashfilesize);

		int64 size_should=hashdata_size;

		if(out->Size()!=size_should)
		{
			if(!overwrite_existing)
			{
				Server->Log("File \""+out->getFilename()+"\" has wrong size. Should="+convert(size_should)+" is="+convert(out->Size())+". Error writing metadata to file. -1", LL_WARNING);
				return false;
			}
			else
			{
				if(!out->Seek(hashdata_size))
				{
					Server->Log("Cannot seek to "+convert(hashdata_size)+"in \""+out->getFilename()+"\". Error writing metadata to file.", LL_WARNING);
					return false;
				}
				_u32 metadata_size = get_metadata_size(out);

				size_should+=metadata_size+sizeof(_u32);

				if(out->Size()>size_should)
				{
					if(!out->Seek(size_should))
					{
						Server->Log("Cannot seek to "+convert(size_should)+" (2) in \""+out->getFilename()+"\". Error writing metadata to file.", LL_WARNING);
						return false;
					}

					int64 os_metadata_size;
					if(out->Read(reinterpret_cast<char*>(&os_metadata_size), sizeof(os_metadata_size))!=sizeof(os_metadata_size))
					{
						Server->Log("Error reading os metadata size. Error writing metadata to file.", LL_ERROR);
						return false;
					}

					size_should+=little_endian(os_metadata_size);

					if(out->Size()!=size_should)
					{
						Server->Log("File \""+out->getFilename()+"\" has wrong size. Should="+convert(size_should)+" is="+convert(out->Size())+". Error writing metadata to file. -3", LL_WARNING);
						return false;
					}

					needs_truncate=true;
				}
				else if(out->Size()!=size_should)
				{
					Server->Log("File \""+out->getFilename()+"\" has wrong size. Should="+convert(size_should)+" is="+convert(out->Size())+". Error writing metadata to file. -2", LL_WARNING);
					return false;
				}
			}
		}

		if(!out->Seek(hashdata_size))
		{
			Server->Log("Cannot seek to "+convert(hashdata_size)+"in \""+out->getFilename()+"\". Error writing metadata to file. -2", LL_WARNING);
			return false;
		}
	}
		

	int64 written;
	bool ret=write_metadata(out, cb, metadata, written);

	if(needs_truncate)
	{
		truncate_to_bytes = hashdata_size+written;
	}

	return ret;
}

bool write_file_metadata(const std::string& out_fn, INotEnoughSpaceCallback *cb, const FileMetadata& metadata, bool overwrite_existing)
{
	std::unique_ptr<IFile> out(Server->openFile(os_file_prefix(out_fn), MODE_RW_CREATE));

	if(!out.get())
	{
		Server->Log("Error writing file metadata to file \""+out_fn+"\"", LL_ERROR);
		return false;
	}

	int64 truncate_to_bytes;
	bool ret = write_file_metadata(out.get(), cb, metadata, overwrite_existing, truncate_to_bytes);

	if(ret && truncate_to_bytes>0)
	{
		out.reset();
		return os_file_truncate(out_fn, truncate_to_bytes);
	}

	return ret;
}

bool is_metadata_only(IFile* hash_file)
{
	int64 hashfilesize;

	hash_file->Seek(0);
	if(hash_file->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize))!=sizeof(hashfilesize))
	{
		return false;
	}

	return hashfilesize==-1;
}

bool read_metadata(const std::string& in_fn, FileMetadata& metadata)
{
	std::unique_ptr<IFile> in(Server->openFile(os_file_prefix(in_fn)));

	if(!in.get())
	{
		Server->Log("Error reading file metadata from file \""+in_fn+"\"", LL_DEBUG);
		return false;
	}

	return read_metadata(in.get(), metadata);
}

bool read_metadata(IFile* in, FileMetadata& metadata)
{
	int64 hashfilesize;

	in->Seek(0);
	if(in->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize))!=sizeof(hashfilesize))
	{
		Server->Log("Error reading file metadata hashfilesize from \""+in->getFilename()+"\"", LL_DEBUG);
		return false;
	}

	hashfilesize=little_endian(hashfilesize);

	if(hashfilesize!=-1)
	{
		in->Seek(get_hashdata_size(hashfilesize));
	}

	return read_metadata_values(in, metadata);
}

std::string escape_metadata_fn( const std::string& fn )
{
	if(fn.find(metadata_dir_fn)==0)
	{
		std::string num_str = fn.substr(sizeof(metadata_dir_fn)/sizeof(metadata_dir_fn[0])-1);
		if(num_str.empty())
		{
			return fn+"0";
		}

		size_t num_num = std::count_if(num_str.begin(), num_str.end(), str_isnumber);
		if(num_num!=num_str.size())
		{
			return fn;
		}

		return std::string(metadata_dir_fn)+convert(watoi64(num_str)+1);
	}
	else
	{
		return fn;
	}
}

std::string unescape_metadata_fn( const std::string& fn )
{
	if(fn.find(metadata_dir_fn)==0)
	{
		std::string num_str = fn.substr(sizeof(metadata_dir_fn)/sizeof(metadata_dir_fn[0])-1);
		if(num_str.empty())
		{
			return fn;
		}

		size_t num_num = std::count_if(num_str.begin(), num_str.end(), str_isnumber);
		if(num_num!=num_str.size())
		{
			return fn;
		}

		int64 c = watoi64(num_str)-1;

		if(c==0)
		{
			return metadata_dir_fn;
		}
		else
		{
			return std::string(metadata_dir_fn)+convert(c);
		}
	}
	else
	{
		return fn;
	}
}

int64 os_metadata_offset( IFile* meta_file )
{
	int64 hashfilesize;

	meta_file->Seek(0);
	if(meta_file->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize))!=sizeof(hashfilesize))
	{
		Server->Log("Error reading file metadata hashfilesize from \""+meta_file->getFilename()+"\"", LL_DEBUG);
		return -1;
	}

	hashfilesize=little_endian(hashfilesize);

	int64 meta_offset = 0;

	if(hashfilesize!=-1)
	{
		meta_offset = get_hashdata_size(hashfilesize);
		meta_file->Seek(meta_offset);
	}
	else
	{
		meta_offset=sizeof(int64);
	}

	if(!meta_file->Seek(meta_offset))
	{
		Server->Log("Error seeking to metadata in \""+meta_file->getFilename()+"\"", LL_DEBUG);
		return -1;
	}

	_u32 metadata_size_and_magic[2];
	if(meta_file->Read(reinterpret_cast<char*>(&metadata_size_and_magic), sizeof(metadata_size_and_magic))!=sizeof(metadata_size_and_magic) ||
		metadata_size_and_magic[0]==0)
	{
		Server->Log("Error reading file metadata hashfilesize from \""+meta_file->getFilename()+"\" -2", LL_DEBUG);
		return -1;
	}

	if(little_endian(metadata_size_and_magic[1])!=METADATA_MAGIC)
	{
		Server->Log("Metadata magic wrong in file \""+meta_file->getFilename(), LL_DEBUG);
		return -1;
	}

	return meta_offset + sizeof(_u32) + little_endian(metadata_size_and_magic[0]);
}

bool copy_os_metadata( const std::string& in_fn, const std::string& out_fn, INotEnoughSpaceCallback *cb)
{
	std::unique_ptr<IFile> in_f(Server->openFile(os_file_prefix(in_fn), MODE_READ));
    std::unique_ptr<IFile> out_f(Server->openFile(os_file_prefix(out_fn), MODE_RW));

	if(in_f.get()==NULL)
	{
		Server->Log("Error opening metadata file \""+in_fn+"\" (input)", LL_ERROR);
		return false;
	}

	if(out_f.get()==NULL)
	{
		Server->Log("Error opening metadata file \""+out_fn+"\" (output)", LL_ERROR);
		return false;
	}

	int64 offset = os_metadata_offset(in_f.get());

	if(offset==-1)
	{
		return false;
	}

	if(!in_f->Seek(offset))
	{
		Server->Log("Error seeking to os metadata in \""+in_fn+"\" (input)", LL_ERROR);
		return false;
	}

	if(!out_f->Seek(out_f->Size()))
	{
		Server->Log("Error seeking to os metadata in \""+out_fn+"\" (output)", LL_ERROR);
		return false;
	}

	std::vector<char> buffer;
	buffer.resize(32768);

	_u32 read;
	do 
	{
		read = in_f->Read(buffer.data(), static_cast<_u32>(buffer.size()));

		if(!writeRepeatFreeSpace(out_f.get(), buffer.data(), read, cb))
		{
			Server->Log("Error while writing os metadata to \""+out_fn+"\" (output)", LL_ERROR);
			return false;
		}

	} while (read>0);

	return true;
}

int64 read_hashdata_size( IFile* meta_file )
{
	int64 hashfilesize;

	meta_file->Seek(0);
	if(meta_file->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize))!=sizeof(hashfilesize))
	{
		Server->Log("Error reading file metadata hashfilesize from \""+meta_file->getFilename()+"\"", LL_DEBUG);
		return -1;
	}

	return little_endian(hashfilesize);
}

int64 get_hashdata_size(IFile * meta_file)
{
	int64 hashfilesize;

	meta_file->Seek(0);
	if (meta_file->Read(reinterpret_cast<char*>(&hashfilesize), sizeof(hashfilesize)) != sizeof(hashfilesize))
	{
		Server->Log("Error reading file metadata hashfilesize from \"" + meta_file->getFilename() + "\"", LL_DEBUG);
		return 0;
	}

	hashfilesize = little_endian(hashfilesize);

	if (hashfilesize>=0)
	{
		return get_hashdata_size(hashfilesize);
	}
	else
	{
		return 0;
	}
}

void FileMetadata::serialize( CWData& data ) const
{
	data.addString(shahash);
	data.addString(orig_path);
    data.addString(file_permissions);
	data.addVarInt(last_modified);
	data.addVarInt(created);
	data.addVarInt(accessed);
	data.addVarInt(rsize);
}

bool FileMetadata::read( CRData& data )
{
	bool ok=true;
	ok &= data.getStr(&shahash);
	ok &= data.getStr(&orig_path);
    ok &= data.getStr(&file_permissions);
	ok &= data.getVarInt(&last_modified);
	ok &= data.getVarInt(&created);
	ok &= data.getVarInt(&accessed);
	ok &= data.getVarInt(&rsize);

	if(ok)
	{
		exist=true;
	}

	return ok;
}

bool FileMetadata::read( str_map& extra_params )
{
	str_map::iterator it_orig_path = extra_params.find("orig_path");
	if(it_orig_path!=extra_params.end())
	{
		orig_path = (it_orig_path->second);
		has_orig_path=true;
	}

    str_map::iterator it_shahash = extra_params.find("sha512");
    if(it_shahash!=extra_params.end())
    {
        shahash = base64_decode_dash(it_shahash->second);
    }

	it_shahash = extra_params.find("thash");
	if (it_shahash != extra_params.end())
	{
		shahash = base64_decode_dash(it_shahash->second);
	}

	if(has_orig_path || !shahash.empty())
	{
		exist=true;
	}

	return true;
}

bool FileMetadata::hasPermission(int64 id, bool& denied) const
{
	return hasPermission(file_permissions, id, denied);
}

bool FileMetadata::hasPermission( const std::string& permissions, int64 id, bool& denied )
{
	CRData perm(permissions.data(),
		permissions.size());

	char action;
	while(perm.getChar(&action))
	{
		int64 pid;
		if(!perm.getVarInt(&pid))
		{
			return false;
		}

		switch(action)
		{
		case ID_GRANT_ACCESS:
			if(pid==0 || pid==id)
			{
				return true;
			}
			break;
		case ID_DENY_ACCESS:
			if(pid==0 || pid==id)
			{
				denied=true;
				return false;
			}
			break;
		}
	}

	return false;
}

void FileMetadata::set_shahash( const std::string& the_shahash )
{
	shahash=the_shahash;
}

void FileMetadata::set_orig_path( const std::string& the_orig_path )
{
	orig_path = the_orig_path;
}
