/*
 * This file is part of the Simutrans project under the Artistic License.
 * (see LICENSE.txt)
 */

#include <stdio.h>
#ifdef MULTI_THREAD
#include "../utils/simthread.h"
static pthread_mutex_t verbinde_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t calc_image_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

#include "leitung2.h"
#include "../simdebug.h"
#include "../world/simworld.h"
#include "simobj.h"
#include "../player/simplay.h"
#include "../display/simimg.h"
#include "../simfab.h"
#include "../simskin.h"

#include "../display/simgraph.h"

#include "../utils/cbuffer.h"

#include "../dataobj/translator.h"
#include "../dataobj/loadsave.h"
#include "../dataobj/powernet.h"
#include "../dataobj/environment.h"
#include "../dataobj/pakset_manager.h"

#include "../ground/grund.h"
#include "../builder/wegbauer.h"

const uint32 POWER_TO_MW = 12;

// use same precision as powernet
const uint8 leitung_t::FRACTION_PRECISION = powernet_t::FRACTION_PRECISION;

/**
 * returns possible directions for powerline on this tile
 */
ribi_t::ribi get_powerline_ribi(grund_t *gr)
{
	slope_t::type slope = gr->get_weg_hang();
	ribi_t::ribi ribi = (ribi_t::ribi)ribi_t::all;
	if (slope == slope_t::flat) {
		// respect possible directions for bridge and tunnel starts
		if (gr->ist_karten_boden()  &&  (gr->ist_tunnel()  ||  gr->ist_bruecke())) {
			ribi = ribi_t::doubles( ribi_type( gr->get_grund_hang() ) );
		}
	}
	else {
		ribi = ribi_t::doubles( ribi_type(slope) );
	}
	return ribi;
}

int leitung_t::gimme_neighbours(leitung_t **conn)
{
	int count = 0;
	grund_t *gr_base = welt->lookup(get_pos());
	ribi_t::ribi ribi = get_powerline_ribi(gr_base);
	for(int i=0; i<4; i++) {
		// get next connected tile (if there)
		grund_t *gr;
		conn[i] = NULL;
		if(  (ribi & ribi_t::nesw[i])  &&  gr_base->get_neighbour( gr, invalid_wt, ribi_t::nesw[i] ) ) {
			leitung_t *lt = gr->get_leitung();
			// check that we can connect to the other tile: correct slope,
			// both ground or both tunnel or both not tunnel
			bool const ok = (gr->ist_karten_boden()  &&  gr_base->ist_karten_boden())  ||  (gr->ist_tunnel()==gr_base->ist_tunnel());
			if(  lt  &&  (ribi_t::backward(ribi_t::nesw[i]) & get_powerline_ribi(gr))  &&  ok  ) {
				const player_t *owner = get_owner();
				const player_t *other = lt->get_owner();
				const player_t *super = welt->get_public_player();
				if (owner==other  ||  owner==super  ||  other==super) {
					conn[i] = lt;
					count++;
				}
			}
		}
	}
	return count;
}


fabrik_t *leitung_t::suche_fab_4(const koord pos)
{
	for(int k=0; k<4; k++) {
		fabrik_t *fab = fabrik_t::get_fab( pos+koord::nesw[k] );
		if(fab) {
			return fab;
		}
	}
	return NULL;
}


leitung_t::leitung_t(loadsave_t *file) : obj_t()
{
	image = IMG_EMPTY;
	set_net(NULL);
	ribi = ribi_t::none;
	is_transformer = false;
	rdwr(file);
}


leitung_t::leitung_t(koord3d pos, player_t *player) : obj_t(pos)
{
	image = IMG_EMPTY;
	set_net(NULL);
	set_owner( player );
	set_desc(way_builder_t::leitung_desc);
	is_transformer = false;
}


leitung_t::~leitung_t()
{
	if (welt->is_destroying()) {
		return;
	}

	grund_t *gr = welt->lookup(get_pos());
	if(gr) {
		leitung_t *conn[4];
		int neighbours = gimme_neighbours(conn);
		gr->obj_remove(this);
		set_flag( obj_t::not_on_map );

		if(neighbours>1) {
			// only reconnect if two connections ...
			bool first = true;
			for(int i=0; i<4; i++) {
				if(conn[i]!=NULL) {
					if(!first) {
						// replace both nets
						powernet_t *new_net = new powernet_t();
						conn[i]->replace(new_net);
					}
					first = false;
				}
			}
		}

		// recalc images
		for(int i=0; i<4; i++) {
			if(conn[i]!=NULL) {
				conn[i]->calc_neighbourhood();
			}
		}

		if(neighbours==0) {
			delete net;
		}
		player_t::add_maintenance(get_owner(), -get_maintenance(), powerline_wt);
	}
}


void leitung_t::cleanup(player_t *player)
{
	player_t::book_construction_costs(player, -desc->get_price()/2, get_pos().get_2d(), powerline_wt);
	mark_image_dirty( image, 0 );
}


/**
 * called during map rotation
 */
void leitung_t::rotate90()
{
	obj_t::rotate90();
	ribi = ribi_t::rotate90( ribi );
}


/**
 * replace networks connection
 * non-trivial to handle transformers correctly
 */
void leitung_t::replace(powernet_t* new_net)
{
	if (get_net() == new_net) {
		return;
	}

	// Use an explicit stack to handle the traversal iteratively to avoid stack overflow on larger maps
	vector_tpl<leitung_t*>todo_stack;
	todo_stack.reserve(256); // Small optimization to avoid frequent reallocations

	todo_stack.append(this);

	while (!todo_stack.empty()) {
		leitung_t* current = todo_stack.back();
		todo_stack.pop_back();

		// lready processed => next
		if (current->get_net() == new_net) {
			continue;
		}

		// Update the network for the current segment
		current->set_net(new_net);

		// Check and gather neighbors
		leitung_t* conn[4];
		if (current->gimme_neighbours(conn) > 0) {
			for (int i = 0; i < 4; i++) {
				// Only add neighbors that actually need updating
				if (conn[i] && conn[i]->get_net() != new_net) {
					todo_stack.append(conn[i]);
				}
			}
		}
	}
}


/**
 * Connect this piece of powerline to its neighbours
 * -> this can merge power networks
 */
void leitung_t::verbinde()
{
	// first get my own ...
	powernet_t *new_net = get_net();
//DBG_MESSAGE("leitung_t::verbinde()","Searching net at (%i,%i)",get_pos().x,get_pos().x);
	leitung_t * conn[4];
	if(gimme_neighbours(conn)>0) {
		for( uint8 i=0;  i<4 && new_net==NULL;  i++  ) {
			if(conn[i]) {
				new_net = conn[i]->get_net();
			}
		}
	}

//DBG_MESSAGE("leitung_t::verbinde()","Found net %p",new_net);

	// we are alone?
	if(get_net()==NULL) {
		if(new_net!=NULL) {
			replace(new_net);
		}
		else {
			// then we start a new net
			set_net(new powernet_t());
//DBG_MESSAGE("leitung_t::verbinde()","Creating new net %p",new_net);
		}
	}
	else if(new_net) {
		powernet_t *my_net = get_net();
		for( uint8 i=0;  i<4;  i++  ) {
			if(conn[i] && conn[i]->get_net()!=new_net) {
				conn[i]->replace(new_net);
			}
		}
		if(my_net && my_net!=new_net) {
			delete my_net;
		}
	}
}


void leitung_t::calc_image()
{
	is_crossing = false;
	const koord pos = get_pos().get_2d();
	bool snow = get_pos().z >= welt->get_snowline()  ||  welt->get_climate( get_pos().get_2d() ) == arctic_climate;

	grund_t *gr = welt->lookup(get_pos());
	if(gr==NULL) {
		// no valid ground; usually happens during building ...
		return;
	}
	if(gr->ist_bruecke() || (gr->get_typ()==grund_t::tunnelboden && gr->ist_karten_boden())) {
		// don't display on a bridge or in a tunnel)
		set_image(IMG_EMPTY);
		return;
	}

	image_id old_image = get_image();
	slope_t::type hang = gr->get_weg_hang();
	if(hang != slope_t::flat) {
		set_image( desc->get_slope_image_id(hang, snow));
	}
	else {
		if(gr->hat_wege()) {
			// crossing with road or rail
			weg_t* way = gr->get_weg_nr(0);
			if(ribi_t::is_straight_ew(way->get_ribi())) {
				set_image( desc->get_diagonal_image_id(ribi_t::north|ribi_t::east, snow));
			}
			else {
				set_image( desc->get_diagonal_image_id(ribi_t::south|ribi_t::east, snow));
			}
			is_crossing = true;
		}
		else {
			if(ribi_t::is_straight(ribi)  &&  !ribi_t::is_single(ribi)  &&  (pos.x+pos.y)&1) {
				// every second skip mast
				if(ribi_t::is_straight_ns(ribi)) {
					set_image( desc->get_diagonal_image_id(ribi_t::north|ribi_t::west, snow));
				}
				else {
					set_image( desc->get_diagonal_image_id(ribi_t::south|ribi_t::west, snow));
				}
			}
			else {
				set_image( desc->get_image_id(ribi, snow));
			}
		}
	}
	if (old_image != get_image()) {
		mark_image_dirty(old_image,0);
	}
}


/**
 * Recalculates the images of all neighbouring
 * powerlines and the powerline itself
 */
void leitung_t::calc_neighbourhood()
{
	leitung_t *conn[4];
	ribi = ribi_t::none;
	if(gimme_neighbours(conn)>0) {
		for( uint8 i=0;  i<4 ;  i++  ) {
			if(conn[i]  &&  conn[i]->get_net()==get_net()) {
				ribi |= ribi_t::nesw[i];
				conn[i]->add_ribi(ribi_t::backward(ribi_t::nesw[i]));
				conn[i]->calc_image();
			}
		}
	}
	set_flag( obj_t::dirty );
	calc_image();
}


void leitung_t::info(cbuffer_t & buf) const
{
	translator::get_obj_info(buf, desc->get_name());

	obj_t::info(buf);

	powernet_t * const net = get_net();

	buf.printf(translator::translate("Net ID: %p"), net);
	buf.printf("\n");
	//buf.printf(translator::translate("Capacity: %.0f MW"), (double)(net->get_max_capacity() >> POWER_TO_MW));
	//buf.printf("\n");
	buf.printf(translator::translate("Demand: %.0f MW"), (double)(net->get_demand() >> POWER_TO_MW));
	buf.printf("\n");
	buf.printf(translator::translate("Generation: %.0f MW"), (double)(net->get_supply() >> POWER_TO_MW));
	buf.printf("\n");
	buf.printf(translator::translate("Usage: %.0f %%"), (double)((100 * net->get_normal_demand()) >> powernet_t::FRACTION_PRECISION));
}


void leitung_t::finish_rd()
{
#ifdef MULTI_THREAD
	pthread_mutex_lock( &verbinde_mutex );
	verbinde();
	pthread_mutex_unlock( &verbinde_mutex );
	pthread_mutex_lock( &calc_image_mutex );
	calc_neighbourhood();
	pthread_mutex_unlock( &calc_image_mutex );
#else
	verbinde();
	calc_neighbourhood();
#endif

	grund_t *gr = welt->lookup(get_pos());
	assert(gr); (void)gr;

	player_t::add_maintenance(get_owner(), get_maintenance(), powerline_wt);
}

void leitung_t::rdwr(loadsave_t *file)
{
	xml_tag_t d( file, "leitung_t" );

	obj_t::rdwr(file);

	// no longer save power net pointer as it is no longer used
	if(  file->is_version_less(120, 4)  ) {
		uint32 value = 0;
		file->rdwr_long(value);
	}
	if(  file->is_loading()  ) {
		set_net(NULL);
	}

	if(get_typ()==leitung) {
		/* ATTENTION: during loading thus MUST not be called from the constructor!!!
		* (Otherwise it will be always true!
		*/
		if(file->is_version_atleast(102, 3)) {
			if(file->is_saving()) {
				const char *s = desc->get_name();
				file->rdwr_str(s);
			}
			else {
				char bname[128];
				file->rdwr_str(bname, lengthof(bname));

				const way_desc_t *desc = way_builder_t::get_desc(bname);
				if(desc==NULL) {
					desc = way_builder_t::get_desc(translator::compatibility_name(bname));
					if(desc==NULL) {
						pakset_manager_t::add_missing_paks( bname, MISSING_WAY );
						desc = way_builder_t::leitung_desc;

						if (!desc) {
							dbg->fatal("leitung_t::rdwr", "Trying to load powerline but pakset has none!");
						}
					}
					dbg->warning("leitung_t::rdwr()", "Unknown powerline %s replaced by %s", bname, desc->get_name() );
				}
				set_desc(desc);
			}
		}
		else {
			if (file->is_loading()) {
				set_desc(way_builder_t::leitung_desc);
			}
		}
	}
}

// returns NULL, if removal is allowed
// players can remove public owned powerlines
const char *leitung_t::get_removal_error(const player_t *player)
{
	if(  this->get_owner_nr()==PLAYER_PUBLIC_NR) {
		return NULL;
	}
	else if (player && (player->get_player_nr() == this->get_owner_nr()  ||  player->is_public_service())) {
		return NULL;
	}

	return "Der Besitzer erlaubt das Entfernen nicht";
}


sint64 leitung_t::get_maintenance() const
{
	if (!is_transformer) {
		return desc->get_maintenance();
	}
	else {
		return -welt->get_settings().cst_maintain_transformer;
	}
}


/************************************ from here on pump (source) stuff ********************************************/

freelist_iter_tpl<pumpe_t> pumpe_t::pl;


pumpe_t::pumpe_t(loadsave_t *file ) : leitung_t( koord3d::invalid, NULL )
{
	fab = NULL;
	power_supply = 0;
	is_transformer = true;
	rdwr( file );
}


pumpe_t::pumpe_t(koord3d pos, player_t *player) : leitung_t(pos, player)
{
	fab = NULL;
	power_supply = 0;
	is_transformer = true;
	player_t::book_construction_costs(player, welt->get_settings().cst_transformer, get_pos().get_2d(), powerline_wt);
}


pumpe_t::~pumpe_t()
{
	if(fab) {
		fab->remove_transformer_connected(this);
		fab = NULL;
	}
	if(  net != NULL  ) {
		net->sub_supply(power_supply);
	}
}

sync_result pumpe_t::sync_step(uint32 delta_t)
{
	if(  fab == NULL  ) {
		return SYNC_DELETE;
	}
	else if(  delta_t == 0  ) {
		return SYNC_OK;
	}

	// resolve image
	uint16 winter_offset = 0;
	if(  skinverwaltung_t::senke->get_count() > 3  &&  (get_pos().z >= welt->get_snowline()  ||  welt->get_climate( get_pos().get_2d() ) == arctic_climate)  ) {
		winter_offset = 2;
	}
	uint16 const image_offset = power_supply > 0 ? 1 : 0;
	image_id const new_image = skinverwaltung_t::pumpe->get_image_id(image_offset + winter_offset);

	// update image
	if(  image != new_image  ) {
		set_flag(obj_t::dirty);
		set_image(new_image);
	}
	return SYNC_OK;
}

void pumpe_t::set_net(powernet_t * p)
{
	powernet_t * p_old = get_net();
	if(  p_old != NULL  ) {
		p_old->sub_supply(power_supply);
	}

	leitung_t::set_net(p);

	if(  p != NULL  ) {
		p->add_supply(power_supply);
	}
}

void pumpe_t::set_power_supply(uint32 newsupply)
{
	// update power network
	powernet_t *const p = get_net();
	if(  p != NULL  ) {
		p->sub_supply(power_supply);
		p->add_supply(newsupply);
	}

	power_supply = newsupply;
}

sint32 pumpe_t::get_power_consumption() const
{
	powernet_t const *const p = get_net();
	return p->get_normal_demand();
}

void pumpe_t::rdwr(loadsave_t * file)
{
	xml_tag_t d( file, "pumpe_t" );

	leitung_t::rdwr(file);

	// current power state
	if(  file->is_version_atleast(120, 4)  ) {
		file->rdwr_long(power_supply);
	}
}


void pumpe_t::finish_rd()
{
	leitung_t::finish_rd();

	assert(get_net());

	if(  fab==NULL  ) {
		if(welt->lookup(get_pos())->ist_karten_boden()) {
			// on surface, check around
			fab = leitung_t::suche_fab_4(get_pos().get_2d());
		}
		else {
			// underground, check directly above
			fab = fabrik_t::get_fab(get_pos().get_2d());
		}
		if(  fab  ) {
			// only add when factory there
			fab->add_transformer_connected(this);
		}
	}

#ifdef MULTI_THREAD
	pthread_mutex_lock( &calc_image_mutex );
	set_image(skinverwaltung_t::pumpe->get_image_id(0));
	is_crossing = false;
	pthread_mutex_unlock( &calc_image_mutex );
#else
	set_image(skinverwaltung_t::pumpe->get_image_id(0));
	is_crossing = false;
#endif
}


void pumpe_t::info(cbuffer_t & buf) const
{
	translator::get_obj_info(buf, desc->get_name());

	obj_t::info( buf );

	buf.printf(translator::translate("Net ID: %p"), get_net());
	buf.printf("\n");
	buf.printf(translator::translate("Generation: %.0f MW"), (double)(power_supply >> POWER_TO_MW));
	buf.printf("\n");
	buf.printf(translator::translate("Usage: %.0f %%"), (double)((100 * get_net()->get_normal_demand()) >> powernet_t::FRACTION_PRECISION));
	buf.printf("\n"); // pad for consistent dialog size
}


/************************************ Distriubtion Transformer Code ********************************************/

freelist_iter_tpl<senke_t> senke_t::sl;
uint32 senke_t::payment_timer = 0;
bool senke_t::payout = false;

void senke_t::new_world()
{
	payment_timer = 0;
	payout = false;
}

void senke_t::static_rdwr(loadsave_t *file)
{
	if(  file->is_version_atleast(120, 4)  ) {
		file->rdwr_long(payment_timer);
	}
}

void senke_t::sync_handler(uint32 delta_t)
{
	// payment period (could be tied to game setting)
	const uint32 pay_period = PRODUCTION_DELTA_T * 10; // 10 seconds

	// revenue payout timer
	payment_timer += delta_t;
	payout = payment_timer >= pay_period;
	payment_timer %= pay_period;

	sl.sync_step(delta_t);
}


senke_t::senke_t(loadsave_t *file) : leitung_t( koord3d::invalid, NULL )
{
	fab = NULL;
	delta_t_sum = 0;
	next_t = 0;
	power_demand = 0;
	energy_acc = 0;
	is_transformer = true;

	rdwr( file );
}


senke_t::senke_t(koord3d pos, player_t *player) : leitung_t(pos, player)
{
	fab = NULL;
	delta_t_sum = 0;
	next_t = 0;
	power_demand = 0;
	energy_acc = 0;
	is_transformer = true;

	player_t::book_construction_costs(player, welt->get_settings().cst_transformer, get_pos().get_2d(), powerline_wt);
}

senke_t::~senke_t()
{
	// one last final income
	pay_revenue();

	if(fab!=NULL) {
		fab->remove_transformer_connected(this);
		fab = NULL;
	}
	if(  net != NULL  ) {
		net->sub_demand(power_demand);
	}
}


void senke_t::pay_revenue()
{
	// megajoules (megawatt seconds) per cent
	const uint64 mjpc = (1 << (POWER_TO_MW-10)) * welt->get_settings().cst_kw_per_credit;

	// calculate payment in cent
	const sint64 payment = (sint64)(energy_acc / mjpc);

	// make payment
	if(  payment  >  0  ) {
		// enough has accumulated for a payment
		get_owner()->book_revenue( payment, get_pos().get_2d(), powerline_wt );

		// remove payment from accumulator
		energy_acc %= mjpc;
	}
}

void senke_t::set_net(powernet_t * p)
{
	powernet_t * p_old = get_net();
	if(  p_old != NULL  ) {
		p_old->sub_demand(power_demand);
	}

	leitung_t::set_net(p);

	if(  p != NULL  ) {
		p->add_demand(power_demand);
	}
}

void senke_t::set_power_demand(uint32 newdemand)
{
	// update power network
	powernet_t *const p = get_net();
	if(  p != NULL  ) {
		p->sub_demand(power_demand);
		p->add_demand(newdemand);
	}

	power_demand = newdemand;
}

sint32 senke_t::get_power_satisfaction() const
{
	powernet_t const *const p = get_net();
	return p->get_normal_supply();
}

sync_result senke_t::sync_step(uint32 delta_t)
{
	if(fab==NULL) {
		return SYNC_DELETE;
	}

	// advance timers
	delta_t_sum += delta_t;
	next_t += delta_t;

	// change graphics at most 16 times a second
	if(  next_t > PRODUCTION_DELTA_T / 16  ) {
		// enforce timer periods
		delta_t_sum %= PRODUCTION_DELTA_T; // 1 second
		next_t %= PRODUCTION_DELTA_T / 16; // 1/16 seconds

		// determine pwm period for image change
		uint32 pwm_period = 0;
		const sint32 satisfaction = get_net()->get_normal_supply();
		if(  satisfaction  >=  1 << powernet_t::FRACTION_PRECISION  ) {
			// always on
			pwm_period = PRODUCTION_DELTA_T;
		}
		else if(  satisfaction  >=  ((7 << powernet_t::FRACTION_PRECISION) / 8)  ) {
			// limit to at most 7/8 of a second
			pwm_period = 7 * PRODUCTION_DELTA_T / 8;
		}
		else if(  satisfaction  >  ((1 << powernet_t::FRACTION_PRECISION) / 8)  ) {
			// duty cycle based on power satisfaction
			pwm_period = (uint32)(((uint64)PRODUCTION_DELTA_T * (uint64)satisfaction) >> powernet_t::FRACTION_PRECISION);
		}
		else if(  satisfaction  >  0  ) {
			// limit to at least 1/8 of a second
			pwm_period = PRODUCTION_DELTA_T / 8;
		}

		// determine image with PWM logic
		const uint16 work_offset = (delta_t_sum < pwm_period) ? 1 : 0;

		// apply seasonal image offset
		uint16 winter_offset = 0;
		if(  skinverwaltung_t::senke->get_count() > 3  &&  (get_pos().z >= welt->get_snowline()  ||
			 welt->get_climate(get_pos().get_2d()) == arctic_climate)  ) {
			winter_offset = 2;
		}

		// update displayed image
		image_id new_image = skinverwaltung_t::senke->get_image_id(work_offset + winter_offset);
		if(  image != new_image  ) {
			set_flag( obj_t::dirty );
			set_image( new_image );
		}
	}

	// energy metering logic
	energy_acc += ((uint64)power_demand * (uint64)get_net()->get_normal_supply() * (uint64)delta_t) / ((uint64)PRODUCTION_DELTA_T << powernet_t::FRACTION_PRECISION);

	if (payout) {
		pay_revenue();
	}

	return SYNC_OK;
}


void senke_t::rdwr(loadsave_t *file)
{
	xml_tag_t d( file, "senke_t" );

	leitung_t::rdwr(file);

	// current power state
	if(  file->is_version_atleast(120, 4)  ) {
		file->rdwr_longlong((sint64 &)energy_acc);
		file->rdwr_long(power_demand);
	}
	else if (file->is_loading()) {
		energy_acc = 0;
		power_demand = 0;
	}

	if (file->is_version_atleast(122, 1)) {
		file->rdwr_long(delta_t_sum);
		file->rdwr_long(next_t);
	}
	else if (file->is_loading()) {
		delta_t_sum = 0;
		next_t = 0;
	}
}

void senke_t::finish_rd()
{
	leitung_t::finish_rd();

	assert(get_net());

	if(  fab==NULL  ) {
		if(welt->lookup(get_pos())->ist_karten_boden()) {
			// on surface, check around
			fab = leitung_t::suche_fab_4(get_pos().get_2d());
		}
		else {
			// underground, check directly above
			fab = fabrik_t::get_fab(get_pos().get_2d());
		}
		if(  fab  ) {
			fab->add_transformer_connected(this);
		}
	}

#ifdef MULTI_THREAD
	pthread_mutex_lock( &calc_image_mutex );
#endif
	set_image(skinverwaltung_t::senke->get_image_id(0));
	is_crossing = false;
#ifdef MULTI_THREAD
	pthread_mutex_unlock( &calc_image_mutex );
#endif
}

void senke_t::info(cbuffer_t & buf) const
{
	translator::get_obj_info(buf, desc->get_name());

	obj_t::info(buf);

	buf.printf(translator::translate("Net ID: %p"), get_net());
	buf.printf("\n");
	buf.printf(translator::translate("Demand: %.0f MW"), (double)(power_demand >> POWER_TO_MW));
	buf.printf("\n");
	buf.printf(translator::translate("Supplied: %.0f %%"), (double)((100 * get_net()->get_normal_supply()) >> powernet_t::FRACTION_PRECISION));
	buf.printf("\n"); // pad for consistent dialog size
}
