/* -*-Mode: C++;-*-
 * PRCS - The Project Revision Control System
 * Copyright (C) 1997  Josh MacDonald
 *
 * 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.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: diff.cc 1.20.1.19.1.3.1.6.1.22 Sun, 17 Sep 2000 17:56:26 -0700 jmacd $
 */

#include "prcs.h"
#include "hash.h"
#include "misc.h"
#include "repository.h"
#include "projdesc.h"
#include "checkin.h"
#include "checkout.h"
#include "vc.h"
#include "fileent.h"
#include "diff.h"
#include "syscmd.h"
#include "setkeys.h"
#include "system.h"
#include "memseg.h"
#include "rebuild.h"


static PrPrcsExitStatusError diff_working_files();
static PrPrcsExitStatusError diff_named_versions();
static PrBoolError diff_similar_files(ProjectDescriptor* toP, ProjectDescriptor* fromP);
static PrBoolError diff_missing_files(ProjectDescriptor* toP, ProjectDescriptor* fromP);
static PrBoolError diff_symlink_pair(FileEntry*, FileEntry*);
static PrBoolError diff_old_new_file(FileEntry*, ProjectDescriptor* fromP, bool new_file);
static PrVoidError prepare_diff_workingfile(FileEntry *fe);
static PrIntError try_optimizations(FileEntry *tofe, FileEntry *fromfe);
static PrVoidError prepare_diff_versionfile(FileEntry *fe, bool use_working);
static PrBoolError diff_project_files(ProjectDescriptor* toP, ProjectDescriptor* fromP);
static PrBoolError diff_two_files(const char* label1, const char* label2,
				  const char* file1, const char* file2,
				  const char* index, const char* from_project,
				  SystemCommand *diff_command);
static PrVoidError get_info(FileEntry* fe, RepEntry* rep_entry);

static bool use_working_to_file = true;
/* Determined by scanning the diff options, given by -q or --brief,
 * and tells PRCS to fake the diff when it can. */
static bool diff_option_brief = false;

#define temp_file_from temp_file_1
#define temp_file_to temp_file_2

PrPrcsExitStatusError diff_command()
{
    kill_prefix(prcsoutput);

    /* This is sort of ugly. */
    for(int i = 0; i < cmd_diff_options_count; i += 1)
	if(strcmp("-q", cmd_diff_options_given[i]) == 0 ||
	   strcmp("--brief", cmd_diff_options_given[i]) == 0) {
	    diff_option_brief = true;
	}

    if(cmd_alt_version_specifier_major == NULL)
	return diff_working_files();
    else
	return diff_named_versions();
}

/* reports the differences from VERSION to WORKING files */
static PrPrcsExitStatusError diff_working_files()
{
    ProjectDescriptor *from, *to;
    ProjectVersionData *from_version;
    bool diffs = false, diff = false;

    use_working_to_file = true;

    if(!fs_file_readable(cmd_root_project_file_path))
	pthrow prcserror << "Can't open file " << squote(cmd_root_project_file_path) << perror;

    /* Set up the project file diff */
    const char* abs_path;

    Return_if_fail(abs_path << absolute_path(cmd_root_project_file_path));

    If_fail(Err_symlink(abs_path, temp_file_to))
	pthrow prcserror << "Failed creating symlink " << squote(temp_file_to) << perror;

    Return_if_fail(to << read_project_file(cmd_root_project_full_name, temp_file_to, true, KeepNothing));

    Return_if_fail(to->init_repository_entry(cmd_root_project_name, false, false));

    eliminate_unnamed_files(to);

    Return_if_fail(from_version << resolve_version(cmd_version_specifier_major,
						   cmd_version_specifier_minor,
						   cmd_root_project_full_name,
						   cmd_root_project_file_path,
						   to,
						   to->repository_entry()));

    if(from_version->prcs_minor_int() == 0) {
	Return_if_fail(from << checkout_create_empty_prj_file(temp_file_from,
							      cmd_root_project_full_name,
							      from_version->prcs_major(),
							      KeepNothing));
    } else {
	Return_if_fail(from << to->repository_entry() ->
		       checkout_create_prj_file(temp_file_from,
						cmd_root_project_full_name,
						from_version->rcs_version(),
						KeepNothing));
    }

    eliminate_unnamed_files(from);

    from->repository_entry(to->repository_entry());

    prcsinfo << "Producing diffs from " << from->full_version() << " to "
	     << to->full_version() << dotendl;

    Return_if_fail(eliminate_working_files(to, NoQueryUserRemoveFromCommandLine));

    to->read_quick_elim();

    to->quick_elim_unmodified();

    if(cmd_prj_given_as_file)
	Return_if_fail(diff << diff_project_files(to, from));

    diffs |= diff;

    Return_if_fail(diff << diff_similar_files(to, from));

    diffs |= diff;

    Return_if_fail(diff << diff_missing_files(to, from));

    diffs |= diff;

    Return_if_fail(warn_unused_files(false));

    if(diffs)
	return ExitDiffs;
    else
	return ExitNoDiffs;
}

/* reports the differences from VERSION1 to VERSION2 */
static PrPrcsExitStatusError diff_named_versions()
{
    ProjectDescriptor *from, *to;
    ProjectVersionData *to_version, *from_version;
    RepEntry *rep_entry;
    bool diffs = false, diff = false;

    use_working_to_file = false;

    Return_if_fail(rep_entry << Rep_init_repository_entry(cmd_root_project_name,
							  false, false, true));

    /* Get the from version */
    Return_if_fail(from_version << resolve_version(cmd_version_specifier_major,
						   cmd_version_specifier_minor,
						   cmd_root_project_full_name,
						   cmd_root_project_file_path,
						   NULL,
						   rep_entry));

    if(from_version->prcs_minor_int() == 0) {
	Return_if_fail(from << checkout_create_empty_prj_file(temp_file_from,
							      cmd_root_project_full_name,
							      from_version->prcs_major(),
							      KeepNothing));
    } else {
	Return_if_fail(from << rep_entry ->
		       checkout_create_prj_file(temp_file_from,
						cmd_root_project_full_name,
						from_version->rcs_version(),
						KeepNothing));
    }

    eliminate_unnamed_files(from);

    /* Get the to version */
    Return_if_fail(to_version << resolve_version(cmd_alt_version_specifier_major,
						 cmd_alt_version_specifier_minor,
						 cmd_root_project_full_name,
						 cmd_root_project_file_path,
						 NULL,
						 rep_entry));

    if(to_version->prcs_minor_int() == 0) {
	Return_if_fail(to << checkout_create_empty_prj_file(temp_file_to,
							    cmd_root_project_full_name,
							    to_version->prcs_major(),
							    KeepNothing));
    } else {
	Return_if_fail(to << rep_entry ->
		       checkout_create_prj_file(temp_file_to,
						cmd_root_project_full_name,
						to_version->rcs_version(),
						KeepNothing));
    }

    eliminate_unnamed_files(to);

    to->repository_entry(rep_entry);
    from->repository_entry(rep_entry);

    /* Diff */

    prcsinfo << "Producing diffs from " << from->full_version() << " to "
	     << to->full_version() << dotendl;

    if(cmd_prj_given_as_file)
	Return_if_fail(diff << diff_project_files(to, from));

    diffs |= diff;

    Return_if_fail(diff << diff_similar_files(to, from));

    diffs |= diff;

    Return_if_fail(diff << diff_missing_files(to, from));

    diffs |= diff;

    Return_if_fail(warn_unused_files(false));

    if(diffs)
	return ExitDiffs;
    else
	return ExitNoDiffs;
}

static PrVoidError get_info(FileEntry* fe, RepEntry* rep_entry)
{
    if(use_working_to_file)
	return fe->get_file_sys_info();
    else
	return fe->get_repository_info(rep_entry);
}

/*
 * prepare_diff_workingfile --
 *
 *     places either a copy of a keywords unset working file or a symlink
 *     to a working file into temp_file_to of the given file entry.
 */
static PrVoidError prepare_diff_workingfile(FileEntry *fe)
{
    If_fail(Err_unlink(temp_file_to)) {
	pthrow prcserror << "Unlink " << squote(temp_file_to) << " failed" << perror;
    }

    if (fe->file_type() == RealFile) {

	Return_if_fail(fe->get_file_sys_info());

	if (fe->keyword_sub() && !option_diff_keywords) {
	    Return_if_fail (setkeys(fe->working_path(),
				    temp_file_to,
				    fe,
				    Unsetkeys));
	} else {
	    const char* abs_path;

	    Return_if_fail(abs_path << absolute_path(fe->working_path()));

	    If_fail (Err_symlink(abs_path, temp_file_to)) {
		pthrow prcserror << "Error creating symbolic link"
				<< squote(temp_file_to) << perror;
	    }
	}
    }

    return NoError;
}

#define DIFFS   3 /* first bit says that optimizations succeeded and to skip this file */
#define NODIFFS 1 /* second bit says whether there were differences */

static PrIntError try_optimizations(FileEntry *tofe, FileEntry *fromfe)
{
    static MemorySegment setkey_buf(false);
    RcsVersionData *to_version_data, *from_version_data;
    bool need_to_replace_to_keywords = option_diff_keywords && tofe->keyword_sub();
    bool need_to_replace_from_keywords = option_diff_keywords && fromfe->keyword_sub();
    bool same_descriptor;

    if(!tofe->descriptor_name()) /* empty descriptor for working file */
	return 0;

    same_descriptor = (strcmp(tofe->descriptor_name(),
			      fromfe->descriptor_name()) == 0 &&
		       strcmp(tofe->descriptor_version_number(),
			      fromfe->descriptor_version_number()) == 0);

    if(!use_working_to_file && !need_to_replace_to_keywords &&
       !need_to_replace_from_keywords && same_descriptor)
	return NODIFFS;

    Return_if_fail(to_version_data << tofe->project()->repository_entry() ->
		   lookup_rcs_file_data(tofe->descriptor_name(),
					tofe->descriptor_version_number()));

    Return_if_fail(from_version_data << fromfe->project()->repository_entry() ->
		   lookup_rcs_file_data(fromfe->descriptor_name(),
					fromfe->descriptor_version_number()));

    /* If brief diff, compare checksums if possible */
    if(diff_option_brief && !use_working_to_file &&
       !need_to_replace_to_keywords && !need_to_replace_from_keywords) {

	if(memcmp(to_version_data->unkeyed_checksum(),
		  from_version_data->unkeyed_checksum(), 16) == 0) {
	    return NODIFFS;
	} else {
	    prcsoutput << "The file " << diff_tuple(fromfe, tofe) << " differs" << prcsendl;
	    return DIFFS;
	}
    }

    if(use_working_to_file && same_descriptor && !need_to_replace_to_keywords) {
	/* If the working file is unmodifed by quick_elim */
	if(tofe->unmodified())
	    return NODIFFS;
    }

    if(use_working_to_file && !need_to_replace_to_keywords &&
       !need_to_replace_from_keywords) {

	if(tofe->keyword_sub()) {
	    Return_if_fail(setkeys_outbuf(tofe->working_path(),
					  &setkey_buf,
					  tofe,
					  Unsetkeys));
	} else {
	    Return_if_fail(setkey_buf.map_file(tofe->working_path()));
	}

	if(setkey_buf.length() == from_version_data->length()) {
	    const char* checksum;

	    checksum = md5_buffer(setkey_buf.segment(), setkey_buf.length());

	    if(memcmp(checksum, from_version_data->unkeyed_checksum(), 16) == 0)
		return NODIFFS;
	}

	/* Checksums differ, if brief diff */
	if(diff_option_brief) {
	    prcsoutput << "The file " << diff_tuple(fromfe, tofe) << " differs" << prcsendl;
	    return DIFFS;
	}
    }

    return 0;
}

/*
 * prepare_diff_versionfile --
 *
 *     writes the file onto either temp_file_from or temp_file_to and
 *     maybe replaces keywords
 */
static PrVoidError prepare_diff_versionfile(FileEntry *fe, bool use_temp_file_from)
{
    const char* filename;

    if(use_temp_file_from)
	filename = temp_file_from;
    else
	filename = temp_file_to;

    Return_if_fail(fe->initialize_descriptor(fe->project()->repository_entry(), false, false));

    Return_if_fail(fe->get_repository_info(fe->project()->repository_entry()));

    if (fe->keyword_sub() && option_diff_keywords) {
	FILE* cofile;

	Return_if_fail(cofile << VC_checkout_stream(fe->descriptor_version_number(),
						    fe->full_descriptor_name()));

	// Blah

	Dstring ds;
	fe->describe (ds);

	RcsVersionData *fe_data;
	Return_if_fail(fe_data << fe->project()->repository_entry() ->
		       lookup_rcs_file_data(fe->descriptor_name(),
					    fe->descriptor_version_number()));

	Return_if_fail(setkeys_infile(ds.cast (), fileno(cofile), fe_data->length (), filename, fe, Setkeys));

	Return_if_fail(VC_close_checkout_stream(cofile,
						fe->descriptor_version_number(),
						fe->full_descriptor_name()));

    } else {
	Return_if_fail(VC_checkout_file(filename,
					fe->descriptor_version_number(),
					fe->full_descriptor_name()));
    }

    return NoError;
}

static PrBoolError diff_similar_files(ProjectDescriptor* toP,
				      ProjectDescriptor* fromP)
{
    FileEntry *tofe, *fromfe;
    bool alldiffs = false, onediff = false;

    foreach_fileent(fe_ptr, toP) {
	tofe = *fe_ptr;

	if(!tofe->on_command_line())
	    continue;

	toP->repository_entry()->Rep_clear_compressed_cache();
	fromP->repository_entry()->Rep_clear_compressed_cache();

	Return_if_fail (fromfe << fromP->match_fileent(tofe));

	if(fromfe) {

	    if(fromfe->file_type() == tofe->file_type()) {
		if(fromfe->file_type() == RealFile) {
		    int no_diffs;

		    /* If they are the same by checksum */
		    Return_if_fail(no_diffs << try_optimizations(tofe, fromfe));

		    if(no_diffs & 1) {
			if(no_diffs & 2) {
			    alldiffs = true;
			    DEBUG("Opt succeeds for " << diff_tuple(tofe, fromfe) << ", diffs");
			} else {
			    DEBUG("Opt succeeds for " << diff_tuple(tofe, fromfe) << ", no diffs");
			}

			continue;
		    }

		    if(use_working_to_file) {
			Return_if_fail(prepare_diff_workingfile(tofe));
		    } else {
			Return_if_fail(prepare_diff_versionfile(tofe, false));
		    }

		    Return_if_fail(prepare_diff_versionfile(fromfe, true));

		    Return_if_fail(onediff << diff_pair(fromfe,
							tofe,
							temp_file_from,
							temp_file_to));

		} else if(fromfe->file_type() == SymLink) {
		    Return_if_fail(tofe->initialize_descriptor(toP->repository_entry(),
							       false, false));

		    Return_if_fail(fromfe->initialize_descriptor(fromP->repository_entry(),
								 false, false));

		    Return_if_fail(onediff << diff_symlink_pair(tofe, fromfe));
		}
		/* directories ignored */
	    } else {
		prcsoutput << "Changes type from type " << format_type(fromfe->file_type())
			   << " to " << format_type(tofe->file_type())
			   << ": " << diff_tuple(fromfe, tofe) << prcsendl;

		onediff = true;
	    }

	    alldiffs |= onediff;
	}
    }

    return alldiffs;
}

static PrBoolError diff_missing_files(ProjectDescriptor* toP,
				      ProjectDescriptor* fromP)
{
    bool alldiffs = false;
    FileEntry *fe;

    foreach_fileent(fe_ptr, toP) {
	fe = *fe_ptr;

	toP->repository_entry()->Rep_clear_compressed_cache();
	fromP->repository_entry()->Rep_clear_compressed_cache();

	if(!fe->real_on_cmd_line())
	    continue;

	FileEntry *bogus;

	Return_if_fail (bogus << fromP->match_fileent(fe));

	if(!bogus) {
	    Return_if_fail(fe->initialize_descriptor(toP->repository_entry(),
						     false, false));

	    Return_if_fail(get_info(fe, toP->repository_entry()));

	    if(option_diff_new_file && fe->file_type() == RealFile && !diff_option_brief) {
		if(use_working_to_file)
		    Return_if_fail(prepare_diff_workingfile(fe));
		else
		    Return_if_fail(prepare_diff_versionfile(fe, false));

		diff_old_new_file (fe, fromP, true);
	    } else {
		prcsoutput << "Only in " << toP->full_version() << ": " << fe->working_path() << prcsendl;
	    }
	    alldiffs = true;
	}
    }

    foreach_fileent(fe_ptr2, fromP) {
	fe = *fe_ptr2;

	if(!fe->real_on_cmd_line())
	    continue;

	FileEntry *bogus;

	Return_if_fail (bogus << toP->match_fileent(fe));

	if(!bogus) {
	    if(option_diff_new_file && fe->file_type() == RealFile && !diff_option_brief) {
		Return_if_fail(prepare_diff_versionfile(fe, false));
		diff_old_new_file (fe, toP, false);
	    } else {
		prcsoutput << "Only in " << fromP->full_version() << ": " << fe->working_path() << prcsendl;
	    }
	    alldiffs = true;
	}
    }

    return alldiffs;
}

static PrBoolError diff_symlink_pair(FileEntry* to, FileEntry* from)
{
    const char *link1, *link2;

    link1 = from->link_name();

    if (use_working_to_file) {
        Return_if_fail (link2 << read_sym_link (from->working_path()));
    } else {
        link2 = to->link_name();
    }

    if(!pathname_equal(link1, link2)) {
	prcsoutput << "Symlink " << diff_tuple(from, to)
		   << " changes from " << squote(link1)
		   << " to " << squote(link2) << prcsendl;
	return true;
    } else {
	return false;
    }
}

static PrBoolError diff_old_new_file(FileEntry *to, ProjectDescriptor* fromP, bool new_file)
{
    Dstring label1, label2;
    Dstring filedes;

    if(!to->descriptor_name()) {
	filedes.assign("()");
    } else {
	filedes.sprintf("(%s/%s %s %03o)",
			to->project()->project_name(),
			to->descriptor_name(),
			to->descriptor_version_number(),
			to->file_mode());
    }

    label1.sprintf("-L%s/%s %s %s ()",
		   fromP->full_version(),
		   to->working_path(),
		   get_utc_time(),
		   get_login());

    label2.sprintf("-L%s/%s %s %s %s",
		   to->project()->full_version(),
		   to->working_path(),
		   to->last_mod_date(),
		   to->last_mod_user(),
		   filedes.cast());

    SystemCommand *cmd = NULL;

    if (to->file_attrs()->diff_tool())
	cmd = sys_cmd_by_name (to->file_attrs()->diff_tool());

    if (new_file)
	return diff_two_files(label1, label2, "/dev/null", temp_file_to, to->working_path(), fromP->full_version(), cmd);
    else
	return diff_two_files(label2, label1, temp_file_to, "/dev/null", to->working_path(), fromP->full_version(), cmd);
}

void format_diff_label(FileEntry* fe, const char* name, Dstring* ds)
{
    Dstring desc;

    const char* date, *user;

    if(fe == NULL) {
	date = get_utc_time();
	user = get_login();
    } else {
	date = fe->last_mod_date();
	user = fe->last_mod_user();
    }

    if(!fe->descriptor_name()) {
	desc.assign("()");
    } else {
	desc.sprintf("(%s/%s %s %03o)",
		     fe->project()->project_name(),
		     fe->descriptor_name(),
		     fe->descriptor_version_number(),
		     fe->file_mode());
    }

    ds->sprintf("-L%s/%s %s %s %s", fe->project()->full_version(), name, date, user, desc.cast());
}

PrBoolError diff_pair(FileEntry* from,
		      FileEntry* to,
		      const char* from_file,
		      const char* to_file)
{
    Dstring label1, label2;

    format_diff_label(from, from->working_path(), &label1);
    format_diff_label(to,   to->working_path(), &label2);

    SystemCommand *cmd = NULL;

    if (to->file_attrs()->diff_tool())
	cmd = sys_cmd_by_name (to->file_attrs()->diff_tool());

    if (!cmd && from->file_attrs()->diff_tool())
	cmd = sys_cmd_by_name (from->file_attrs()->diff_tool());

    return diff_two_files(label1, label2, from_file, to_file, to->working_path(), from->project()->full_version(), cmd);
}

static PrBoolError diff_two_files(const char* label1, const char* label2,
				  const char* file1, const char* file2,
				  const char* index, const char* version,
				  SystemCommand *cmd)
{
    ArgList *args;
    char buf[512];
    FILE* output;
    bool n_index_written = true;
    int c, ret;

    if (cmd)
	Return_if_fail(args << cmd->new_arg_list());
    else
	Return_if_fail(args << gdiff_command.new_arg_list());

    for(int i = 0; i < cmd_diff_options_count; i += 1)
	args->append(cmd_diff_options_given[i]);

   if (!cmd) {
	args->append("-a");
	args->append(label1);
	args->append(file1);
	args->append(label2);
	args->append(file2);

	Return_if_fail(gdiff_command.open(true, false));

	output = gdiff_command.standard_out();
    } else {
	args->append(label1 + 2);
	args->append(file1);
	args->append(label2 + 2);
	args->append(file2);

	Return_if_fail(cmd->open(true, false));

	output = cmd->standard_out();
    }

    for (;;) {
        If_fail (c << Err_fread(buf, 512, output))
	  pthrow prcserror << "Error reading from diff output pipe" << perror;

	if (c == 0)
	  break;

	if(n_index_written) {
	    /* this sorta depends on diffs output, a leading Files
	     * means its in --brief mode, lets filter the filenames */
	    if(strncmp(buf, "Files ", 6) == 0) {
		prcsoutput << "The file " << squote(index) << " differs" << prcsendl;
		break;
	    }
	    prcsoutput << "Index: " << version << "/" << index << prcsendl;
	    n_index_written = false;
	}
	If_fail (Err_fwrite(buf, c, stdout))
	  pthrow prcserror << "Error writing diff output" << perror;
    }

    if (cmd)
	Return_if_fail(ret << cmd->close());
    else
	Return_if_fail(ret << gdiff_command.close());

    if(ret > 1)
	pthrow prcserror << "Diff command exited abnormally on files "
			 << squote(file1) << " and " << squote(file2) << dotendl;

    return (bool)ret;
}

static PrBoolError diff_project_files(ProjectDescriptor* toP, ProjectDescriptor* fromP)
{
    Dstring label1, label2;

    label1.sprintf("-L%s/%s", fromP->full_version(), fromP->project_file_path());
    label2.sprintf("-L%s/%s", toP->full_version(), toP->project_file_path());

    return diff_two_files(label1,
			  label2,
			  temp_file_from,
			  temp_file_to,
			  toP->project_file_path(),
			  fromP->full_version(),
			  NULL /* @@@ */);
}


syntax highlighted by Code2HTML, v. 0.9.1