/*
	grive: an GPL program to sync a local directory with Google Drive
	Copyright (C) 2012  Wan Wai Ho

	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 version 2
	of the License.

	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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "State.hh"

#include "Entry.hh"
#include "Resource.hh"
#include "Syncer.hh"

#include "util/Crypt.hh"
#include "util/File.hh"
#include "util/log/Log.hh"
#include "json/JsonParser.hh"

#include <fstream>

namespace gr {

const std::string state_file = ".grive_state" ;
const std::string ignore_file = ".griveignore" ;
const int MAX_IGN = 65536 ;
const char* regex_escape_chars = ".^$|()[]{}*+?\\";
const boost::regex regex_escape_re( "[.^$|()\\[\\]{}*+?\\\\]" );

inline std::string regex_escape( std::string s )
{
	return regex_replace( s, regex_escape_re, "\\\\&", boost::format_sed );
}

State::State( const fs::path& root, const Val& options  ) :
	m_root		( root ),
	m_res		( options["path"].Str() ),
	m_cstamp	( -1 )
{
	Read() ;

	// the "-f" option will make grive always think remote is newer
	m_force = options.Has( "force" ) ? options["force"].Bool() : false ;

	std::string m_orig_ign = m_ign;
	if ( options.Has( "ignore" ) && options["ignore"].Str() != m_ign )
		m_ign = options["ignore"].Str();
	else if ( options.Has( "dir" ) )
	{
		const boost::regex trim_path( "^/+|/+$" );
		std::string m_dir = regex_replace( options["dir"].Str(), trim_path, "" );
		if ( !m_dir.empty() )
		{
			// "-s" is internally converted to an ignore regexp
			m_dir = regex_escape( m_dir );
			size_t pos = 0;
			while ( ( pos = m_dir.find( '/', pos ) ) != std::string::npos )
			{
				m_dir = m_dir.substr( 0, pos ) + "$|" + m_dir;
				pos = pos*2 + 3;
			}
			std::string ign = "^(?!"+m_dir+"(/|$))";
			m_ign = ign;
		}
	}

	m_ign_changed = m_orig_ign != "" && m_orig_ign != m_ign;
	m_ign_re = boost::regex( m_ign.empty() ? "^\\.(grive$|grive_state$|trash)" : ( m_ign+"|^\\.(grive|grive_state|trash)" ) );
}

State::~State()
{
}

/// Synchronize local directory. Build up the resource tree from files and folders
/// of local directory.
void State::FromLocal( const fs::path& p )
{
	m_res.Root()->FromLocal( m_st ) ;
	FromLocal( p, m_res.Root(), m_st.Item( "tree" ) ) ;
}

bool State::IsIgnore( const std::string& filename )
{
	return regex_search( filename.c_str(), m_ign_re );
}

void State::FromLocal( const fs::path& p, Resource* folder, Val& tree )
{
	assert( folder != 0 ) ;
	assert( folder->IsFolder() ) ;

	Val::Object leftover = tree.AsObject();

	for ( fs::directory_iterator i( p ) ; i != fs::directory_iterator() ; ++i )
	{
		std::string fname = i->path().filename().string() ;
		std::string path = folder->IsRoot() ? fname : ( folder->RelPath() / fname ).string();
		
		if ( IsIgnore( path ) )
			Log( "file %1% is ignored by grive", path, log::verbose ) ;
		else
		{
			// if the Resource object of the child already exists, it should
			// have been so no need to do anything here
			Resource *c = folder->FindChild( fname ), *c2 = c ;
			if ( !c )
			{
				c2 = new Resource( fname, "" ) ;
				folder->AddChild( c2 ) ;
			}
			leftover.erase( fname );
			Val& rec = tree.Item( fname );
			if ( m_force )
				rec.Del( "srv_time" );
			c2->FromLocal( rec ) ;
			if ( !c )
				m_res.Insert( c2 ) ;
			if ( c2->IsFolder() )
				FromLocal( *i, c2, rec.Item( "tree" ) ) ;
		}
	}

	for( Val::Object::iterator i = leftover.begin(); i != leftover.end(); i++ )
	{
		std::string path = folder->IsRoot() ? i->first : ( folder->RelPath() / i->first ).string();
		if ( IsIgnore( path ) )
			Log( "file %1% is ignored by grive", path, log::verbose ) ;
		else
		{
			// Restore state of locally deleted files
			Resource *c = folder->FindChild( i->first ), *c2 ;
			if ( !c )
			{
				c2 = new Resource( i->first, i->second.Has( "tree" ) ? "folder" : "file" ) ;
				folder->AddChild( c2 ) ;
			}
			Val& rec = tree.Item( i->first );
			if ( m_force || m_ign_changed )
				rec.Del( "srv_time" );
			c2->FromDeleted( rec );
			if ( !c )
				m_res.Insert( c2 ) ;
		}
	}
}

void State::FromRemote( const Entry& e )
{
	std::string fn = e.Filename() ;
	std::string k = e.IsDir() ? "folder" : "file";

	// common checkings
	if ( !e.IsDir() && ( fn.empty() || e.ContentSrc().empty() ) )
		Log( "%1% \"%2%\" is a google document, ignored", k, e.Name(), log::verbose ) ;
	
	else if ( fn.find('/') != fn.npos )
		Log( "%1% \"%2%\" contains a slash in its name, ignored", k, e.Name(), log::verbose ) ;
	
	else if ( !e.IsChange() && e.ParentHrefs().size() != 1 )
		Log( "%1% \"%2%\" has multiple parents, ignored", k, e.Name(), log::verbose ) ;

	else if ( e.IsChange() )
		FromChange( e ) ;

	else if ( !Update( e ) )
		m_unresolved.push_back( e ) ;
}

void State::ResolveEntry()
{
	while ( !m_unresolved.empty() )
	{
		if ( TryResolveEntry() == 0 )
			break ;
	}
}

std::size_t State::TryResolveEntry()
{
	assert( !m_unresolved.empty() ) ;

	std::size_t count = 0 ;
	std::list<Entry>& en = m_unresolved ;

	for ( std::list<Entry>::iterator i = en.begin() ; i != en.end() ; )
	{
		if ( Update( *i ) )
		{
			i = en.erase( i ) ;
			count++ ;
		}
		else
			++i ;
	}
	return count ;
}

void State::FromChange( const Entry& e )
{
	assert( e.IsChange() ) ;
	
	// entries in the change feed is always treated as newer in remote,
	// so we override the last sync time to 0
	if ( Resource *res = m_res.FindByHref( e.SelfHref() ) )
		m_res.Update( res, e ) ;
}

bool State::Update( const Entry& e )
{
	assert( !e.IsChange() ) ;
	assert( !e.ParentHref().empty() ) ;

	if ( Resource *res = m_res.FindByHref( e.SelfHref() ) )
	{
		std::string path = res->RelPath().string();
		if ( IsIgnore( path ) )
		{
			Log( "%1% is ignored by grive", path, log::verbose ) ;
			return true;
		}
		m_res.Update( res, e ) ;
		return true;
	}
	else if ( Resource *parent = m_res.FindByHref( e.ParentHref() ) )
	{
		assert( parent->IsFolder() ) ;

		std::string path = parent->IsRoot() ? e.Name() : ( parent->RelPath() / e.Name() ).string();
		if ( IsIgnore( path ) )
		{
			Log( "%1% is ignored by grive", path, log::verbose ) ;
			return true;
		}

		// see if the entry already exist in local
		std::string name = e.Name() ;
		Resource *child = parent->FindChild( name ) ;
		if ( child )
		{
			// since we are updating the ID and Href, we need to remove it and re-add it.
			m_res.Update( child, e ) ;
		}
		
		// folder entry exist in google drive, but not local. we should create
		// the directory
		else if ( e.IsDir() || !e.Filename().empty() )
		{
			// first create a dummy resource and update it later
			child = new Resource( name, e.IsDir() ? "folder" : "file" ) ;
			parent->AddChild( child ) ;
			m_res.Insert( child ) ;
			
			// update the state of the resource
			m_res.Update( child, e ) ;
		}
		
		return true ;
	}
	else
		return false ;
}

Resource* State::FindByHref( const std::string& href )
{
	return m_res.FindByHref( href ) ;
}

State::iterator State::begin()
{
	return m_res.begin() ;
}

State::iterator State::end()
{
	return m_res.end() ;
}

void State::Read()
{
	try
	{
		File st_file( m_root / state_file ) ;
		m_st = ParseJson( st_file );
		m_cstamp = m_st["change_stamp"].Int() ;
	}
	catch ( Exception& )
	{
	}

	try
	{
		File ign_file( m_root / ignore_file ) ;
		char ign[MAX_IGN] = { 0 };
		int s = ign_file.Read( ign, MAX_IGN-1 ) ;
		ParseIgnoreFile( ign, s );
	}
	catch ( Exception& e )
	{
	}
}

bool State::ParseIgnoreFile( const char* buffer, int size )
{
	const boost::regex re1( "/\\\\\\*\\\\\\*$" );
	const boost::regex re2( "^\\\\\\*\\\\\\*/" );
	const boost::regex re3( "([^\\\\](\\\\\\\\)*)/\\\\\\*\\\\\\*/" );
	const boost::regex re4( "([^\\\\](\\\\\\\\)*|^)\\\\\\*" );
	const boost::regex re5( "([^\\\\](\\\\\\\\)*|^)\\\\\\?" );
	std::string exclude_re, include_re;
	int prev = 0;
	for ( int i = 0; i <= size; i++ )
	{
		if ( buffer[i] == '\n' || ( i == size && i > prev ) )
		{
			while ( prev < i && ( buffer[prev] == ' ' || buffer[prev] == '\t' || buffer[prev] == '\r' ) )
				prev++;
			if ( buffer[prev] != '#' )
			{
				int j;
				for ( j = i-1; j > prev; j-- )
					if ( buffer[j-1] == '\\' || ( buffer[j] != ' ' && buffer[j] != '\t' && buffer[j] != '\r' ) )
						break;
				std::string str( buffer+prev, j+1-prev );
				bool inc = str[0] == '!';
				if ( inc )
					str = str.substr( 1 );
				str = regex_escape( str );
				str = regex_replace( str, re1, "/.*", boost::format_perl );
				str = regex_replace( str, re2, ".*/", boost::format_perl );
				str = regex_replace( str, re3, "$1/(.*/)*", boost::format_perl );
				str = regex_replace( str, re4, "$1[^/]*", boost::format_perl );
				std::string str1;
				while (1)
				{
					str1 = regex_replace( str, re5, "$1[^/]", boost::format_perl );
					if ( str1.size() == str.size() )
						break;
					str = str1;
				}
				if ( !inc )
					exclude_re = exclude_re + ( exclude_re.size() > 0 ? "|" : "" ) + str;
				else
					include_re = include_re + ( include_re.size() > 0 ? "|" : "" ) + str;
			}
			prev = i+1;
		}
	}

	if ( exclude_re.size() > 0 )
	{
		m_ign = "^" + ( include_re.size() > 0 ? "(?!" + include_re + ")" : std::string() ) + "(" + exclude_re + ")$";
		return true;
	}
	return false;
}

void State::Write()
{
	m_st.Set( "change_stamp", Val( m_cstamp ) ) ;
	m_st.Set( "ignore_regexp", Val( m_ign ) ) ;
	
	fs::path filename = m_root / state_file ;
	std::ofstream fs( filename.string().c_str() ) ;
	fs << m_st ;
}

void State::Sync( Syncer *syncer, const Val& options )
{
	// set the last sync time to the time on the client
	m_res.Root()->Sync( syncer, &m_res, options ) ;
}

long State::ChangeStamp() const
{
	return m_cstamp ;
}

void State::ChangeStamp( long cstamp )
{
	Log( "change stamp is set to %1%", cstamp, log::verbose ) ;
	m_cstamp = cstamp ;
}

} // end of namespace gr
