/*
 * Copyright 2024, ttcoder
 * All rights reserved. Distributed under the terms of the MIT license.
 */


// genode's base.a
#include <base/attached_rom_dataspace.h>  // member

// libc.lib.so
#include <stdio.h>

// haiku.lib.so
#include <app/MessageRunner.h>
#include <app/Roster.h>
#include <interface/Alert.h>
#include <interface/Button.h>
#include <interface/StringView.h>
#include <interface/Window.h>



class Win : public BWindow
{
	typedef BWindow inherited;

public:

		Win();
		
		void MessageReceived( BMessage * msg ) override;
		

private:  // Code

		void handlePartitionsUpdate();
		
		void rebuildGui();
		void lineFeed( BRect & r );
		
		void launchApp( const char * leafname, unsigned ram_mb, BString config );
		void terminateChild( const char * leafname );
		BString confPartBlock( bool enable_readwrite );


private:  // Data

		Genode::Attached_rom_dataspace	partitionsRx;    // access ROM "part_block -> partitions" from report_rom
		Genode::Signal_handler<Win>		partUpdatesSigH; // get ROM update notifications
		bool roughFlag;  // internal signaling (between Genode sighandler area and libc area)


private:  // Constants

		enum
		{
			msgbase = 'msg0',
			
			POLL_PARTITION_LIST,  // internal msg
			
			WIPE_EVERYTHING,
			ADD,
			//DEL,
			FORMAT
		};
};



//#pragma mark -


Win::Win()
:	inherited( BRect(50., 50., 650., 650.), "Partition setup GPT", B_DOCUMENT_WINDOW, B_QUIT_ON_WINDOW_CLOSE | B_ASYNCHRONOUS_CONTROLS )
	,partitionsRx( be_app->Env(), "partitions" )
	,partUpdatesSigH( be_app->Env().ep(), *this, &Win::handlePartitionsUpdate )
	,roughFlag( false )
{
	// T-
	AddChild( new BView(Bounds(), "root", B_FOLLOW_ALL, B_WILL_DRAW) );
	
	// T+
	partitionsRx.sigh( partUpdatesSigH );
	handlePartitionsUpdate();
	launchApp( "part_block", 10, confPartBlock(false) );
	
	// poll <roughFlag>
	(void)new BMessageRunner( this, new BMessage(POLL_PARTITION_LIST), 99000, -1 );
}



void Win::handlePartitionsUpdate()
{
	//Genode::log( "handlePartitionsUpdate" );
	
	// We must absolutely avoid executing libc/Be code here in this Genode signal handler.
	// Even calling BMessenger code would trip asserts -- let's merely set a flag for the Be side to read:
	roughFlag = true;
}


void Win::MessageReceived( BMessage * msg )
{
	switch( msg->what )
	{
		case POLL_PARTITION_LIST:
			if( false == roughFlag )
				//
				break;
			
			roughFlag = false;
			
			rebuildGui();
		break;
		
		case WIPE_EVERYTHING:
		{
			if( 1 == (new BAlert("confirmdialog", "ERASE ALL PARTITIONS on the drive ?", "ERASE ALL", "Cancel"))->Go() )
				//
				break;
				//
			
			terminateChild( "part_block" );
			
			// "initialize=yes" means start over (reset the partition table)
			launchApp( "gpt_write", 2,
				"<config verbose=\"yes\" initialize=\"yes\" align=\"4K\">"
				"	<actions>"
		// we *have* to create at least one partition, otherwise the partitioning scheme is not detected by part_block, and the "Add new partition" button would never get SetEnabled()
		//		"		<add entry=\"1\" type=\"BIOS\"  label=\"GRUB BIOS\"                  size=\"1M\" />"
				"		<add type=\"BIOS\"  label=\"GRUB BIOS\"                  size=\"1M\" />"
		//		"		<add type=\"BIOS\"  label=\"GRUB BIOS\"  start=\"2048\"  size=\"1M\" />"
		//!				<add entry="2" type="EFI"   label="EFI System" start="4096"  size="16M"/>
				"	</actions>"
	 			"</config>"
				);
			snooze( 1999000 );
			terminateChild( "gpt_write" );
			
			launchApp( "part_block", 10, confPartBlock(false) );
		}
		break;
		
		case ADD:
		{
			msg->PrintToStream();
			
			int64 remaining_bytes = 0;
			int64 remaining_in_mb = 0;
			{
				if( B_OK != msg->FindInt64("remaining-bytes-for-partitioning", &remaining_bytes ) )
					//
					break;
					//
				
				remaining_in_mb = remaining_bytes;
				remaining_in_mb /= (1024*1024);
			}
			
			terminateChild( "part_block" );
			
			///later-2: add more safeguards ? Gray out "Add" button if no space left ?
			BString s;
			s <<
				// "initialize=no" to preserve pre-existing partitions:
				"<config verbose=\"yes\" initialize=\"no\" align=\"4K\">"
				"	<actions>"
				;
			// Try to create a 100 GiB partition, but cap that size if there is not enough room left on the drive:
			if( remaining_bytes > (100LL << 30LL) )
			{
	///ToDo: type = NTFS, not FAT32/"BDP" ...
				s << "		<add  type=\"BDP\"  label=\"FAT32 Data\" size=\"100G\"/>" ;
			}
			else
			{
				Genode::warning( "...... ------ not enough space for a 100 GiB partition, capping to ", remaining_in_mb, " MiB  ------ ......." );
				
				s << "		<add  type=\"BDP\"  label=\"FAT32 Data\" size=\"" << remaining_in_mb << "M\"/>" ;
			}
			s <<
				"	</actions>"
	 			"</config>"
				;
			launchApp( "gpt_write", 2, s );
	///ToDo-2: instead of snoozing, exploit the return status (requires registrar support?) from gpt_write: "_env.parent().exit(success ? 0 : 1);"
			snooze( 1999000 );
			terminateChild( "gpt_write" );
			
			launchApp( "part_block", 10, confPartBlock(true) );
			
			///later: PostMessage( FORMAT + AddInt32(pa-num) ) ? That would require reliably getting the newly-created partition number
		}
		break;
		
		case FORMAT:
		{
			msg->PrintToStream();
			
			if( 1 == (new BAlert("confirmdialog", "FORMAT this partition ? All files on this partition will be lost", "FORMAT", "Cancel"))->Go() )
				//
				break;
				//
			
			int32 par_number = -1;
			int64 sector_count = -1;
			if( B_OK != msg->FindInt32("partition-number", &par_number)
				|| B_OK != msg->FindInt64("count-blocks", &sector_count ) )
				//
				break;
				//
			
			BString s;
			s <<
	 			"<config ld_verbose=\"no\">"
		 		"	<libc  rtc=\"/dev/rtc\" stdin=\"/dev/null\" stdout=\"/dev/log\" stderr=\"/dev/log\" />"
		 		"	<env key=\"_\" value=\"/boot/apps/genode/mkntfs\"/>"
		 		"	<arg value=\"/boot/apps/genode/mkntfs\"/>"
		//		"	<arg value=\"--verbose\"/>"
		// 		"	<arg value=\"--no-action\"/>"  // dry run
				"	<arg value=\"--fast\"/>"
		// using --force to force mkntfs past the "not a block device" error...:
				"	<arg value=\"--force\"/>"
				"	<arg value=\"/blockdevs/partition_blocks\"/>"
				;
			// Explicitely specify the sector count here (one naked argument following the device path),
			// otherwise mkntfs.c attempts to guess and calls some FreeBSD ioctl(DIOCGMEDIASIZE) thingy
			// which wrongly returns e.g. count_sectors value "2" (!)
			s <<
				"	<arg value=\"" << sector_count << "\"/>"  // e.g. a naked "100000" token
				"	<vfs>"
				"		<dir name=\"dev\">"
				"			<rtc/>"
				"			<log/>"
				"			<null/>"
				"		</dir>"
				;
			s <<
				"		<dir name=\"blockdevs\">"
				"			<block label=\"partition_" << par_number << "\" name=\"partition_blocks\"  block_buffer_count=\"128\" />"
				"		</dir>"
				"	</vfs>"
	 			"</config>"
				;
			launchApp( "mkntfs", 3, s );
			snooze( 7999000 );  // For QEMU, where it takes way longer than 2 s...
		//	snooze( 2100100 );
			terminateChild( "mkntfs" );
		}
		break;
		
		default:
			inherited::MessageReceived( msg );
	}
}



//#pragma mark -


void Win::lineFeed( BRect & r )
{
	r.OffsetTo( 10., r.bottom +5. );
}


void Win::rebuildGui()
{
	//Genode::log( " rebuildGui:" );
	
	// GUI: remove old
	BView * p = ChildAt( 0 );
	while( p->ChildAt(0) )
	{
		BView * c = p->ChildAt( 0 );
		c->RemoveSelf();
		delete c;
	}
	
	// GUI: prep new
	ChildAt(0)->SetViewUIColor( B_PANEL_BACKGROUND_COLOR );
	BRect r( Bounds() );
	r.bottom = r.top + 25.;
	
	// List: Rx
	partitionsRx.update();
	
	// List: analyze/apply
	try
	{
		Genode::Xml_node top = partitionsRx.xml();
		
		const Genode::String<32> none( "?" );
		auto scheme = top.attribute_value( "type", none );
		Genode::log( "Partitioning scheme: <", scheme, ">" );
		BString s; s << "\"" << scheme.string() << "\"" << " partitioning scheme:";
		;
		ChildAt(0)->AddChild( new BStringView(r, "heading", s) );
		lineFeed( r );
		
		/* stats */
		
		const bool has_valid_partitioning = (scheme == "gpt");  // we do not support "mbr" scheme, with gpt_write we can only edit GPT
		int64 remaining_available = 0;
		bool  has_one_or_more_ntfs = false;
		
		/****** Top: labels ******/
		
		top.for_each_sub_node([&] (Genode::Xml_node part) {
			auto const type = part.attribute_value( "type", none );
			unsigned long const num = part.attribute_value( "number", 0UL );
			unsigned long const sector_sz = part.attribute_value( "block_size", 0UL );
			unsigned long const sector_count = part.attribute_value( "length", 0UL );
			unsigned long const expandable = part.attribute_value( "expandable", 0UL );
			
			if( int64(expandable)*int64(sector_sz) > remaining_available )
				remaining_available = int64(expandable) * int64(sector_sz);
			
			int64 gigabytes = 0;
			{
				gigabytes = sector_sz;
				gigabytes *= sector_count;
				gigabytes /= (1<<30);
			}
			
	// as per haiku/src/add-ons/kernel/partitioning_systems/gpt_known_guids.h , except we add a dash since
	// Genode's gpt_write actually adds another dash after 87c0 etc:
	static const char mswin_partition[] = "ebd0a0a2-b9e5-4433-87c0-68b6b72";//699c7";
	//printf( " num %ld : type <%s> vs NTFS (FAT32 ?!) type <%s>\n", num, type.string(), mswin_partition );
			if( type == mswin_partition
				&& num <= 2  ///the rest of our infrastructure/distro only looks for an NTFS partition among the first two, partition 3 and above are not considered (currently)
				)
				has_one_or_more_ntfs = true;
			
			BRect r1, r2;
			BMessage m( FORMAT );
			{
				r1 = r; r1.right = r1.left + 50.;
				r2 = r; r2.left = r1.right + 10.;
				
				m.AddInt32( "partition-number", num );
				m.AddInt64( "count-blocks", sector_count );
			}
			auto format_button = new BButton( r1, "format", "Format", new BMessage(m) );
			if( num < 1 || sector_count < 10000 )
				format_button->SetEnabled( false );
			ChildAt(0)->AddChild( format_button );
			
			BString s;
			{
				s << " Partition " << num << ": ";
				if( gigabytes > 0 )
					s << gigabytes << " GiB";
				else
					s << (sector_count*sector_sz/(1<<20)) << " MiB";  // no risk of math overflow a byte count is < 1024 MiB (less than 1 GiB)
				s << " (" << sector_count << " blocks of " << sector_sz << " bytes)";
			}
			
			//printf( "partition %ld:  %ld blocks\n", num, sector_count );
			
			ChildAt(0)->AddChild( new BStringView(r2, "item", s) );
			lineFeed( r );
		});
		
		/****** Middle: diagnostic **/
		
		BString assess;
		if( false == has_valid_partitioning )
			assess = "No (suitable) storage partition found ; you may wipe the drive and re-configure it with 'Clear all partitions' (this will DESTROY ALL existing files if any)";
//			assess = "This drive does not contain a (usable) set of partitions ; you may wipe and start over with 'Clear all partitions' (THIS WILL DESTROY existing files if any)";
		else if( false == has_one_or_more_ntfs )
			assess = "Ready to create a new NTFS partitions -- click 'Add partition', then 'Format' it ";
		else
			assess = "At least one usable NTFS partition has been found. You may add more if desired";
		
		ChildAt(0)->AddChild( new BStringView(r, "diagnostic", assess) );
		lineFeed( r );
		
		/****** Bottom: buttons ******/
		
		r = Bounds();
		r.top = r.bottom - 30.;
		BMessage m( ADD );
		{
			m.AddInt64( "remaining-bytes-for-partitioning", remaining_available );
		}
		auto addp = new BButton(r, "add", "Add partition", new BMessage(m));
		ChildAt(0)->AddChild( addp );
		addp->SetEnabled( has_valid_partitioning );
		
		r.OffsetBy( 0., -(r.Height() +1.) );
		ChildAt(0)->AddChild( new BButton(r, "wipe", "Clear all partitions", new BMessage(WIPE_EVERYTHING)) );
	}
	catch (...)
	{
		Genode::error( "XML exception while parsing partitions list" );
	}
	
	/*
	-- Sample reports --
	
	Unpartitioned (whole) disk, as pseudo part "0":
		[init -> storage_report_rom]   <partitions type="disk">
		[init -> storage_report_rom]    <partition number="0" start="0" length="131072" block_size="512"/>
		[init -> storage_report_rom]   </partitions>
	
	MBR partitions:
		[init -> storage_report_rom]   <partitions type="mbr">
		[init -> storage_report_rom]    <partition number="1" start="2048" length="1021952" block_size="512" type="7"/>
		[init -> storage_report_rom]    <partition number="2" start="1024000" length="246923210" block_size="512" type="7"/>
		..
		[init -> storage_report_rom]    <partition number="5" start="247947273" length="123186357" block_size="512" type="7"/>
		..
		[init -> storage_report_rom]   </partitions>
	
	GPT partitions:
		[init -> storage_report_rom]   <partitions type="gpt" total_blocks="131072" gpt_total="131005" gpt_used="131039">
		[init -> storage_report_rom]    <partition number="1" name="FAT32 Data" type="ebd0a0a2-b9e5-4433-87c0-68b6b72699c7" guid="e6db2c6f-2e09-4013-a94a-c9b98536db2a" start="40" length="131039" block_size="512"/>
		[init -> storage_report_rom]   </partitions>
	*/
	
	//Genode::log( " ...rebuildGui done" );
}



//#pragma mark -


///XXXXXX temporarily done here in the app, but this should be done instead in some mount_daemon (hosted by registrar ?)
void Win::launchApp( const char * leafname, unsigned ram_mb, BString config )
{
	status_t res = 0;
	
	BMessage ge_msg;
	{
		ge_msg.AddInt32( "Genode:InitXml:ram_MB", ram_mb );
		ge_msg.AddString("Genode:InitXml:Config", config );
	}
	
	BString s;
	s << "/boot/apps/genode/" << leafname;
	
	entry_ref ref;
	res = get_ref_for_path( s, &ref );
	if( res != 0 )
		printf("<%s> res: 0x%x *******\n", leafname, res);
	
	res = be_roster->Launch( &ref, &ge_msg );
	if( res != 0 )
		printf("<%s> launch res: 0x%x *******\n", leafname, res);
}



void Win::terminateChild( const char * leafname )
{
	///later: to 'terminate' we use Launch/Toggle, seems reliable, but come up with real non-toggling 'terminate' later ?
	launchApp( leafname, 0, "" );
}


BString Win::confPartBlock( bool enable_readwrite )
{
	BString s;
	
	s <<
		///ToDo: the field sent to registrar is named "Genode:InitXml:Config", yet in some cases (like here) we also add a <provides></provides> node, which works ok thanks to taking advantage of the concatenation... Clean this up somehow, create a separate "Genode:Routing" field ?
		"		<provides> <service name=\"Block\"/> </provides>\n"
		"		<config use_gpt=\"yes\" io_buffer=\"4M\"  >\n"
		"			<report partitions=\"yes\" />\n"
		;
/////	if( enable_readwrite )
	{
		s <<
			//"			<policy  ...  partition=\"0\" ? Nope, don't allow "whole-disk" access
			"			<policy label_suffix=\"partition_1\"  partition=\"1\"  writeable=\"yes\"/>\n"
			"			<policy label_suffix=\"partition_2\"  partition=\"2\"  writeable=\"yes\"/>\n"
			"			<policy label_suffix=\"partition_3\"  partition=\"3\"  writeable=\"yes\"/>\n"
			"			<policy label_suffix=\"partition_4\"  partition=\"4\"  writeable=\"yes\"/>\n"
			;
	}
	s <<
		"		</config>\n"
		;
	
	return s;
}





//#pragma mark -


class App : public BApplication
{
	typedef BApplication inherited;

public:
		App()
		:	inherited( "application/x-vnd.HoG-PartitionSetupGPT" )
		{
		}
		
		void ReadyToRun() override
		{
			(new Win)->Show();
		}
};


int main()
{
	App().Run();
	
	return 0;
}


