/*
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */
/* ====================================================================
 * Copyright (c) 2003-2006, Martin Hauner
 *                          http://subcommander.tigris.org
 *
 * Subcommander is licensed as described in the file doc/COPYING, which
 * you should have received as part of this distribution.
 * ====================================================================
 */

// sc
#include "config.h"
#include "VisualDiff.h"
#include "util/CommandArgs.h"

// svn
#include <svn_client.h>
#include <svn_path.h>
#include <svn_ra.h>
#include <svn_wc.h>


namespace svn
{


static svn_error_t* getRevNumber( svn_revnum_t *revnum, svn_ra_session_t* session,
  const svn_opt_revision_t *revision, const char *path, apr_pool_t* pool )
{
  // svn_client__get_revision_number

  /* sanity checks */

  if( session == NULL
      && (   (revision->kind == svn_opt_revision_date)
          || (revision->kind == svn_opt_revision_head) )
    )
    {
      return svn_error_create( SVN_ERR_CLIENT_RA_ACCESS_REQUIRED, NULL, NULL );
    }

  if( path == NULL )
    return svn_error_create( SVN_ERR_CLIENT_VERSIONED_PATH_REQUIRED, NULL, NULL );


  if( revision->kind == svn_opt_revision_number )
    *revnum = revision->value.number;

  else if( revision->kind == svn_opt_revision_date )
    SVN_ERR( svn_ra_get_dated_revision( session, revnum, revision->value.date, pool) );

  else if( revision->kind == svn_opt_revision_head )
    SVN_ERR( svn_ra_get_latest_revnum( session, revnum, pool ) );

  else if (revision->kind == svn_opt_revision_unspecified)
    *revnum = SVN_INVALID_REVNUM;

  else if( (revision->kind == svn_opt_revision_committed)
    || (revision->kind == svn_opt_revision_working)
    || (revision->kind == svn_opt_revision_base)
    || (revision->kind == svn_opt_revision_previous) )
    {
      svn_wc_adm_access_t*  access;
      const svn_wc_entry_t* entry;

      SVN_ERR( svn_wc_adm_probe_open3( &access, NULL, path, FALSE, 0, NULL, NULL, pool ) );
      SVN_ERR( svn_wc_entry( &entry, path, access, FALSE, pool) );
      SVN_ERR( svn_wc_adm_close(access) );

      if(! entry)
        return svn_error_createf( SVN_ERR_UNVERSIONED_RESOURCE, NULL,
	        "'%s' is not under version control", svn_path_local_style(path, pool) );
      
      if(  (revision->kind == svn_opt_revision_base)
        || (revision->kind == svn_opt_revision_working) )
        *revnum = entry->revision;
      else
      {
        *revnum = entry->cmt_rev;

        if( revision->kind == svn_opt_revision_previous )
          (*revnum)--;
      }
    }
  else
  {
    return svn_error_createf( SVN_ERR_CLIENT_BAD_REVISION, NULL,
       "Unrecognized revision type requested for '%s'", svn_path_local_style( path, pool ) );
  }

  return SVN_NO_ERROR;
}

///////////////////////////////////////////////////////////////////////////////

VisualDiff::VisualDiff( svn_client_ctx_t* context, const sc::String& diffCmd,
  apr_pool_t* pool )
: _pool(pool), _context(context), _diffCmd(diffCmd)
{
}

VisualDiff::~VisualDiff()
{
  if( ! _removePath1.isEmpty() )
  {
    svn_error_t* svnerr = svn_io_remove_file( _removePath1, _pool );

    if( svnerr != SVN_NO_ERROR )
    {
      // todo
    }
  }


  if( ! _removePath2.isEmpty() )
  {
    svn_error_t* svnerr = svn_io_remove_file( _removePath2, _pool );

    if( svnerr != SVN_NO_ERROR )
    {
      // todo
    }
  }
}

svn_error_t* VisualDiff::run( const char* path1, const svn_opt_revision_t*
  revision1, const char* path2, const svn_opt_revision_t* revision2 )
{
  svn_opt_revision_t peg;
  peg.kind = svn_opt_revision_unspecified;

  bool isRepos1 = svn_path_is_url(path1) == TRUE;
  bool isRepos2 = svn_path_is_url(path2) == TRUE;

  if(  (revision1->kind == svn_opt_revision_unspecified)
    || (revision2->kind == svn_opt_revision_unspecified)
    )
    return svn_error_create( SVN_ERR_CLIENT_BAD_REVISION, NULL,
      "Not all required revisions are specified");

  bool isLocal1 = ((revision1->kind == svn_opt_revision_base)
                 || (revision1->kind == svn_opt_revision_working));
  bool isLocal2 = ((revision2->kind == svn_opt_revision_base)
                 || (revision2->kind == svn_opt_revision_working));

  if ((! isRepos1) && (! isLocal1))
    isRepos1 = true;
  if ((! isRepos2) && (! isLocal2))
    isRepos2 = true;

  if( isRepos1 )
  {
    if( isRepos2 )
    {
      // repository <-> repository
      return diffRpRp( path1, revision1, path2, revision2, &peg );
    }
    else
    {
      // repository <-> working
      return diffRpWc( path1, revision1, path2, revision2, &peg, true );
    }
  }
  else
  {
    if( isRepos2 )
    {
      // working <-> repository
      return diffRpWc( path2, revision2, path1, revision1, &peg, false );
    }
    else
    {
      // working <-> working
      return diffWcWc( path1, revision1, path2, revision2 );
    }
  }
  //return SVN_NO_ERROR;
}



svn_error_t* VisualDiff::run( const char* path, const svn_opt_revision_t*
  revision1, const svn_opt_revision_t* revision2, const svn_opt_revision_t*
  peg )
{
  if(  (revision1->kind == svn_opt_revision_unspecified)
    || (revision2->kind == svn_opt_revision_unspecified)
    )
    return svn_error_create( SVN_ERR_CLIENT_BAD_REVISION, NULL,
      "Not all required revisions are specified" );

  bool isLocal1 = ((revision1->kind == svn_opt_revision_base)
                 || (revision1->kind == svn_opt_revision_working));
  bool isLocal2 = ((revision2->kind == svn_opt_revision_base)
                 || (revision2->kind == svn_opt_revision_working));

  if( isLocal1 && isLocal2 )
    return svn_error_create( SVN_ERR_CLIENT_BAD_REVISION, NULL,
      "At least one revision must be non-local for a pegged diff" );

  if( ! isLocal1 )
  {
    if( ! isLocal2 )
    {
      // repository <-> repository
      return diffRpRp( path, revision1, path, revision2, peg );
    }
    else
    {
      // repository <-> working
      return diffRpWc( path, revision1, path, revision2, peg, true );
    }
  }
  else
  {
    if( ! isLocal2 )
    {
      // working <-> repository
      diffRpWc( path, revision2, path, revision1, peg, false );
    }
    else
    {
      // working <-> working
      return diffWcWc( path, revision1, path, revision2 );
    }
  }

  return SVN_NO_ERROR;
}


svn_error_t* VisualDiff::diffRpWc( const char* path1, const svn_opt_revision_t*
  revision1, const char* path2, const svn_opt_revision_t* revision2, const
  svn_opt_revision_t* peg, bool rpwc )
{
  const char* url1;
  SVN_ERR( svn_client_url_from_path(&url1, path1, _pool) );

  svn_ra_callbacks_t* cb = (svn_ra_callbacks_t*)apr_pcalloc( _pool, sizeof(*cb) );
  cb->auth_baton         = _context->auth_baton;

  svn_ra_session_t* session;
  SVN_ERR( svn_ra_open( &session, url1, cb, NULL, _context->config, _pool ) );

  const char* reposroot;
  SVN_ERR( svn_ra_get_repos_root( session, &reposroot, _pool ) );

  svn_revnum_t revnum1;
  SVN_ERR( getRevNumber( &revnum1, session, revision1, url1, _pool ) );

  const char* fetchurl1 = url1;

  if( peg->kind != svn_opt_revision_unspecified )
  {
    svn_revnum_t pegnum;
    SVN_ERR( getRevNumber( &pegnum, session, peg, url1, _pool ) );

    apr_hash_t* locations;
    apr_array_header_t* revisions = apr_array_make( _pool, 1, sizeof(svn_revnum_t) );
    APR_ARRAY_PUSH( revisions, svn_revnum_t ) = revnum1;

    SVN_ERR( svn_ra_get_locations( session, &locations, "", pegnum, revisions,
      _pool) );

    const char* rurl1;
    rurl1 = (char*)apr_hash_get (locations, &revnum1, sizeof (svn_revnum_t));

    fetchurl1 = apr_pstrcat( _pool, reposroot, rurl1, NULL );
  }


  const char* tdir;
  SVN_ERR( svn_io_temp_dir( &tdir, _pool ) );
  tdir = apr_pstrcat(_pool, tdir, "/subcommander", NULL );

  const char*   name1;
  apr_file_t*   file1;
  SVN_ERR( svn_io_open_unique_file( &file1, &name1, tdir, ".tmp", false, _pool) );
  _removePath1 = name1;

  svn_stream_t* stream1;
  stream1 = svn_stream_from_aprfile( file1, _pool );

  svn_ra_session_t* session1;
  svn_revnum_t      fetchnum1;
  SVN_ERR( svn_ra_open( &session1, fetchurl1, cb, NULL, _context->config, _pool ) );
  SVN_ERR( svn_ra_get_file( session1, "", revnum1, stream1, &fetchnum1, NULL, _pool) );
  apr_file_close(file1);


  const char* wcpath;
  if( revision2->kind == svn_opt_revision_base )
  {
    // get base file
    SVN_ERR( svn_wc_get_pristine_copy_path( path2, &wcpath, _pool ) );
  }
  else //(revision2->kind == svn_opt_revision_working)
  {
    // get "clean" wc file
    svn_wc_adm_access_t* access;
    SVN_ERR( svn_wc_adm_probe_open3( &access, 0, path2, false, 0, NULL, NULL,
      _pool ) );

    SVN_ERR( svn_wc_translated_file( &wcpath, path2, access, false, _pool ) );

    // if the translation created a temporary file we have to remember it
    // so we can remove it when it is no longer needed.
    sc::String sBase(path2);
    sc::String sWc(wcpath);

    if( sWc != sBase )
    {
      _removePath2 = sWc;
    }
  }

  sc::String label1(fetchurl1);
  const char* strrev1 = apr_off_t_toa(_pool, revnum1);
  label1 += "\t(Revision ";
  label1 += strrev1;
  label1 += ")";

  sc::String labelwc(path2);
  if( revision2->kind == svn_opt_revision_base )
  {
    labelwc += "\t(Base)";
  }
  else
  {
    labelwc += "\t(Working Copy)";
  }

  if( rpwc )
  {
    SVN_ERR( run( name1, label1, wcpath, labelwc ) );
  }
  else
  {
    SVN_ERR( run( wcpath, labelwc, name1, label1 ) );
  }

  return SVN_NO_ERROR;
}

svn_error_t* VisualDiff::diffRpRp( const char* path1, const svn_opt_revision_t*
  revision1, const char* path2, const svn_opt_revision_t* revision2, const
  svn_opt_revision_t* peg )
{
  const char* url1;
  const char* url2;

  SVN_ERR( svn_client_url_from_path(&url1, path1, _pool) );
  SVN_ERR( svn_client_url_from_path(&url2, path2, _pool) );


  svn_ra_callbacks_t* cb = (svn_ra_callbacks_t*)apr_pcalloc( _pool, sizeof(*cb) );
  cb->auth_baton         = _context->auth_baton;

  svn_ra_session_t* session;
  SVN_ERR( svn_ra_open( &session, url2, cb, NULL, _context->config, _pool ) );

  const char* reposroot;
  SVN_ERR( svn_ra_get_repos_root( session, &reposroot, _pool ) );


  svn_revnum_t revnum1;
  svn_revnum_t revnum2;
  SVN_ERR( getRevNumber( &revnum1, session, revision1, url2, _pool ) );
  SVN_ERR( getRevNumber( &revnum2, session, revision2, url2, _pool ) );

  const char* fetchurl1 = url1;
  const char* fetchurl2 = url2;

  if( peg->kind != svn_opt_revision_unspecified )
  {
    svn_revnum_t pegnum;
    SVN_ERR( getRevNumber( &pegnum, session, peg, url2, _pool ) );

    apr_hash_t* locations;
    apr_array_header_t* revisions = apr_array_make( _pool, 2, sizeof(svn_revnum_t) );
    APR_ARRAY_PUSH( revisions, svn_revnum_t ) = revnum1;
    APR_ARRAY_PUSH( revisions, svn_revnum_t ) = revnum2;

    SVN_ERR( svn_ra_get_locations( session, &locations, "", pegnum, revisions,
      _pool) );

    const char* rurl1;
    const char* rurl2;
    rurl1 = (char*)apr_hash_get (locations, &revnum1, sizeof (svn_revnum_t));
    rurl2 = (char*)apr_hash_get (locations, &revnum2, sizeof (svn_revnum_t));

    fetchurl1 = apr_pstrcat( _pool, reposroot, rurl1, NULL );
    fetchurl2 = apr_pstrcat( _pool, reposroot, rurl2, NULL );
  }

  const char* tdir;
  SVN_ERR( svn_io_temp_dir( &tdir, _pool ) );
  tdir = apr_pstrcat(_pool, tdir, "/subcommander", NULL );

  const char*   name1;
  apr_file_t*   file1;
  SVN_ERR( svn_io_open_unique_file( &file1, &name1, tdir, ".tmp", false, _pool) );
  _removePath1 = name1;

  svn_stream_t* stream1;
  stream1 = svn_stream_from_aprfile( file1, _pool );

  svn_ra_session_t* session1;
  svn_revnum_t      fetchnum1;
  SVN_ERR( svn_ra_open( &session1, fetchurl1, cb, NULL, _context->config, _pool ) );
  svn_error_t* err1 = svn_ra_get_file( session1, "", revnum1, stream1, &fetchnum1, NULL, _pool);
  apr_file_close(file1);

  if( err1 && err1->apr_err != SVN_ERR_FS_NOT_FOUND )
  {
    return err1;
  }
  if( err1 && err1->apr_err == SVN_ERR_FS_NOT_FOUND )
  {
    fetchurl1 = "file does not exist in this revision!";
  }

  const char*   name2;
  apr_file_t*   file2;
  SVN_ERR( svn_io_open_unique_file( &file2, &name2, tdir, ".tmp", false, _pool) );
  _removePath2 = name2;

  svn_stream_t* stream2;
  stream2 = svn_stream_from_aprfile( file2, _pool );

  svn_ra_session_t* session2;
  svn_revnum_t      fetchnum2;
  SVN_ERR( svn_ra_open( &session2, fetchurl2, cb, NULL, _context->config, _pool ) );
  svn_error_t* err2 = svn_ra_get_file( session2, "", revnum2, stream2, &fetchnum2, NULL, _pool);
  apr_file_close(file2);

  if( err2 && err2->apr_err != SVN_ERR_FS_NOT_FOUND )
  {
    return err2;
  }
  if( err2 && err2->apr_err == SVN_ERR_FS_NOT_FOUND )
  {
    fetchurl2 = "file does not exist in this revision!";
  }


  sc::String label1(fetchurl1);
  const char* strrev1 = apr_off_t_toa(_pool, revnum1);
  label1 += "\t(Revision ";
  label1 += strrev1;
  label1 += ")";

  sc::String label2(fetchurl2);
  const char* strrev2 = apr_off_t_toa(_pool, revnum2);
  label2 += "\t(Revision ";
  label2 += strrev2;
  label2 += ")";

  SVN_ERR( run( name1, label1, name2, label2 ) );

  return SVN_NO_ERROR;
}

// assumes path1 == path2 with revisions base <-> working
svn_error_t* VisualDiff::diffWcWc( const char* path1, const svn_opt_revision_t*
  revision1, const char* path2, const svn_opt_revision_t* revision2 )
{
  if( (strcmp(path1, path2) != 0)
    || (! (revision1->kind == svn_opt_revision_base)
       && (revision2->kind == svn_opt_revision_working))
    )
    return svn_error_create( SVN_ERR_INCORRECT_PARAMS, NULL,
      "Only diffs between a path's text-base and its working files are supported at this time");

  // get base file
  const char* base;
  SVN_ERR( svn_wc_get_pristine_copy_path( path1, &base, _pool ) );

  // create empty file if there is no base file
  apr_finfo_t  info = {};
  apr_status_t status = apr_stat( &info, base, APR_FINFO_SIZE, _pool );
  if( status != APR_SUCCESS )
  {
    const char* tdir;
    SVN_ERR( svn_io_temp_dir( &tdir, _pool ) );
    tdir = apr_pstrcat(_pool, tdir, "/subcommander", NULL );

    apr_file_t* file;
    SVN_ERR( svn_io_open_unique_file( &file, &base, tdir, ".tmp", false, _pool) );
    _removePath1 = base;
    apr_file_close(file);
  }

  // get "clean" wc file
  svn_wc_adm_access_t* access;
  SVN_ERR( svn_wc_adm_probe_open3( &access, 0, path1, false, 0, NULL, NULL,
    _pool ) );

  const char* wc;
  SVN_ERR( svn_wc_translated_file( &wc, path1, access, false, _pool ) );

  // if the translation created a temporary file we have to remember it
  // so we can remove it when it is no longer needed.
  sc::String sBase(path1);
  sc::String sWc(wc);

  if( sWc != sBase )
  {
    _removePath2 = sWc;
  }

  sc::String labelBase(path1);
  labelBase += "\t(Base)";

  sc::String labelWc(path1);
  labelWc += "\t(Working Copy)";

  SVN_ERR( run( base, labelBase, wc, labelWc ) );

  return SVN_NO_ERROR;
}


svn_error_t* VisualDiff::run( const char* path1, const char* label1,
  const char* path2, const char* label2 )
{
  int            result;
  apr_exit_why_e why;

  CommandArgs cmdArgs( _diffCmd, _pool );
  cmdArgs.setArg( sc::String("{left}"),   sc::String(path1) );
  cmdArgs.setArg( sc::String("{llabel}"), sc::String(label1) );
  cmdArgs.setArg( sc::String("{right}"),  sc::String(path2) );
  cmdArgs.setArg( sc::String("{rlabel}"), sc::String(label2) );
 
  return svn_io_run_cmd( cmdArgs.getPath(), cmdArgs.getArgs()[0],
    cmdArgs.getArgs(), &result, &why, true, 0, 0, 0, _pool );
}

} // namespace


syntax highlighted by Code2HTML, v. 0.9.1