// Copyright (c) 2010, Jens Peter Secher <jpsecher@gmail.com>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

//
// A component is either self-titled, or named "." if it is the main component.
// 
typedef Component =
{
	var component : String;
	var tarball : String;
}

//
// The distilled information extracted from the list of tarball filenames to
// extract.  The main component is named ".", and the rest (if any) are
// self-titled.  The changlelog is filled with the remaining version information
// they have in common.
//
typedef Upstream =
{
	var components : Array<Component>;
	var changeLog : DebianChangeLog;
	var noMerge : Bool;
};

class MercurialImportOrig
{
	public static function main()
	{
		logger = new Script();
		// It must be possible to switch branch, so the working directory has to
		// be clean.
		Mercurial.exitIfUncommittedChanges( logger );
		Mercurial.switchToBranch( Constants.mainBranch(), logger );
		var upstream : Upstream = parseCommandLine();
		var version = upstream.changeLog.version.upstream;
		if( FileUtil.fileExists( "debian/changelog" ) )
		{
			// Make sure the changelog is readable.
			DebianChangeLog.parseCurrentChangeLog( logger );
		}
		// Put each tarball in its own branch.
		for( component in upstream.components )
		{
			// Check that the component has not already been imported.
			var name = component.component;
			var tag = Util.upstreamTag( upstream.changeLog, name );
			if( Mercurial.hasTag( tag, logger ) )
			{
				logger.info( 0, "Skipping " + tag + ": already imported." );
				continue;
			}
			xdeltaComponent( component, upstream );
		}
		// Create a composition of all the components in the upstream branch.
		// Main upstream tarball must be cleaned from excess directories, namely
		// the debian directory and each of the compnent directories.
		var excess = [];
		for( component in upstream.components )
		{
			var name = component.component;
			if( name == "." ) name = Constants.packageDirectory();
			excess.push( name );
		}		
		try
		{
			var out = Mercurial.switchToBranch( Constants.upstreamBranch(), logger );
			logger.infos( 3, out );
			// Delete everything except the Mercurial files.
			for( entry in sys.FileSystem.readDirectory( "." ) )
			{
				var mercurial = ~/^\.hg$/;
				if( ! mercurial.match( entry ) ) FileUtil.unlink( entry );
			}
			for( component in upstream.components )
			{
				var name = component.component;
				var branch = Util.upstreamTag( upstream.changeLog, name );
				var root = extractComponent( component );
				// TODO: cleanup if error.
				if( root == null ) throw "Extract " + name + " failed.";
				if( name == "." )
				{
					for( inway in excess )
					{
						if( sys.FileSystem.exists( inway ) )
						{
							logger.info
							(
								1,
								"Removing excess " + inway +
								" directory from upstream."
							);
							try { FileUtil.unlink( inway ); }
							catch( e : Dynamic )
							{
								throw "Removing " + inway + ": " + Std.string(e);
							}
						}
					}
				}
			}
			// Add and remove files from the combined components, but do a lot
			// to detect renames.
			var addremove = new Process
			(
				"hg", [ "addremove", "-s", Constants.similarity() ]
			);
			if( addremove.process.exitCode() != 0 )
			{
				Util.writeError( "addremove failed:" );
				logger.infos( 0, addremove.stdouterr() );
				throw "addremove failed.";
			}
			logger.infos( 2, addremove.stdouterr() );
			// Commit the combined component version.
			var msg = Constants.importOrig() + " imported " +
				upstream.changeLog.source +
				" version " + upstream.changeLog.version.upstream;
			var commit = new Process( "hg", [ "commit", "-m", msg ] );
			var output = Lambda.array( commit.stdouterr() );
			if( commit.process.exitCode() != 0 )
			{
				// commit will treat no change as an error.
				if( output.length < 1 || output[0] != "nothing changed" )
				{
					Util.writeError( "commit failed:" );
					logger.infos( 0, output );
					throw "commit failed.";
				}
			}
			logger.infos( 2, output );
		}
		catch( e : Dynamic )
		{
			logger.info( 0, Std.string( e ) );
			logger.info( 0, "Reverting upstream branch to state before import." );
			logger.infos( 2, Mercurial.revert( logger ) );
			logger.infos( 3, Mercurial.switchToBranch( Constants.mainBranch(), logger ) );
			Util.die( "Could not import tarballs" );				
		}
		logger.infos( 3, Mercurial.switchToBranch( Constants.mainBranch(), logger ) );
		if( upstream.noMerge )
		{
			logger.info
			(
				0,
				"Run hg merge " + Constants.upstreamBranch() +
				" to pull in new version."
			);
		}
		else
		{
			// Are there any Quilt-managed patches?  Then unapply the patches to
			// avoid getting merge conflicts.
			if( FileUtil.fileExists( ".pc/applied-patches" ) )
			{
				var output = new List<String>();		
				output.add( "Unapplying Quilt patches." );
				var unpatch = new Process( "quilt", [ "pop", "-a", "-q" ] );
				for( line in unpatch.stdouterr() ) output.add( line );
				if( unpatch.process.exitCode() != 0 )
				{
					logger.infos( 0, output );
					Util.writeError( "Could not unapply patches." );
					Util.writeError( "You have to manually unapply patches." );
					Util.die
					(
						"Then run hg merge " + Constants.upstreamBranch() +
						" to pull in new version."
					);
				}
				logger.infos( 1, output );
			}
			if( FileUtil.fileExists( "debian/changelog" ) )
			{
				createChangeLogEntry( version ); 
			}
 			// Merge the new upstream into the default branch.
			var merge = Mercurial.merge( Constants.upstreamBranch(), logger );
			for( line in merge )
			{
				// Filter out "(branch merge, don't forget to commit)".
				var dontForget = ~/don.t forget to commit/;
				if( ! dontForget.match( line ) ) logger.info( 1, line );
			}
			var msg = Constants.importOrig() + " merged upstream version "
				+ version;
			var commit = Mercurial.commit( msg, logger );
			logger.infos( 1, commit );
		}
	}

	//
	// Create a new *-1 entry in debian/changelog.
	//
	static function createChangeLogEntry( version : String )
	{
		// Chreate a new changelog entry.
		var fullVersion = version + "-1";
		var epoch = DebianChangeLog.parseCurrentChangeLog( logger ).version.epoch;
		if( epoch != null )
		{
			fullVersion = epoch + ":" + fullVersion;
		}
		var dch = Process.runButExitOnError
		(
			"dch",
			[
				"-v", fullVersion, Constants.importOrig() +
				" imported new upstream version."
			],
			logger
		);
		var format = "debian/source/format";
		if( FileUtil.fileExists( format ) )
		{
			var quilt = ~/3\.0 \(quilt\)/;
			if( quilt.match( sys.io.File.getContent( format ) ) )
			{
				logger.info( 0, "Remember to quilt push -a and sort out any problems." );
				logger.info( 0, "After that, commit everything including .pc directory." );
			}
		}		
	}

	//
	// Imports the component tarball into a branch named after the component and
	// commits the changes.  Then creates an xdelta file between original
	// tarball and what can be recreated from the component branch, and commits
	// the xdelta in the pristine branch.
	//
	static function xdeltaComponent( component : Component, upstream : Upstream )
	{
		var name = component.component;
		var branch = Util.componentBranch( upstream.changeLog.source, name );
		Mercurial.switchToBranch( branch, logger );
		try {
			var root = importComponent( component, upstream.changeLog );
			if( root == null ) throw "Could not import " + component.tarball;
			// Recreate a tarball from the component branch contents by
			// transforming the file names so they are rooted in the component
			// name.
			var tarballgz = haxe.io.Path.withoutDirectory( component.tarball );
			var tarball = tarballgz.substr( 0, tarballgz.lastIndexOf(".") );
			var recreated = "recreated-" + tarball;
			var tarBaseArgs = ["--mode=0", "--owner=0", "--group=0"];
			var tarArgs = tarBaseArgs.concat (["-cof", recreated ]);
			if( name == "." )
			{
				var escape = new EReg ("[%&]", "");
				var escapedRoot = escape.replace (root[0], "\\$1");
				tarArgs = tarArgs.concat
				([
					"--no-recursion",
					"--transform",
					"s%^.%" + escapedRoot + "%"
				]);
				tarArgs.push( "." );

				logger.info( 3, "Starting " + tarArgs.join(" ") );
				var pack = new Process( "tar", tarArgs );
				if( pack.process.exitCode() != 0 )
				{
					logger.infos( 1, pack.stdouterr() );
					throw "Could not recreate " + tarball;
				}

				// Now for the rest of it...
				tarArgs = tarBaseArgs.concat (
						  [ "-rof", recreated,
							"--transform",
							"s%^.%" + escapedRoot + "/&%"
						  ]);

				for( entry in sys.FileSystem.readDirectory( "." ) )
				{
					if( ! Constants.precious().match( entry ) )
					{
						tarArgs.push( entry );
					}
				}
			}
			else
			{
				tarArgs.push( root[1] );
			}
			logger.info( 3, "Starting " + tarArgs.join(" ") );
			var pack = new Process( "tar", tarArgs );
			if( pack.process.exitCode() != 0 )
			{
				logger.infos( 0, pack.stdouterr() );
				throw "Packing " + tarball + " failed.";
			}
			logger.infos( 3, pack.stdouterr() );
			// Create an uncompressed copy of the upstream tarball.
			if( ! Util.uncompress( component.tarball, tarball ) )
			{
				throw "Could not uncompress " + component.tarball;
			}
			// Create an xdelta file between uncompressed upstream tarball and
			// what was recreated from the component branch.			
			var args =
			[
				"delta", "-p", recreated, tarball,
				tarball + "." + Constants.xdeltaExtension()
			];
			logger.info( 3, "Starting xdelta " + args.join(" ") );
			var xdelta = new Process( "xdelta", args );
			var exit = xdelta.process.exitCode();
			if( exit != 1 && exit != 0 )
			{
				logger.infos( 0, xdelta.stdouterr() );
				throw "Could not calculate xdelta for pristine tarball.";
			}
			logger.infos( 2, xdelta.stdouterr() );
			// Create enough information to be able to compress the pristine
			// tarball so that it becomes identical the the original compressed
			// tarball.
			var compression = tarballgz.substr( tarballgz.lastIndexOf(".")+1 );
			var pristineInfo = tarball + "." + compression + "." +
				Constants.pristineExtension();
			if( ! Util.preparePristine( component.tarball, pristineInfo ) )
			{
				throw "Could not generate info for pristine tarball.";
			}
			// Cleanup and commit component branch.
			FileUtil.unlink( recreated );
			FileUtil.unlink( tarball );
			var componentName = name;
			if( name == "." ) componentName = upstream.changeLog.source;
			var msg = Constants.importOrig() + " imported " + componentName
				+ " version " + upstream.changeLog.version.upstream;
			var commit = new Process( "hg", [ "commit", "-m", msg ] );
			if( commit.process.exitCode() != 0 )
			{
				logger.infos( 0, commit.stdouterr() );
				throw "commit new component failed.";
			}
			logger.infos( 2, commit.stdouterr() );
			// Remember the revision number so pristinetar can get back to it.
			var identify = new Process( "hg", ["identify"] );
			var out = identify.stdouterr();
			if( identify.process.exitCode() != 0 || ! out.hasNext() )
			{
				logger.infos( 0, out );
				throw "hg identify failed.";
			}
			var revisionRegExp = ~/^([^\s]+)\s/i;
			if( ! revisionRegExp.match( out.next() ) )
			{
				throw "hg identify did not give a revision.";
			}
			var revision = revisionRegExp.matched( 1 );
			var revisionInfo = tarball + "." + Constants.revisionExtension();
			logger.info( 2, "Storing revision " + revision + " in " + revisionInfo );
			var revisionFile = sys.io.File.write( revisionInfo, false );
			revisionFile.writeString( revision );
			revisionFile.close();
			if (name == ".")
			{
				logger.info( 2, "Storing root directory name" );
				var dir = sys.io.File.write (tarball + "." + Constants.dirExtension(), false);
				dir.writeString (root[0]);
				dir.close ();
			}
			// Commit the xdelta, pristine-tar, and revision in pristine branch.
			Mercurial.switchToBranch( Constants.pristineBranch(), logger );
			var msg = Constants.importOrig() + " stored xdelta for " + tarball;
			var commit = Process.runButExitOnError
			(
				"hg", [ "commit", "--addremove", "-m", msg ], logger
			);
			// Tag the upstream branch with the successful import.
			Mercurial.switchToBranch( Constants.upstreamBranch(), logger );
			var tag = Util.upstreamTag( upstream.changeLog, name );
			var msg = Constants.importOrig() + " added tag " + tag;
			var tagger = Mercurial.tag( tag, msg, logger );
			logger.infos( 2, tagger );
		}
		catch( e : Dynamic )
		{
			Util.writeError( Std.string( e ) );
			logger.info( 0, "Reverting branch " + branch + " to state before import." );
			logger.infos( 2, Mercurial.revert( logger ) );
			logger.infos( 3, Mercurial.switchToBranch( Constants.mainBranch(), logger ) );
			Sys.exit( 1 );			
		}
	}
	
	//
	// Imports the component tarball into current branch and prepares for
	// committing the changes.  Returns the names of the original and real
	// root directories in the component tarball on successful import, or
	// null if something went wrong (in which case the caller is expected to
	// revert the changes).
	//
	static function importComponent
	(
		component : Component,
		changeLog : DebianChangeLog
	)
	: Array<String>
	{
		var name = component.component;
		// Delete everything except the Mercurial files.
		for( entry in sys.FileSystem.readDirectory( "." ) )
		{
			var mercurial = ~/^\.hg$/;
			if( ! mercurial.match( entry ) ) FileUtil.unlink( entry );
		}
		var root = extractComponent( component );
		if( root == null ) return null;
		// Add and remove files from new component version, but do a lot to
		// detect renames.
		var addremove = new Process
		(
			"hg", [ "addremove", "-s", Constants.similarity() ]
		);
		if( addremove.process.exitCode() != 0 )
		{
			Util.writeError( "addremove failed:" );
			Lambda.iter( addremove.stdouterr(), logger.error );
			return null;
		}
		logger.infos( 1, addremove.stdouterr() );
		return root;
	}

	//
	// Extracts a component tarball into CWD.  The tarball needs to have a
	// single root directory.  The root directory is simply ignored if the
	// component is ".".  Returns the names of the original and real root
	// directories in the component tarball on successful extraction, or
	// null if something went wrong.
	//
	static function extractComponent( comp : Component ) : Array<String>
	{
		var component = comp.component;
		var tarball = comp.tarball;
		logger.info( 1, "Unpacking " + tarball );
		// Unpack tarballs into a temporary directory.
		var tmpDir = ",,importorig-" + Sys.time();
		try { sys.FileSystem.createDirectory( tmpDir ); }
		catch( e : Dynamic )
		{
			Util.writeError( "Creating " + tmpDir + ": " + Std.string( e ) );
			return null;
		}
		if( ! Util.extract( tarball, tmpDir ) )
		{
			FileUtil.unlink( tmpDir );
			return null;
		}
		// Delete empty directories because they are not tracked by Mercurial.
// FIXME/BUG: empty directories may be present in the tarball - omit them and we can not re-create the tarball!
//		logger.info( 2, "Pruning empty directories from " + tarball );
//		FileUtil.prune( tmpDir );
		// The tarballs are required (by dpkg-source) to behave nicely and
		// expand into one single directory, the contents of which are moved to
		// a directory with the component name, except if the component name is
		// ".".
		var entries = sys.FileSystem.readDirectory( tmpDir );
		if( entries.length != 1 || ! sys.FileSystem.isDirectory( tmpDir + "/" + entries[0] ) )
		{
			FileUtil.unlink( tmpDir );
			Util.writeError
			(
				tarball + " must contain a single root directory."
			);
			return null;
		}
		// Move the contents into the right place in the branch.
		var root = entries[0];
		var sourceDir = tmpDir + "/" + root;
		var target = component;
		if( target != "." )
		{
			try { sys.FileSystem.createDirectory( target ); }
			catch( e : Dynamic )
			{
				Util.writeError( "Creating " + target + ": " + Std.string( e ) );
				return null;
			}
		}
		logger.info( 2, "Moving contents of " + root + " to " + target );
		entries = sys.FileSystem.readDirectory( sourceDir );
		for( entry in entries )
		{
			// Ignore files that would interfere with mercurial-buildpackage or
			// dpkg-source.
			if( ! Constants.interfere().match( entry ) )
			{
				FileUtil.copy( sourceDir + "/" + entry, target );
			}
		}
		FileUtil.unlink( tmpDir );
		return [root, target];
	}

	//
	// Returns a structure containing the parsed command-line arguments.
	//
	static function parseCommandLine() : Upstream
	{
		var usage = "Usage: " + Constants.importOrig() + " [-V|--version]" +
		" [-v|--verbose] [-n|--no-merge] maintarball [componenttarball...]";
		var options = new GetPot( Sys.args() );
		Util.maybeReportVersion( options, usage );
		// Collect verbosity options.
		while( options.got( ["--verbose","-v"] ) ) ++logger.verbosity;
		var noMerge = options.got( ["-n","--no-merge"] );
		// Reject other options.
		Util.exitOnExtraneousOptions( options, usage );
		// Get the file name of the tarball(s).
		var origs = new Array<String>();
		var orig = options.unprocessed();
		while( orig != null )
		{
			origs.push( orig );
			orig = options.unprocessed();			
		}
		// There must be at least one tarball.
		if( origs.length == 0 ) Util.die( usage );
		for( tarball in origs )
		{
			if( ! FileUtil.fileExists( tarball ) )
			{
				Util.die( "Tarball " + tarball + " does not exist." );
			}
		}
		// The names of tarballs must follow Debian conventions of dpkg format
		// 3.0 (quilt).
		var ballRegExp = ~/([^\/]+)_(.+)\.orig\.tar\.(bz2|lzma|xz|gz)$/;
		if( ! ballRegExp.match( origs[0] ) )
		{
			Util.die( "Filename of main tarball must follow the pattern defined by dpkg format 3.0." );
		}
		var packageName = ballRegExp.matched( 1 );
		var version = ballRegExp.matched( 2 );
		logger.info( 2, "Package " + packageName + " version " + version );
		// Only format 3.0 allows multiple tarballs, and format 1.0 only accepts
		// gzip compression.
		if( ballRegExp.matched( 3 ) != "gz" )
		{
			logger.info( 2, "Assuming dpkg format 3.0" );			
		}
		// Collect tarballs together with their component names.  The main
		// tarball has a component name ".".
		var components = new Array<Component>();
		components.push( { component: ".", tarball: origs[0] } );
		// Process additional tarball components, if any.
		var componentRegExp =
		~/([^\/]+)_(.*)\.orig-([a-z0-9][-a-z0-9]*)\.tar\.(bz2|lzma|xz|gz)$/i;
		for( i in 1...origs.length )
		{
			if( ! componentRegExp.match( origs[i] ) )
			{
				Util.die( "Filenames of component tarballs must follow the pattern defined by dpkg format 3.0 (quilt)." );
			}
			if( componentRegExp.matched( 1 ) != packageName )
			{
				Util.die
				(
					origs[0] + " and " + origs[i] +
					" do not agree on package name."
				);
			}
			if( componentRegExp.matched( 1 ) != packageName )
			{
				Util.die
				(
					origs[0] + " and " + origs[i] +
					" do not agree on package name."
				);
			}
			if( componentRegExp.matched( 2 ) != version )
			{
				Util.die
				(
					origs[0] + " and " + origs[i] +
					" do not agree on upstream version."
				);
			}
			components.push
			(
				{ component: componentRegExp.matched( 3 ), tarball: origs[i] }
			);
		}
		// Reject other arguments.
		Util.exitOnExtraneousArguments( options, usage );
		return
		{
			components: components,
			changeLog: new DebianChangeLog( packageName,version ),
			noMerge: noMerge
		};
	}

	static var logger : Script;
}

