/**
 * Module:		PackageSlideShow.ycp
 *
 * Purpose:		Module to access slides from installation repository
 *
 * Author:		Stefan Hundhammer <sh@suse.de>
 *                      Stanislav Visnovsky <visnov@suse.cz>
 *
 */
{
    module "PackageSlideShow";

    textdomain "packager";

    import "Slides";
    import "SlideShow";
    import "String";

    global list<list<integer> > total_sizes_per_cd_per_src	= [];	// total sizes per inst-src: [ [42, 43, 44], [12, 13, 14] ]
    global list<list<integer> > remaining_sizes_per_cd_per_src	= [];	// remaining sizes
    global list<list<integer> > remaining_times_per_cd_per_src	= [];	// remaining times
    global list<string>		inst_src_names			= [];	// a list of strings identifying each repository
    global list<list<integer> > total_pkg_count_per_cd_per_src	= [];	// number of pkgs per inst-src: [ [7, 5, 3], [2, 3, 4] ]
    global list<list<integer> > remaining_pkg_count_per_cd_per_src = []; // remaining number of pkgs
    global map<integer,integer> srcid_to_current_src_no = $[];
    // the string is follwed by a media number, e.g. "Medium 1"
    global string	media_type		= _("Medium");
    global integer	total_size_installed	= 0;
    global integer	total_size_to_install	= 0;
    global integer	min_time_per_cd		= 10;	// const - minimum time displayed per CD if there is something to install
    global integer	max_time_per_cd		= 7200; // const - seconds to cut off predicted time (it's bogus anyway)
    global integer	size_column		= 1;	// const - column number for remaining size per CD
    global integer	pkg_count_column	= 2;	// const - column number for remaining number of packages per CD
    global integer	time_column		= 3;	// const - column number for remaining time per CD
    global integer	current_src_no		= -1;	// 1..n
    global integer	current_cd_no		= -1;	// 1..n
    global integer	next_src_no		= -1;
    global integer	next_cd_no		= -1;
    global boolean	last_cd			= false;
    global integer	total_cd_count		= 0;
    global boolean	unit_is_seconds		= false;	// begin with package sizes
    global integer	bytes_per_second	= 1;
    global boolean	init_pkg_data_complete	= false;

    boolean debug				= false;	// more debugging info
    string provide_name 			= "";		// currently downlaoded package name
    string provide_size 			= "";		// currently downlaoded package size


/*****************************************************************************/
/***************  Formatting functions and helpers ***************************/
/*****************************************************************************/

    /**
     * Get version info for a package (without build no.)
     *
     * @param pkg_name name of the package without path and ".rpm" extension
     * @return version string
     **/
    global string StripReleaseNo( string pkg_name )
    {
	integer build_no_pos = findlastof (pkg_name, "-" );	// find trailing build no.

	if ( build_no_pos != nil && build_no_pos > 0 )
	{
	    // cut off trailing build no.
	    pkg_name = substring( pkg_name , 0, build_no_pos );
	}

	return pkg_name;
    }

    /**
     * Get package file name from path
     *
     * @param pkg_name location of the package
     * @return string package file name
     **/
    global string StripPath(string pkg_name)
    {
	if (pkg_name == nil)
	{
	    return nil;
	}

	integer file_pos = findlastof(pkg_name, "/");

	if (file_pos != nil && file_pos > 0 )
	{
	    // return just the file name
	    pkg_name = substring(pkg_name, file_pos + 1);
	}

	return pkg_name;
    }


    /**
     * set media type "CD" or "DVD"
     */
    global void SetMediaType (string new_media_type)
    {
	media_type = new_media_type;
    }

    /**
     * Sum up all list items
     **/
    integer ListSum( list<integer> sizes )
    {
	integer sum = 0;

	foreach( integer item, sizes, ``{
	    if ( item != -1 )
		sum = sum + item;
	});

	return sum;
    }


    /**
     * Sum up all positive list items, but cut off individual items at a maximum value.
     * Negative return values indicate overflow of any individual item at "max_cutoff".
     * In this case, the real sum is the absolute value of the return value.
     **/
    integer ListSumCutOff( list<integer> sizes, integer max_cutoff )
    {
	boolean overflow = false;
	integer sum = 0;

	foreach( integer item, sizes, ``{
	    if ( item > 0 )
	    {
		if ( item > max_cutoff )
		{
		    overflow = true;
		    sum = sum + max_cutoff;
		}
		else
		    sum = sum + item;
	    }
	});

	if ( overflow )
	    sum = -sum;

	return sum;
    }


    integer TotalRemainingSize()
    {
	return ListSum( flatten( remaining_sizes_per_cd_per_src ) );
    }


    integer TotalRemainingTime()
    {
	return ListSumCutOff( flatten( remaining_times_per_cd_per_src ),
			      max_time_per_cd );
    }


    integer TotalRemainingPkgCount()
    {
	return ListSum( flatten( remaining_pkg_count_per_cd_per_src ) );
    }


    integer TotalInstalledSize()
    {
	return total_size_to_install - TotalRemainingSize();
    }


    /**
     * Format an integer number as (at least) two digits; use leading zeroes if
     * necessary.
     * @return number as two-digit string
     **/
    string FormatTwoDigits( integer x )
    {
	return x < 10 && x >= 0 ?
	    sformat( "0%1", x ) :
	    sformat(  "%1", x );
    }


    /**
     * Format an integer seconds value with min:sec or hours:min:sec
     **/
    string FormatTime( integer seconds )
    {
	if ( seconds < 0 )
	    return "";

	if ( seconds < 3600 )	// Less than one hour
	{
	    return sformat( "%1:%2", FormatTwoDigits( seconds / 60 ), FormatTwoDigits( seconds % 60 ) );
	}
	else	// More than one hour - we don't hope this will ever happen, but who knows?
	{
	    integer hours = seconds / 3600;
	    seconds = seconds % 3600;
	    return sformat( "%1:%2:%3", hours, FormatTwoDigits( seconds / 60 ), FormatTwoDigits( seconds % 60 ) );
	}
    }


    /**
     * Format an integer seconds value with min:sec or hours:min:sec
     *
     * Negative values are interpreted as overflow - ">" is prepended and the
     * absolute value is used.
     **/
    string FormatTimeShowOverflow( integer seconds )
    {
	string text = "";

	if ( seconds < 0 )	// Overflow (indicated by negative value)
	{
	    // When data throughput goes downhill (stalled network connection etc.),
	    // cut off the predicted time at a reasonable maximum.
	    // "%1" is a predefined maximum time.

	    text = sformat( _(">%1"), FormatTime( -seconds ) );
	}
	else
	{
	    text = FormatTime( seconds );
	}

	return text;
    }


    /**
     * Format number of remaining bytes to be installed as string.
     * @param remaining		bytes remaining, -1 for 'done'
     * @return			string human readable remaining time or byte / kB/ MB size
     **/
    string FormatRemainingSize( integer remaining )
    {
	if ( remaining <  0 )
	{
	    // Nothing more to install from this CD (very concise - little space!!)
	    return _("Done.");
	}
	if ( remaining == 0 )
	{
	    return "";
	}

	return String::FormatSize( remaining );
    }


    /**
     * Format number of remaining packages to be installed as string.
     * @param remaining		bytes remaining, -1 for 'done'
     * @return			string human readable remaining time or byte / kB/ MB size
     **/
    string FormatRemainingCount( integer remaining )
    {
	if ( remaining <  0 )
	{
	    // Nothing more to install from this CD (very concise - little space!!)
	    return _("Done.");
	}
	if ( remaining == 0 )
	{
	    return "";
	}

	return sformat( "%1", remaining );
    }


    string FormatNextMedia()
    {
	string text = "";

	if ( next_src_no >= 0 && next_cd_no >= 0 )
	{
	    string next_media_name = sformat( "%1 %2 %3",
					      inst_src_names[ next_src_no ]:"",
					      media_type, next_cd_no+1 );

	    if ( unit_is_seconds )
	    {
		// Status line informing about the next CD that will be used
		// %1: Media type ("CD" / "DVD", ???)
		// %2: Media name ("SuSE Linux Professional CD 2" )
		// %3: Time remaining until this media will be needed
		text = sformat( _("Next %1: %2 -- %3"), media_type, next_media_name,
				FormatTime( remaining_times_per_cd_per_src[ current_src_no-1, current_cd_no-1 ]: 1) );
	    }
	    else
	    {
		// Status line informing about the next CD that will be used
		// %1: Media type ("CD" / "DVD", ???)
		// %2: Media name ("SuSE Linux Professional CD 2" )
		text = sformat( _("Next %1: %2"), media_type, next_media_name );
	    }
	}

	return text;
    }

/*****************************************************************************/
/***********************  Computing Helpers **********************************/
/*****************************************************************************/


    /**
     * Perform sanity check for correct initialzation etc.
     * @param silent	don't complain in log file
     * @return		true if OK, false if any error
     **/
    boolean SanityCheck( boolean silent )
    {
	return true;  // FIXME!
	if ( ! init_pkg_data_complete )
	{
	    if ( ! silent )
	    {
		y2error( "SlideShow::SanityCheck(): Slide show not correctly initialized: " +
			 "SlideShow::InitPkgData() never called!" );
	    }
	    return false;
	}

	if ( current_src_no < 1 || current_cd_no < 1 )
	{
	    // nothing to install but something is going to be deleted, so it's OK
	    if (Pkg::IsAnyResolvable(`package, `to_remove))
	    {
		return true;
	    }
	    else if (!silent)
	    {
		y2error(-1, "SlideShow::SanityCheck(): Illegal values for current_src (%1) or current_cd (%2)",
		     current_src_no, current_cd_no );
		y2milestone( "total sizes: %1", total_sizes_per_cd_per_src );
	    }
	    return false;
	}

	return true;
    }

    /**
     * Update internal bookkeeping: subtract size of one package from the
     * global list of remaining sizes per CD
     **/
    void SubtractPackageSize( integer pkg_size )
    {
	integer remaining = remaining_sizes_per_cd_per_src [ current_src_no-1, current_cd_no-1 ]: 1;
	remaining = remaining - pkg_size;
	total_size_installed = total_size_installed + pkg_size;

	if ( remaining <= 0 )
	{
	    // -1 is the indicator for "done with this CD" - not to be
	    // confused with 0 for "nothing to install from this CD".
	    remaining = -1;
	}

	remaining_sizes_per_cd_per_src [ current_src_no-1, current_cd_no-1 ] = remaining;
	remaining_pkg_count_per_cd_per_src [ current_src_no-1, current_cd_no-1 ] =
	    remaining_pkg_count_per_cd_per_src [ current_src_no-1, current_cd_no-1 ]:0 -1;

	if ( unit_is_seconds )
	{
	    integer seconds = 0;

	    if ( remaining > 0 && bytes_per_second > 0 )
		seconds = remaining / bytes_per_second;

	    remaining_times_per_cd_per_src [ current_src_no-1, current_cd_no-1 ] = seconds;
	}

	if ( debug )
	    y2milestone( "SubtractPackageSize( %1 ) -> %2", pkg_size, remaining_sizes_per_cd_per_src);
    }


    /**
     * Initialize internal pacakge data, such as remaining package sizes and
     * times. This may not be called before the pkginfo server is up and
     * running, so this cannot be reliably done from the constructor in all
     * cases.
     * @param force true to force reinitialization
     **/
    global void InitPkgData(boolean force)
    {
	if ( init_pkg_data_complete && ! force)
	    return;

	// Reinititalize some globals (in case this is a second run)
	total_size_installed	= 0;
	//total_time_elapsed	= 0;
	//start_time		= -1;
	current_src_no		= -1;	// 1..n
	current_cd_no		= -1;	// 1..n
	next_src_no		= -1;
	next_cd_no		= -1;
	last_cd			= false;
	unit_is_seconds		= false;	// begin with package sizes
	bytes_per_second	= 1;

	list< list > src_list = Pkg::PkgMediaNames();
	inst_src_names = maplist( list src, src_list, ``(src[0]:"CD") );

	y2milestone ("Media names: %1", inst_src_names);

	integer index = 0;

	srcid_to_current_src_no = listmap( list src, src_list, {
	  index = index + 1;
	  return $[src[1]:-1 : index];
	});

	y2milestone ("Repository  mapping information: %1", srcid_to_current_src_no );

	total_sizes_per_cd_per_src		= Pkg::PkgMediaSizes();
	total_pkg_count_per_cd_per_src		= Pkg::PkgMediaCount();


	total_size_to_install			= ListSum( flatten( total_sizes_per_cd_per_src ) );
	y2milestone("total_size_to_install: %1", total_size_to_install);
	remaining_sizes_per_cd_per_src		= (list<list <integer> >) eval (total_sizes_per_cd_per_src);
	remaining_pkg_count_per_cd_per_src	= (list<list <integer> >) eval (total_pkg_count_per_cd_per_src);
	total_cd_count				= size( flatten( total_sizes_per_cd_per_src ) );
	init_pkg_data_complete			= true;
	
	y2milestone( "SlideShow::InitPkgData() done; total_sizes_per_cd_per_src: %1", total_sizes_per_cd_per_src );
	y2milestone( "SlideShow::InitPkgData(): pkg: %1", total_pkg_count_per_cd_per_src );

	// RebuildDialog(true);
    }

    /**
     * Try to figure out what media will be needed next
     * and set next_src_no and next_cd_no accordingly.
     **/
    void FindNextMedia()
    {
	// Normally we would have to use current_cd_no+1,
	// but since this uses 1..n and we need 0..n-1
	// for array subscripts anyway, use it as it is.
	next_cd_no	= current_cd_no;
	next_src_no	= current_src_no-1;
	last_cd		= false;

	while ( next_src_no < size( remaining_sizes_per_cd_per_src ) )
	{
	    list<integer> remaining_sizes = remaining_sizes_per_cd_per_src[ next_src_no ]: [];

	    while ( next_cd_no < size( remaining_sizes ) )
	    {
		if ( remaining_sizes[ next_cd_no ]:0 > 0 )
		{
		    if ( debug )
			y2milestone( "Next media: src: %1 CD: %2", next_src_no, next_cd_no );
		    return;
		}
		else
		{
		    next_cd_no = next_cd_no + 1;
		}
	    }

	    next_src_no = next_src_no + 1;
	}

	if ( debug )
	    y2milestone( "No next media - all done" );

	next_src_no	= -1;
	next_cd_no	= -1;
	last_cd		= true;
    }


    /**
     * Set the current repository and CD number. Must be called for each CD change.
     * src_no: 1...n
     * cd_no:  1...n
     **/
    global void SetCurrentCdNo( integer src_no, integer cd_no )
    {
	if (cd_no == 0)
	{
	    y2milestone("medium number 0, using medium number 1");
	    cd_no = 1;
	}

	y2milestone("SetCurrentCdNo() - src: %1 , CD: %2", src_no, cd_no);
	current_src_no = srcid_to_current_src_no[src_no]:-1;
	current_cd_no  = cd_no;

	SlideShow::CheckForSlides();
	FindNextMedia();

	if ( Slides::HaveSlides() && Slides::HaveSlideSupport() )
	{
	    if ( ! SlideShow::HaveSlideWidget() )
	    {
		SlideShow::RebuildDialog();

		if ( SlideShow::user_switched_to_details )
		    SlideShow::SwitchToDetailsView();
	    }

	    if ( ! SlideShow::user_switched_to_details ) // Don't override explicit user request!
	    {
		SlideShow::SwitchToSlideView();
	    }
	}
	else
	{
	    if ( ! SlideShow::ShowingDetails() )
		SlideShow::RebuildDialog();
	}
    }



    /**
     * Recalculate remaining times per CD based on package sizes remaining
     * and data rate so far. Recalculation is only done each 'recalc_interval'
     * seconds unless 'force_recalc' is set to 'true'.
     *
     * @param force_recalc force recalculation even if timeout not reached yet
     * @return true if recalculated, false if not
     **/
    boolean RecalcRemainingTimes( boolean force_recalc )
    {
	if ( ! force_recalc
	    && time() < SlideShow::next_recalc_time )
	{
	    // Nothing to do (yet) - simply return
	    return false;
	}


	// Actually do recalculation

	integer elapsed = SlideShow::total_time_elapsed;

	if ( SlideShow::start_time >= 0 )
	{
	    elapsed = elapsed + time() - SlideShow::start_time;
	}

	if ( elapsed == 0 )
	{
	    // Called too early - no calculation possible yet.
	    // This happens regularly during initialization, so an error
	    // message wouldn't be a good idea here.

	    return false;
	}

	// This is the real thing.

	integer real_bytes_per_second = total_size_installed / elapsed;

	// But this turns out to be way to optimistic - RPM gets slower and
	// slower while installing. So let's add some safety margin to make
	// sure initial estimates are on the pessimistic side - the
	// installation being faster than initially estimated will be a
	// pleasant surprise to the user. Most users don't like it the other
	// way round.
	//
	// The "pessimistic factor" progressively decreases as the installation
	// proceeds.  It begins with about 1.7, i.e. the data transfer rate is
	// halved to what it looks like initially. It decreases to 1.0 towards
	// the end.

	float pessimistic_factor = 1.0;

	if ( total_size_to_install > 0 )
	    pessimistic_factor = 1.7 - tofloat( total_size_installed ) / tofloat( total_size_to_install );
	bytes_per_second = tointeger( tofloat( real_bytes_per_second ) / pessimistic_factor + 0.5 );

	if ( bytes_per_second < 1 )
	    bytes_per_second = 1;

	remaining_times_per_cd_per_src = [];

	// Recalculate remaining times for the individual CDs

	foreach ( list<integer> remaining_sizes_list, remaining_sizes_per_cd_per_src,
	``{
	    list<integer> remaining_times_list = [];
	    integer remaining_time = -1;

	    foreach ( integer remaining_size, remaining_sizes_list,
	    ``{
		remaining_time = remaining_size;

		if ( remaining_size > 0 )
		{
		    remaining_time = remaining_size / bytes_per_second;

		    if ( remaining_time < min_time_per_cd )
		    {
			// It takes at least this long for the CD drive to spin up and
			// for RPM to do _anything_. Times below this values are
			// ridiculously unrealistic.
			remaining_time = min_time_per_cd;
		    }
		    else if ( remaining_time > max_time_per_cd ) // clip off at 2 hours
		    {
			// When data throughput goes downhill (stalled network connection etc.),
			// cut off the predicted time at a reasonable maximum.
			remaining_time = max_time_per_cd;
		    }
		}
		remaining_times_list = add( remaining_times_list, remaining_time );
	    });

	    remaining_times_per_cd_per_src = add( remaining_times_per_cd_per_src, remaining_times_list );
	});


	// Recalculate slide interval

	if ( Slides::HaveSlides() )
	{
	    integer slides_remaining = size( Slides::slides ) - SlideShow::current_slide_no - 1;

	    if ( slides_remaining > 0 )
	    {
		// The remaining time for the rest of the slides depends on the
		// remaining time for the current CD only: This is where the
		// slide images and texts reside. Normally, only CD1 has slides
		// at all, i.e. the slide show must be finished when CD1 is
		// done.
		//
		// In addition to that, take elapsed time for current slide
		// into account so all slides get about the same time.

		integer time_remaining = remaining_times_per_cd_per_src[current_src_no-1, current_cd_no-1]:1 + time() - SlideShow::slide_start_time;
		SlideShow::slide_interval = time_remaining / slides_remaining;
		y2debug( "New slide interval: %1 - slides remaining: %2 - remaining time: %3",
			 SlideShow::slide_interval, slides_remaining, time_remaining );

		if ( SlideShow::slide_interval < SlideShow::slide_min_interval )
		{
		    SlideShow::slide_interval = SlideShow::slide_min_interval;
		    y2debug( "Resetting slide interval to min slide interval: %1", SlideShow::slide_interval );
		}

		if ( SlideShow::slide_interval > SlideShow::slide_max_interval )
		{
		    SlideShow::slide_interval = SlideShow::slide_max_interval;
		    y2debug( "Resetting slide interval to max slide interval: %1", SlideShow::slide_interval );
		}
	    }
	}

	SlideShow::next_recalc_time = time() + SlideShow::recalc_interval;

	return true;
    }

    /**
     * Switch unit to seconds if necessary and recalc everything accordingly.
     * @return true if just switched from sizes to seconds, false otherwise
     **/
    boolean SwitchToSecondsIfNecessary()
    {
	if ( unit_is_seconds
	    || time() < SlideShow::start_time + SlideShow::initial_recalc_delay )
	{
	    return false;	// no need to switch
	}

	RecalcRemainingTimes( true );	// force recalculation
	unit_is_seconds = true;

	return true;	// just switched
    }



/*****************************************************************************/
/******************  Callbacks and progress bars *****************************/
/*****************************************************************************/



    /**
     * Update progress widgets for the current CD: Label and ProgressBar.
     * Use global statistics variables for that.
     **/
    global void UpdateCurrentCdProgress(boolean silent_check)
    {
	if ( ! SanityCheck( silent_check ) )			return;
	if ( ! UI::WidgetExists(`cdStatisticsTable) )	return;


	//
	// Update table entries for current CD
	//

	integer remaining = remaining_sizes_per_cd_per_src [ current_src_no-1, current_cd_no-1 ]:0;
	UI::ChangeWidget(`id(`cdStatisticsTable ),
			 `Item( sformat( "cd(%1,%2)", current_src_no-1, current_cd_no-1), size_column ),
			 FormatRemainingSize( remaining ) );

	UI::ChangeWidget(`id(`cdStatisticsTable ),
			 `Item( sformat( "cd(%1,%2)", current_src_no-1, current_cd_no-1), pkg_count_column ),
			 FormatRemainingCount( remaining_pkg_count_per_cd_per_src [ current_src_no-1, current_cd_no-1 ]:0 ) );

	if ( unit_is_seconds )
	{
	    // Convert 'remaining' from size (bytes) to time (seconds)

	    remaining = remaining / bytes_per_second;

	    if ( remaining <= 0 )
		remaining = 0;

	    if ( remaining > max_time_per_cd )		// clip off at 2 hours
	    {
		// When data throughput goes downhill (stalled network connection etc.),
		// cut off the predicted time at a reasonable maximum.
		remaining = -max_time_per_cd;
	    }

	    UI::ChangeWidget(`id(`cdStatisticsTable ),
			     `Item( sformat( "cd(%1,%2)", current_src_no-1, current_cd_no-1), time_column ),
			     FormatTimeShowOverflow( remaining ) );
	}


	//
	// Update "total" table entries
	//

	UI::ChangeWidget(`id( `cdStatisticsTable ),
			 `Item( "total", size_column ),
			 FormatRemainingSize( TotalRemainingSize() ) );

	UI::ChangeWidget(`id( `cdStatisticsTable ),
			 `Item( "total", pkg_count_column ),
			 FormatRemainingCount( TotalRemainingPkgCount() ) );

	if ( unit_is_seconds )
	{
	    UI::ChangeWidget(`id( `cdStatisticsTable ), `Item( "total", time_column ),
			     FormatTimeShowOverflow( TotalRemainingTime() ) );

	}
    }

    /**
     * Update progress widgets
     **/
    void UpdateTotalProgress(boolean silent_check)
    {
	SlideShow::StageProgress( ( TotalInstalledSize() >> 10 ) * 100 / ( total_size_to_install >> 10 ), nil /*, SlideShow::GetProgressLabel()*/ );

	UpdateCurrentCdProgress(silent_check);

	if ( UI::WidgetExists(`nextMedia ) )
	{
	    string nextMedia = FormatNextMedia();

	    if ( nextMedia != "" || last_cd )
	    {
		UI::ChangeWidget(`nextMedia, `Value, nextMedia );
		UI::RecalcLayout();
		last_cd = false;
	    }
	}
    }


    /**
     * Returns a table widget item list for CD statistics
     **/
    list<term> CdStatisticsTableItems()
    {
	list<term> itemList = [];

	//
	// Add "Total" item - at the top so it is visible by default even if there are many items
	//

	{
	    // List column header for total remaining MB and time to install
	    string caption 	=  _("Total");
	    integer remaining 	= TotalRemainingSize();
	    string rem_size	= FormatRemainingSize( remaining );
	    string rem_count 	= FormatRemainingCount( TotalRemainingPkgCount() );
	    string rem_time 	= "";

	    if ( unit_is_seconds && bytes_per_second > 0 )
	    {
		rem_time = FormatTimeShowOverflow( TotalRemainingTime() );
	    }

	    itemList = add( itemList, SlideShow::TableItem( "total", caption, "   " + rem_size, "   " + rem_count, "   " + rem_time ) );
	}


	//
	// Now go through all repositories
	//

	integer src_no = 0;

	foreach ( list<integer> inst_src, remaining_sizes_per_cd_per_src, ``{
	    y2milestone( "src #%1: %2", src_no, inst_src );

	    if (ListSum(inst_src) > 0)	 // Ignore repositories from where there is nothing is to install
	    {
		// Add heading for this repository
		itemList = add( itemList, SlideShow::TableItem( sformat( "src(%1)", src_no ),
						     inst_src_names[ src_no ]:"", "", "", "" ) );

		integer cd_no = 0;

		foreach ( integer remaining, inst_src, ``{
		    if ( remaining > 0
			 || ( src_no+1 == current_src_no && cd_no+1 == current_cd_no ) )	// suppress current CD
		    {
			string caption  = sformat( "%1 %2", media_type, cd_no+1 );		// "CD 1" - column #0
			string rem_size = FormatRemainingSize( remaining );			// column #1
			string rem_count = FormatRemainingCount( remaining_pkg_count_per_cd_per_src[ src_no, cd_no ]:0 );
			string rem_time = "";

			if ( unit_is_seconds && bytes_per_second > 0 )
			{
			    remaining = remaining / bytes_per_second;
			    rem_time = FormatTime( remaining );					// column #2

			    if ( remaining > max_time_per_cd )		// clip off at 2 hours
			    {
				// When data throughput goes downhill (stalled network connection etc.),
				// cut off the predicted time at a reasonable maximum.
				// "%1" is a predefined maximum time.
				rem_time = FormatTimeShowOverflow( -max_time_per_cd );
			    }
			}

			itemList = add( itemList,
					SlideShow::TableItem( sformat("cd(%1,%2)", src_no, cd_no ),	// ID
						   caption, "   " + rem_size, "   " + rem_count, "   " + rem_time ) );
		    }

		    cd_no = cd_no + 1;
		});
	    }

	    src_no = src_no + 1;
	});

	if ( debug )
	{
	    y2milestone( "Remaining: %1", remaining_sizes_per_cd_per_src );
	    y2milestone( "CD table item list:\n%1", itemList );
	}

	return itemList;
    }



    /**
     * Progress display update
     * This is called via the packager's progress callbacks.
     *
     * @param pkg_percent	package percentage
     **/
    global void UpdateCurrentPackageProgress(integer pkg_percent)
    {
	SlideShow::SubProgress( pkg_percent, nil );
    }

    // update the download rate
    global void UpdateCurrentPackageRateProgress(integer pkg_percent, integer bps_avg, integer bps_current)
    {
	if( ! SlideShow::ShowingDetails() )	return;
	
	string new_text = nil;	// no update of the label
	if (bps_current > 0)
	{
	    // do not show the average download rate if the space is limited
	    if (SlideShow::textmode && SlideShow::display_width < 100)
	    {
		bps_avg = -1;
	    }
	    new_text = String::FormatRateMessage(provide_name + " - %1", bps_avg, bps_current);
	    new_text = sformat(_("Downloading %1 (download size %2)"), new_text, provide_size);
	}

	SlideShow::SubProgress( pkg_percent, new_text );
    }


    /**
     * Update progress widgets for all CDs.
     * Uses global statistics variables.
     **/
    global void UpdateAllCdProgress(boolean silent_check)
    {
	if ( ! SanityCheck( silent_check ) )	return;

	if ( unit_is_seconds )
	    RecalcRemainingTimes( true );	// force

	SlideShow::UpdateTable( CdStatisticsTableItems() );
    }


    /**
     * Return a CD's progress bar ID
     * @param src_no number of the repository (from 0 on)
     * @param cd_no number of the CD within that repository (from 0 on)
     **/
    string CdProgressId( integer src_no, integer cd_no )
    {
	return sformat( "Src %1 CD %2", src_no, cd_no );
    }


    /**
     * package start display update
     * - this is called at the end of a new package
     *
     * @param pkg_name		package name
     * @param deleting		Flag: deleting (true) or installing (false) package?
     **/
    global void SlideDisplayDone ( string	pkg_name,
				   integer	pkg_size,
				   boolean	deleting	)
    {
	if ( ! deleting )
	{
	    SubtractPackageSize( pkg_size );

	    if (SwitchToSecondsIfNecessary()
		|| RecalcRemainingTimes( false ) )	// no forced recalculation
	    {
		y2debug( "Updating progress for all CDs" );
		UpdateAllCdProgress(false);
	    }
	    else
	    {
		UpdateCurrentCdProgress(false);
	    }

	    UpdateTotalProgress(false);

	} // ! deleting

    }



    /**
     * package start display update
     * - this is called at the beginning of a new package
     *
     * @param pkg_name		package name
     * @param pkg_summary	package summary (short description)
     * @param deleting		Flag: deleting (true) or installing (false) package?
     **/
    global void SlideDisplayStart( string	pkg_name,
				   string	pkg_summary,
				   integer	pkg_size,
				   boolean	deleting	)
    {
	if ( ! SanityCheck( false ) )	return;

	// remove path
	pkg_name = StripPath(pkg_name);

	// remove release and .rpm suffix
	// pkg_name = StripReleaseNo( pkg_name );	// bug #154872

	if ( deleting )
	{
	    pkg_size = -1;

	    // This is a kind of misuse of insider knowledge: If there are packages to delete, this
	    // deletion comes first, and only then packages are installed. This, however, greatly
	    // distorts the estimated times based on data throughput so far: While packages are
	    // deleted, throughput is zero, and estimated times rise to infinity (they are cut off
	    // at max_time_per_cd to hide this). So we make sure the time spent deleting packages is
	    // not counted for estimating remaining times - reset the timer.
	    //
	    // Note: This will begin to fail when some day packages are deleted in the middle of the
	    // installaton process.

	    SlideShow::ResetTimer();
	}

	if ( pkg_summary == nil )
	    pkg_summary = "";

	string msg = "";

	if ( deleting )
	{
	    // Heading for the progress bar for the current package
	    // while it is deleted. "%1" is the package name.
	    msg = sformat( _("Deleting %1"), pkg_name );
	}
	else
	{
	    // package installation - summary text
	    // %1 is RPM name, %2 is installed (unpacked) size (e.g. 6.20MB)
	    msg = sformat( _("%1 (installed size %2)"), pkg_name, String::FormatSize( pkg_size ) );
	}


	//
	// Update package progress bar
	//
	SlideShow::SubProgress( 0, msg );

	// Update global progress bar
	string rem_string = "";
	integer tot_rem_t = TotalRemainingTime();

	rem_string = ( unit_is_seconds && bytes_per_second > 0 && tot_rem_t > 0) ?
		sformat("%1 / %2", FormatRemainingSize(TotalRemainingSize()), FormatTimeShowOverflow(tot_rem_t))
		: FormatRemainingSize(TotalRemainingSize());
	SlideShow::SetGlobalProgressLabel( SlideShow::CurrentStageDescription() + sformat(_(" (Remaining: %1)"), rem_string));

	//
	// Update (user visible) installation log
	//
	SlideShow::AppendMessageToInstLog( sformat( _("Installing %1"), msg) );

	//
	// Update the current slide if applicable
	//
	if ( SlideShow::ShowingSlide() )
	{
	    SlideShow::ChangeSlideIfNecessary();
	}
    }


    global void SlideGenericProvideStart (string pkg_name, integer sz,
	string pattern, boolean remote)
    {
	if ( ! SanityCheck( false ) )	return;
	if ( ! SlideShow::ShowingDetails() )	return;

	string provide_msg = "";
	    
	if (remote)
	{
	    provide_name = pkg_name;
	    provide_size = String::FormatSize(sz);

	    provide_msg = sformat(_("Downloading %1 (download size %2)"), provide_name, provide_size);
	}
	else
	{
	    provide_msg = pkg_name;
	}

	SlideShow::SubProgress( 0, provide_msg );

	//
	// Update (user visible) installation log
	// for remote download only
	//
	
	if( ! remote ) return;

	y2milestone( "Package '%1' is remote", pkg_name );
	
	// message in the installatino log, %1 is package name,
	// %2 is package size
	SlideShow::AppendMessageToInstLog( sformat (pattern, pkg_name, String::FormatSize (sz)) );
    }

    global void SlideDeltaApplyStart (string pkg_name) {
	if ( ! SanityCheck( false ) )	return;
	if ( ! SlideShow::ShowingDetails() )	return;

	SlideShow::SubProgress( 0, pkg_name );

	SlideShow::AppendMessageToInstLog( sformat (_("Applying delta RPM: %1"), pkg_name) );
    }


    /**
     * Package providal start
     */
    global void SlideProvideStart (string pkg_name, integer sz, boolean remote)
    {
	// message in the installatino log, %1 is package name,
	// %2 is package size
	SlideGenericProvideStart (pkg_name, sz, _("Downloading %1 (download size %2)"),
	    remote);
    }



}
