/*
 * \brief  List of target areas where windows may be placed
 * \author Norman Feske
 * \date   2018-09-26
 */

/*
 * 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 _TARGET_LIST_H_
#define _TARGET_LIST_H_

/* local includes */
#include <target.h>

namespace Window_layouter { class Target_list; }


class Window_layouter::Target_list
{
	private:

		Allocator &_alloc;

		Registry<Registered<Target> > _targets { };

		/*
		 * Keep information of the rules internally to reproduce this
		 * information in 'gen_screens'.
		 */
		Constructible<Buffered_xml> _rules { };

		/**
		 * Calculate layout and populate '_targets'
		 *
		 * \param row  true of 'node' is a row, false if 'node' is a column
		 */
		void _process_rec(Xml_node node, Rect avail, bool row)
		{
			unsigned long const avail_px = row ? avail.w() : avail.h();

			char const *sub_node_type = row ? "column" : "row";
			char const *px_size_attr  = row ? "width"  : "height";

			unsigned long px_pos = row ? avail.x1() : avail.y1();

			unsigned long const default_weight = 1;

			/*
			 * Determinine space reserved in pixels, the total weight, and
			 * number of weighted rows/columns.
			 */
			unsigned long preserved_pixels = 0;
			unsigned long total_weight     = 0;
			unsigned long num_weighted     = 0;

			/* ignore weight if pixel size is provided */
			auto weight_attr_value = [&] (Xml_node node) {
				return node.has_attribute(px_size_attr)
				     ? 0 : node.attribute_value("weight", default_weight); };

			node.for_each_sub_node(sub_node_type, [&] (Xml_node child) {
				preserved_pixels += child.attribute_value(px_size_attr, 0UL);
				total_weight     += weight_attr_value(child);
				num_weighted     += child.has_attribute(px_size_attr) ? 0 : 1;
			});

			if (preserved_pixels > avail_px) {
				warning("layout does not fit in available area ", avail_px, ":", node);
				return;
			}

			/* amount of pixels we can use for weighed columns */
			unsigned long const weigthed_avail = avail_px - preserved_pixels;

			/*
			 * Calculate positions
			 */
			unsigned long count_weighted = 0;
			unsigned long used_weighted  = 0;
			node.for_each_sub_node(sub_node_type, [&] (Xml_node child) {

				auto calc_px_size = [&] () {

					unsigned long const px = child.attribute_value(px_size_attr, 0UL);
					if (px)
						return px;

					unsigned long const weight = weight_attr_value(child);
					if (weight && total_weight)
						return (((weight << 16)*weigthed_avail)/total_weight) >> 16;

					return 0UL;
				};

				bool const weighted = !child.has_attribute(px_size_attr);

				if (weighted)
					count_weighted++;

				/* true if target is the last weigthed column or row */
				bool const last_weighted = weighted
				                        && (count_weighted == num_weighted);

				unsigned long const px_size = last_weighted
				                            ? (weigthed_avail - used_weighted)
				                            : calc_px_size();

				if (weighted)
					used_weighted += px_size;

				Rect const sub_rect = row ? Rect(Point(px_pos, avail.y1()),
				                                 Area(px_size, avail.h()))
				                          : Rect(Point(avail.x1(), px_pos),
				                                 Area(avail.w(), px_size));

				_process_rec(child, sub_rect, !row);

				if (child.attribute_value("name", Target::Name()).valid())
					new (_alloc)
						Registered<Target>(_targets, child, sub_rect);

				px_pos += px_size;
			});
		}

		static constexpr unsigned MAX_LAYER = 9999;

		/**
		 * Generate windows for the top-most layer, starting at 'min_layer'
		 *
		 * \return  layer that was processed by the method
		 */
		unsigned _gen_top_most_layer(Xml_generator &xml, unsigned min_layer,
		                             Assign_list const &assignments) const
		{
			/* search targets for next matching layer */
			unsigned layer = MAX_LAYER;
			_targets.for_each([&] (Target const &target) {
				if (target.layer() >= min_layer && target.layer() <= layer)
					layer = target.layer(); });

			/* visit all windows on the layer */
			assignments.for_each([&] (Assign const &assign) {

				Target::Name const target_name = assign.target_name();

				/* search target by name */
				_targets.for_each([&] (Target const &target) {

					if (target.name() != target_name)
						return;

					if (target.layer() != layer)
						return;

					/* found target area, iterate though all assigned windows */
					assign.for_each_member([&] (Assign::Member const &member) {
						member.window.generate(xml); });
				});
			});

			return layer;
		}

	public:

		Target_list(Allocator &alloc) : _alloc(alloc) { }

		/*
		 * The 'rules' XML node is expected to contain at least one <screen>
		 * node. Subseqent <screen> nodes are ignored. The <screen> node may
		 * contain any number of <column> nodes. Each <column> node may contain
		 * any number of <row> nodes, which, in turn, can contain <column>
		 * nodes.
		 */
		void update_from_xml(Xml_node rules, Area screen_size)
		{
			_targets.for_each([&] (Registered<Target> &target) {
				destroy(_alloc, &target); });

			_rules.construct(_alloc, rules);

			if (!rules.has_sub_node("screen"))
				return;

			Xml_node const screen = rules.sub_node("screen");

			Rect const avail(Point(0, 0), screen_size);

			if (screen.attribute_value("name", Target::Name()).valid())
				new (_alloc)
					Registered<Target>(_targets, screen, avail);

			_process_rec(screen, avail, true);
		}

		void gen_layout(Xml_generator &xml, Assign_list const &assignments) const
		{
			unsigned min_layer = 0;

			/* iterate over layers, starting at top-most layer (0) */
			for (;;) {

				unsigned const layer =
					_gen_top_most_layer(xml, min_layer, assignments);

				if (layer == MAX_LAYER)
					break;

				/* skip layer in next iteration */
				min_layer = layer + 1;
			}
		}

		/**
		 * Generate screen-layout definitions for the 'rules' report
		 */
		void gen_screens(Xml_generator &xml) const
		{
			if (!_rules.constructed())
				return;

			_rules->xml().for_each_sub_node("screen", [&] (Xml_node screen) {
				screen.with_raw_node([&] (char const *start, size_t length) {
					xml.append(start, length); });
				xml.append("\n");
			});
		}

		template <typename FN>
		void for_each(FN const &fn) const { _targets.for_each(fn); }
};

#endif /* _TARGET_LIST_H_ */
