/*******************************************************************************

Copyright (c) 1997-2004, Perforce Software, Inc.  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1.  Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTR
IBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*******************************************************************************/

/*******************************************************************************
 * Name		: clientuserruby.cc
 *
 * Author	: Tony Smith <tony@perforce.com> or <tony@smee.org>
 *
 * Description	: Ruby bindings for the Perforce API. User interface class
 * 		  for getting Perforce results into Ruby.
 *
 ******************************************************************************/
#include <ctype.h>
#include <ruby.h>
#include <clientapi.h>
#include <spec.h>
#include <diff.h>
#include "gc_hack.h"
#include "p4result.h"
#include "clientuserruby.h"


/*******************************************************************************
 * ClientUserRuby - the user interface part. Gets responses from the Perforce
 * server, and converts the data to Ruby form for returning to the caller.
 ******************************************************************************/

ClientUserRuby::ClientUserRuby()
{ 
    debug = 0;
    input = Qnil;
}


void
ClientUserRuby::Reset()
{
    results.Reset();
    // Leave input alone.
}

void	
ClientUserRuby::Finished()
{
    // Reset input coz we should be done with it now. Keeping hold of
    // it just prevents GC from sweeping it if possible
    if ( debug && input != Qnil )
	fprintf( stderr, "[P4] Cleaning up saved input\n" );

    input = Qnil;
}

void
ClientUserRuby::HandleError( Error *e )
{
    if ( debug )
	fprintf( stderr, "[P4] HandleError()\n" );

    results.AddError( e );
}

void
ClientUserRuby::OutputText( const_char *data, int length )
{
    if ( debug )
	fprintf( stderr, "[P4] OutputText()\n" );

    results.AddOutput( data );
}

void
ClientUserRuby::OutputInfo( char level, const_char *data )
{
    if ( debug )
	fprintf( stderr, "[P4] OutputInfo()\n" );

    results.AddOutput( data );
}

void
ClientUserRuby::OutputBinary( const_char *data, int length )
{
    if ( debug )
	fprintf( stderr, "[P4] OutputBinary() %d bytes\n", length );

    //
    // Binary is just stored in a string. Since the char * version of
    // P4Result::AddOutput() assumes it can strlen() to find the length,
    // we'll make the String object here.
    //
    results.AddOutput( rb_str_new( data, length) );
}

void
ClientUserRuby::OutputStat( StrDict *values )
{
    StrPtr	*spec, *data;

    // If both specdef and data are set, then we need to parse the form
    // and return the results. If not, then we just convert it as is.

    spec = values->GetVar( "specdef" );
    data = values->GetVar( "data" );

    if ( spec && data )
    {
	if ( debug )
	    fprintf( stderr, "[P4] OutputStat() - parsing form\n" );

	// Parse up the form. Use the ParseNoValid() interface to prevent
	// errors caused by the use of invalid defaults for select items in
	// jobspecs.
	SpecDataTable	specData;
	Spec		s( spec->Text(), "" );
	Error		e;

	s.ParseNoValid( data->Text(), &specData, &e );
	if ( e.Test() )
	{
	    HandleError( &e );
	    return;
	}

	results.AddOutput( DictToHash( specData.Dict(), spec ) );
    }
    else
    {
	if ( debug )
	    fprintf( stderr, "[P4] OutputStat() - converting StrDict to hash\n" );

	results.AddOutput( DictToHash( values, NULL ) );
    }
}


/*
 * Diff support for Ruby API. Since the Diff class only writes its output
 * to files, we run the requested diff putting the output into a temporary
 * file. Then we read the file in and add its contents line by line to the 
 * results.
 */

void
ClientUserRuby::Diff( FileSys *f1, FileSys *f2, int doPage, 
				char *diffFlags, Error *e )
{

    if ( debug )
	fprintf( stderr, "[P4] Diff() - comparing files\n" );

    //
    // Duck binary files. Much the same as ClientUser::Diff, we just
    // put the output into Ruby space rather than stdout.
    //
    if( !f1->IsTextual() || !f2->IsTextual() )
    {
	if ( f1->Compare( f2, e ) )
	    results.AddOutput( "(... files differ ...)" );
	return;
    }

    // Time to diff the two text files. Need to ensure that the
    // files are in binary mode, so we have to create new FileSys
    // objects to do this.

    FileSys *f1_bin = FileSys::Create( FST_BINARY );
    FileSys *f2_bin = FileSys::Create( FST_BINARY );
    FileSys *t = FileSys::CreateGlobalTemp( f1->GetType() );

    f1_bin->Set( f1->Name() );
    f2_bin->Set( f2->Name() );

    {
	//
	// In its own block to make sure that the diff object is deleted
	// before we delete the FileSys objects.
	//
#ifndef OS_NEXT
	::
#endif
	Diff d;

	d.SetInput( f1_bin, f2_bin, diffFlags, e );
	if ( ! e->Test() ) d.SetOutput( t->Name(), e );
	if ( ! e->Test() ) d.DiffWithFlags( diffFlags );
	d.CloseOutput( e );

	// OK, now we have the diff output, read it in and add it to 
	// the output.
	if ( ! e->Test() ) t->Open( FOM_READ, e );
	if ( ! e->Test() ) 
	{
	    StrBuf 	b;
	    while( t->ReadLine( &b, e ) )
		results.AddOutput( b.Text() );
	}
    }

    delete t;
    delete f1_bin;
    delete f2_bin;

    if ( e->Test() ) HandleError( e );
}


/*
 * convert input from the user into a form digestible to Perforce. This
 * involves either (a) converting any supplied hash to a Perforce form, or
 * (b) running to_s on whatever we were given. 
 */

void
ClientUserRuby::InputData( StrBuf *strbuf, Error *e )
{
    if ( debug )
	fprintf( stderr, "[P4] InputData(). Using supplied input\n" );

    if ( Qtrue == rb_obj_is_kind_of( input, rb_cHash ) )
    {
	HashToForm( input, strbuf );
	return;
    }

    ID	to_s 	= rb_intern( "to_s" );
    VALUE	str 	= rb_funcall( input, to_s, 0 );
    strbuf->Set( STR2CSTR( str ) );
}

/*
 * Accept input from Ruby and convert to a StrBuf for Perforce
 * purposes.  We just save what we're given here because we may not 
 * have the specdef available to parse it with at this time.
 */

VALUE
ClientUserRuby::SetInput( VALUE i )
{
    if ( debug )
	fprintf( stderr, "[P4] SetInput()\n" );

    input = i;
    return Qtrue;
}


/*
 * Convert a Perforce StrDict into a Ruby hash. Convert multi-level 
 * data (Files0, Files1 etc. ) into (nested) array members of the hash. If
 * specDef is NULL, then the specDef member will be skipped over, other
 * wise it will be saved as a wrapped structure in the hash.
 */

VALUE
ClientUserRuby::DictToHash( StrDict *dict, StrPtr *specDef )
{
    StrRef	var, val;
    int		i;
    VALUE	hash = rb_hash_new();
    VALUE	hkey;
    VALUE	sdv;
    StrBuf	*sd = new StrBuf;

    for ( i = 0; dict->GetVar( i, var, val ); i++ )
    {
	if ( var == "specdef" && ! specDef ) 
	    continue;

	if ( var == "func" ) continue;
	InsertItem( hash, &var, &val );
    }
    return hash;
}

/*
 * Convert a Ruby hash - possibly containing array members into a
 * formatted StrBuf ready for sending to Perforce. Return Qtrue or
 * Qfalse to indicate success or failure. The form itself is saved in
 * this->input
 */

int
ClientUserRuby::HashToForm( VALUE hash, StrBuf *strbuf )
{
    StrPtr	*specDef;
    
    specDef = varList->GetVar( "specdef" );
    if ( ! specDef )
    {
	rb_warn( "No specdef available. Cannot convert hash to a "
		 "Perforce form" );
	return 0;
    }

    SpecDataTable	specData;
    Spec		s( specDef->Text(), "" );

    ID		idKeys = rb_intern( "keys" );
    ID		idLength = rb_intern( "length" );
    ID		idToS = rb_intern( "to_s" );
    VALUE	keys	= rb_funcall( hash, idKeys, 0 );
    int		keyCount = NUM2INT( rb_funcall( keys, idLength, 0 ) );

    for ( int idx = 0; idx < keyCount; idx++ )
    {
	VALUE	key;
	VALUE	val;
	StrBuf	keyStr;

	key = rb_ary_entry( keys, idx );
	if ( key == Qnil ) break;

	keyStr.Set( STR2CSTR( rb_funcall( key, idToS, 0 ) ) );

	val = rb_hash_aref( hash, key );
	if ( rb_obj_is_kind_of( val, rb_cArray ) )
	{
	    // Need to flatten the array
	    VALUE	subVal;

	    for( int idx2 = 0; subVal = rb_ary_entry( val, idx2 ) ; idx2++ )
	    {
		if ( subVal == Qnil ) break;

		StrBuf	tKey;
		tKey.Alloc( 32 );
		sprintf( tKey.Text(), "%s%d", keyStr.Text(), idx2 );

		specData.Dict()->SetVar( tKey.Text(), 
				 STR2CSTR( rb_funcall( subVal, idToS, 0 )));
	    }
	}
	else
	{
	    specData.Dict()->SetVar( keyStr.Text(), 
		    		     STR2CSTR( rb_funcall( val, idToS, 0 ) ) );
	}
    }

    s.Format( &specData, strbuf );
    return 1;
}


/*
 * Split a key into its base name and its index. i.e. for a key "how1,0"
 * the base name is "how" and they index is "1,0". We work backwards from
 * the end of the key looking for the first char that is neither a
 * digit, nor a comma.
 */

void
ClientUserRuby::SplitKey( const StrPtr *key, StrBuf &base, StrBuf &index )
{
    int i = 0;

    base = *key;
    index = "";
    for ( i = key->Length(); i; i-- )
    {
	char	prev = (*key)[ i-1 ];
	if ( !isdigit( prev ) && prev != ',' )
	{
	    base.Set( key->Text(), i );
	    index.Set( key->Text() + i );
	    break;
	}
    }
}

/*
 * Insert an element into the response structure. The element may need to
 * be inserted into an array nested deeply within the enclosing hash.
 */

void
ClientUserRuby::InsertItem( VALUE hash, const StrPtr *var, const StrPtr *val )
{
    VALUE	ary = 0;
    VALUE	tary = 0;
    VALUE	key;
    StrBuf	base, index;
    StrRef	comma( "," );

    SplitKey( var, base, index );

    // If there's no index, then we insert into the top level hash 
    // but if the key is already defined then we need to rename the key. This
    // is probably one of those special keys like otherOpen which can be
    // both an array element and a scalar. The scalar comes last, so we
    // just rename it to "otherOpens" to avoid trashing the previous key
    // value
    if ( index == "" )
    {
	ID idHasKey 	= rb_intern( "has_key?");
	ID idPlus	= rb_intern( "+" );

	key = rb_str_new2( var->Text() );
	if ( rb_funcall( hash, idHasKey, 1, key ) == Qtrue )
	    key = rb_funcall( key, idPlus, 1, rb_str_new2( "s" ) );
	
	rb_hash_aset( hash, key, rb_str_new2( val->Text() ) );
	return;
    }

    //
    // Get or create the parent array from the hash.
    //
    key = rb_str_new2( base.Text() );
    ary = rb_hash_aref( hash, key );

    if ( Qnil == ary )
    {
	ary = rb_ary_new();
	rb_hash_aset( hash, key, ary );
    }

    // The index may be a simple digit, or it could be a comma separated
    // list of digits. For each "level" in the index, we need a containing
    // array.
    for( const char *c = 0 ; c = index.Contains( comma ); )
    {
	StrBuf	level;
	level.Set( index.Text(), c - index.Text() );
	index.Set( c + 1 );

	// Found another level so we need to get/create a nested array
	// under the current entry. We use the level as an index so that
	// missing entries are left empty deliberately.
	
	tary = rb_ary_entry( ary, level.Atoi() );
	if ( ! RTEST( tary ) )
	{
	    tary = rb_ary_new();
	    rb_ary_store( ary, level.Atoi(), tary );
	}
	ary = tary;
    }
    rb_ary_push( ary, rb_str_new2( val->Text() )  );
}


void
ClientUserRuby::GCMark()
{
    if ( debug >= 2 )
    	fprintf( stderr, 
		"[P4] Marking results and errors for garbage collection\n" );

    if ( input != Qnil ) 
	rb_gc_mark( input );

    results.GCMark();
}
