<?php
/**
 * Where the line between free and paid is drawn.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse;

defined( 'ABSPATH' ) || exit;

/**
 * Declares which settings belong to the paid tier, in one list.
 *
 * The gate itself lives in `Pro`, which reads `ui.pro`, `ui.proOptions` and
 * `ui.proMaxItems` out of a schema. Those hints could be written beside each
 * setting — and ten of them once were — but the tier split is a commercial
 * decision that gets revisited as a whole, and reading it meant opening five
 * schema files and grepping. It lives here instead: one file that answers
 * "what does Pro actually buy" without running anything.
 *
 * Three kinds of gate, because the honest answer differs by setting:
 *
 * - `settings` — the whole setting is paid. Free renders the schema default.
 * - `options`  — the setting is free, some of its *answers* are not. One
 *                colour behind the bar is free; a gradient, frosted glass or
 *                a picture are not. Gating the setting would take the free
 *                answer away along with the paid ones.
 * - `caps`     — how many rows of a list the free tier draws. The rest stay
 *                stored and come back with a licence.
 *
 * Nothing here ever writes to the database. A lapsed licence costs somebody
 * the *effect* of these settings for as long as it is lapsed, never the
 * settings themselves — see the note atop `Pro`.
 */
final class Pro_Tiers {

	/**
	 * The tier map, keyed by module id.
	 *
	 * @var array<string,array>
	 */
	const MAP = array(
		'bottom_menu' => array(

			/*
			 * Free builds a bar that works: up to four buttons, an icon and
			 * wording each, a colour, a height, a corner radius and a marker
			 * for the current page. What is paid for is the bar that does more
			 * than sit there — where it appears, who sees it, what a button
			 * can do besides open a link, and every finish beyond one colour.
			 */
			'settings' => array(
				// Showing the bar on some pages and not others.
				'show_on',
				'show_on_pages',
				'show_on_exclude_pages',
				'hide_for_admins',

				// Finishes beyond a single colour.
				'bg_gradient_start',
				'bg_gradient_end',
				'glass_tint',
				'glass_blur',
				'glass_border',
				'bg_image',
				'bg_image_overlay',
				'shadow_color',
				'bar_padding',
				'z_index',

				// The current page marked with more than a colour change.
				'active_pulse',
				'pulse_color',

				// A second palette for dark mode.
				'auto_dark_mode_enabled',
				'dark_bg_type',
				'dark_bg_color',
				'dark_bg_gradient_start',
				'dark_bg_gradient_end',
				'dark_bar_color',
				'dark_bar_active_color',
				'dark_label_color',
				'dark_label_active_color',
			),
			'options'  => array(
				'bg_type'          => array( 'gradient', 'glass', 'image' ),
				'bar_shadow'       => array( 'soft', 'medium', 'strong' ),
				'item_align'       => array( 'start', 'center', 'end' ),
				'overflow_mode'    => array( 'scroll' ),
				// A colour change, a pill and an underline are free: the pill
				// is what a phone bar looks like by default, and gating it
				// took five otherwise free looks out of the gallery with it —
				// a free gallery that is seven eighths locked reads as bait
				// rather than as a product. The line and the lifted button are
				// the ones that need the rest of the paid palette to look
				// deliberate.
				'active_indicator' => array( 'line', 'raised' ),
			),
			'caps'     => array(
				// Four buttons is a usable phone bar and the number most sites
				// end up with. The fifth is where a bar becomes a navigation
				// system, and that is the paid tier.
				'items' => 4,
			),
			'rows'     => array(
				'items' => array(
					'settings' => array(
						// Your own artwork instead of the bundled icons.
						'icon_image',
						'icon_image_active',
						'icon_image_width',
						'active_icon',

						// A panel that opens out of one button.
						'panel_content',

						// One button styled against the rest.
						'icon_color',
						'icon_active_color',
						'text_color',
						'text_active_color',
						'icon_bg_enabled',
						'icon_bg_type',
						'icon_bg_color',
						'icon_bg_gradient_start',
						'icon_bg_gradient_end',

						// Live counters, and who the button is for.
						'show_cart_count',
						'show_wishlist_count',
						'visibility',
						'visibility_roles',
					),
					'options'  => array(
						'icon_source' => array( 'image' ),
						// A link and a search box are free. Starting a chat,
						// dialling a number, opening a popup or sliding out a
						// panel are what turns a menu into a way of being
						// contacted, and that is what Pro sells.
						'link_type'   => array( 'whatsapp', 'call', 'popup', 'panel' ),
					),
				),
			),
		),
		'mobile_menu' => array(

			/*
			 * Free is a menu that opens, closes and looks like the site it is
			 * on. Paid is the panel that carries more than the menu — search,
			 * social links, free text — plus targeting and the motion.
			 */
			'settings' => array(
				'show_on',
				'show_on_pages',
				'show_on_exclude_pages',
				'hide_for_admins',
				'hide_selector',

				// A header bar that behaves as you scroll.
				'header_sticky',
				'header_hide_on_scroll',
				'header_offset_page',
				'header_shadow',

				// Panel finishes past its own colours.
				'panel_dividers',
				'panel_link_active',
				'overlay_color',

				// Everything the panel can carry besides the menu itself.
				'show_search',
				'search_placeholder',
				'social_links',
				'social_size',
				'panel_html',

				// Submenu behaviour past the plain accordion.
				'submenu_single',
				'submenu_indent',

				// Gestures.
				'swipe_to_close',
				'close_on_scroll',
			),
			'options'  => array(
				'header_brand'      => array( 'custom' ),
				'trigger_animation' => array( 'spin', 'arrow' ),
				'panel_side'        => array( 'fullscreen' ),
				'panel_motion'      => array( 'push' ),
				'submenu_mode'      => array( 'expanded' ),
			),
			'caps'     => array(
				// One or two social links read as a footnote under the menu.
				// A row of them is a feature.
				'social_links' => 2,
			),
		),
	);

	/**
	 * Marks every gated setting on the schemas as they are read.
	 *
	 * Hooked at priority 5 so a site that filters a schema afterwards can
	 * still move the line — an agency licence that unlocks a setting for its
	 * own build, say — rather than finding it stamped after the fact.
	 *
	 * @return void
	 */
	public function boot() {
		foreach ( array_keys( self::MAP ) as $module_id ) {
			add_filter( "menuverse_{$module_id}_schema", array( $this, 'mark' ), 5, 2 );
		}
	}

	/**
	 * Applies the tier map to one module's schema.
	 *
	 * @param array  $schema    Schema definition.
	 * @param string $module_id Module identifier.
	 * @return array
	 */
	public function mark( $schema, $module_id ) {
		if ( ! is_array( $schema ) || empty( $schema['properties'] ) || ! isset( self::MAP[ $module_id ] ) ) {
			return $schema;
		}

		$map = self::MAP[ $module_id ];

		$schema['properties'] = self::apply_level( $schema['properties'], $map );

		if ( ! empty( $schema['ui']['presets'] ) && is_array( $schema['ui']['presets'] ) ) {
			$schema['ui']['presets'] = self::mark_presets(
				$schema['ui']['presets'],
				$schema['properties'],
				isset( $map['presets'] ) ? $map['presets'] : array()
			);
		}

		foreach ( isset( $map['rows'] ) ? $map['rows'] : array() as $list => $row_map ) {
			if ( empty( $schema['properties'][ $list ]['items']['properties'] ) ) {
				continue;
			}

			$schema['properties'][ $list ]['items']['properties'] = self::apply_level(
				$schema['properties'][ $list ]['items']['properties'],
				$row_map
			);
		}

		return $schema;
	}

	/**
	 * Marks the ready made looks that need a licence.
	 *
	 * Derived rather than listed: a look is paid when it *needs* something
	 * paid — a gradient, frosted glass, a lifted marker. Writing the ids out
	 * by hand would let the gallery drift from the gate and offer somebody a
	 * look that renders as something else entirely, which is the worst version
	 * of this screen: it would appear to work and quietly not.
	 *
	 * A look that asks for nothing paid stays free even if it is the best one
	 * in the gallery. Holding back a look the free tier can actually render
	 * would be a gate on nothing — the settings behind it are free, so anybody
	 * could reproduce it by hand in a minute and feel cheated for having had
	 * to.
	 *
	 * @param array[] $presets    Declared looks.
	 * @param array   $properties Schema properties, already stamped.
	 * @param array   $forced     Ids to mark paid whatever their values say.
	 * @return array[]
	 */
	private static function mark_presets( array $presets, array $properties, array $forced ) {
		foreach ( $presets as $index => $preset ) {
			$values = isset( $preset['values'] ) ? (array) $preset['values'] : array();
			$paid   = in_array( isset( $preset['id'] ) ? $preset['id'] : '', $forced, true );

			foreach ( $values as $key => $value ) {
				if ( $paid ) {
					break;
				}

				if ( empty( $properties[ $key ]['ui'] ) ) {
					continue;
				}

				$ui = $properties[ $key ]['ui'];

				if ( ! empty( $ui['pro'] ) ) {
					$paid = true;
					break;
				}

				if ( ! empty( $ui['proOptions'] ) && in_array( $value, (array) $ui['proOptions'], true ) ) {
					$paid = true;
				}
			}

			$presets[ $index ]['pro'] = $paid;
		}

		return $presets;
	}

	/**
	 * Stamps one level of properties — the module's own, or one list's rows.
	 *
	 * A key in the map that no longer exists in the schema is ignored rather
	 * than created: a renamed setting should lose its gate and show up in the
	 * dashboard's Pro count as missing, not resurrect itself as an empty
	 * property nothing reads.
	 *
	 * @param array $properties Schema properties.
	 * @param array $map        Tier map for this level.
	 * @return array
	 */
	private static function apply_level( array $properties, array $map ) {
		foreach ( isset( $map['settings'] ) ? $map['settings'] : array() as $key ) {
			if ( isset( $properties[ $key ] ) ) {
				$properties[ $key ]['ui']['pro'] = true;
			}
		}

		foreach ( isset( $map['options'] ) ? $map['options'] : array() as $key => $options ) {
			if ( isset( $properties[ $key ] ) ) {
				$properties[ $key ]['ui']['proOptions'] = $options;
			}
		}

		foreach ( isset( $map['caps'] ) ? $map['caps'] : array() as $key => $cap ) {
			if ( isset( $properties[ $key ] ) ) {
				$properties[ $key ]['ui']['proMaxItems'] = (int) $cap;
			}
		}

		return $properties;
	}
}
