/*
 * \brief  Common representation of all storage devices
 * \author Norman Feske
 * \date   2018-05-02
 */

/*
 * Copyright (C) 2018 Genode Labs GmbH
 *
 * This file is part of the Genode OS framework, which is distributed
 * under the terms of the GNU Affero General Public License version 3.
 */

#ifndef _MODEL__STORAGE_DEVICE_H_
#define _MODEL__STORAGE_DEVICE_H_

#include "types.h"
#include "partition.h"
#include "capacity.h"
#include "xml.h"

namespace Sculpt { struct Storage_device; };


struct Sculpt::Storage_device
{
	enum State {
		UNKNOWN,  /* partition information not yet known */
		USED,     /* part_block is running and has reported partition info */
		RELEASED, /* partition info is known but part_block is not running */
		FAILED    /* driver failed to access the device */
	};

	Allocator &_alloc;

	typedef String<32> Label;

	Label const label;

	Capacity capacity; /* non-const because USB storage devices need to update it */

	State state { UNKNOWN };

	bool whole_device = false;

	Reconstructible<Partition> whole_device_partition {
		Partition::Args::whole_device(capacity) };

	Partitions partitions { };

	Attached_rom_dataspace _partitions_rom;

	unsigned _part_block_version = 0;

	/**
	 * Trigger the rediscovery of the device, e.g., after partitioning of after
	 * formatting the whole device.
	 */
	void rediscover()
	{
		state = UNKNOWN;
		_part_block_version++;

		Partition_update_policy policy(_alloc);
		partitions.update_from_xml(policy, Xml_node("<partitions/>"));
	}

	void process_part_block_report()
	{
		_partitions_rom.update();

		Xml_node const report = _partitions_rom.xml();
		if (!report.has_type("partitions"))
			return;

		whole_device = (report.attribute_value("type", String<16>()) == "disk");

		Partition_update_policy policy(_alloc);
		partitions.update_from_xml(policy, report);

		/*
		 * Import whole-device partition information.
		 *
		 * Ignore reports that come in while the device is in use. Otherwise,
		 * the reconstruction of 'whole_device_partition' would wrongly reset
		 * the partition state such as the 'file_system_inspected' flag.
		 */
		if (!whole_device_partition.constructed() || whole_device_partition->idle()) {
			whole_device_partition.construct(Partition::Args::whole_device(capacity));
			report.for_each_sub_node("partition", [&] (Xml_node partition) {
				if (partition.attribute_value("number", Partition::Number()) == "0")
					whole_device_partition.construct(Partition::Args::from_xml(partition)); });
		}

		/* finish initial discovery phase */
		if (state == UNKNOWN)
			state = RELEASED;
	}

	/**
	 * Constructor
	 *
	 * \param label  label of block device
	 * \param sigh   signal handler to be notified on partition-info updates
	 */
	Storage_device(Env &env, Allocator &alloc, Label const &label,
	               Capacity capacity, Signal_context_capability sigh)
	:
		_alloc(alloc), label(label), capacity(capacity),
		_partitions_rom(env, String<80>("report -> runtime/", label, ".part_block/partitions").string())
	{
		_partitions_rom.sigh(sigh);
		process_part_block_report();
	}

	~Storage_device()
	{
		/* release partition info */
		rediscover();
	}

	bool part_block_needed_for_discovery() const
	{
		return state == UNKNOWN;
	}

	bool part_block_needed_for_access() const
	{
		bool needed_for_access = false;
		partitions.for_each([&] (Partition const &partition) {
			needed_for_access |= partition.check_in_progress;
			needed_for_access |= partition.format_in_progress;
			needed_for_access |= partition.file_system_inspected;
			needed_for_access |= partition.fs_resize_in_progress;
		});

		if (whole_device_partition->format_in_progress
		 || whole_device_partition->check_in_progress) {
			needed_for_access = false;
		}

		return needed_for_access;
	}

	/**
	 * Generate content of start node for part_block
	 *
	 * \param service_name  name of server that provides the block device, or
	 *                      if invalid, request block device from parent.
	 */
	inline void gen_part_block_start_content(Xml_generator &xml, Label const &server_name) const;

	template <typename FN>
	void for_each_partition(FN const &fn) const
	{
		fn(*whole_device_partition);
		partitions.for_each([&] (Partition const &partition) { fn(partition); });
	}

	template <typename FN>
	void for_each_partition(FN const &fn)
	{
		fn(*whole_device_partition);
		partitions.for_each([&] (Partition &partition) { fn(partition); });
	}

	bool all_partitions_idle() const
	{
		bool idle = true;
		partitions.for_each([&] (Partition const &partition) {
			idle &= partition.idle(); });
		return idle;
	}

	bool relabel_in_progress() const
	{
		bool result = false;
		partitions.for_each([&] (Partition const &partition) {
			result |= partition.relabel_in_progress(); });
		return result;
	}

	bool gpt_expand_in_progress() const
	{
		bool result = false;
		partitions.for_each([&] (Partition const &partition) {
			result |= partition.gpt_expand_in_progress; });
		return result;
	}

	bool fs_resize_in_progress() const
	{
		bool result = false;
		partitions.for_each([&] (Partition const &partition) {
			result |= partition.fs_resize_in_progress; });
		return result;
	}

	bool expand_in_progress() const
	{
		return gpt_expand_in_progress() || fs_resize_in_progress();
	}

	Start_name relabel_start_name() const { return Start_name(label, ".relabel"); }
	Start_name expand_start_name()  const { return Start_name(label, ".expand");  }
};


void Sculpt::Storage_device::gen_part_block_start_content(Xml_generator &xml,
                                                          Label const &server_name) const
{
	xml.attribute("version", _part_block_version);

	gen_common_start_content(xml, Label(label, ".part_block"),
	                         Cap_quota{100}, Ram_quota{8*1024*1024});

	gen_named_node(xml, "binary", "part_block");

	xml.node("config", [&] () {
		xml.node("report", [&] () { xml.attribute("partitions", "yes"); });

		for (unsigned i = 1; i < 10; i++) {
			xml.node("policy", [&] () {
				xml.attribute("label",     i);
				xml.attribute("partition", i);
				xml.attribute("writeable", "yes");
			});
		}
	});

	gen_provides<Block::Session>(xml);

	xml.node("route", [&] () {

		gen_service_node<Block::Session>(xml, [&] () {
			if (server_name.valid())
				gen_named_node(xml, "child", server_name);
			else
				xml.node("parent", [&] () {
					xml.attribute("label", label); }); });

		gen_parent_rom_route(xml, "part_block");
		gen_parent_rom_route(xml, "ld.lib.so");
		gen_parent_route<Cpu_session> (xml);
		gen_parent_route<Pd_session>  (xml);
		gen_parent_route<Log_session> (xml);

		gen_service_node<Report::Session>(xml, [&] () {
			xml.attribute("label", "partitions");
			xml.node("parent", [&] () { }); });
	});
}

#endif /* _MODEL__STORAGE_DEVICE_H_ */
