<?php
/**
 * Classic nav menu adapter.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Menus;

defined( 'ABSPATH' ) || exit;

/**
 * Adapts the `nav_menu` taxonomy and its `nav_menu_item` posts.
 */
final class Classic_Menu_Source implements Menu_Source {

	/**
	 * Returns the source identifier.
	 *
	 * @return string
	 */
	public function id() {
		return 'classic';
	}

	/**
	 * Returns the translated source label.
	 *
	 * @return string
	 */
	public function label() {
		return __( 'Menus', 'menuverse' );
	}

	/**
	 * Lists classic menus.
	 *
	 * @return array[]
	 */
	public function list_menus() {
		$menus = wp_get_nav_menus();

		if ( is_wp_error( $menus ) || ! is_array( $menus ) ) {
			return array();
		}

		$locations = array_flip( array_filter( (array) get_nav_menu_locations() ) );
		$results   = array();

		foreach ( $menus as $menu ) {
			$results[] = array(
				'source'    => $this->id(),
				'id'        => (int) $menu->term_id,
				'name'      => $menu->name,
				'count'     => (int) $menu->count,
				'locations' => isset( $locations[ $menu->term_id ] ) ? array( $locations[ $menu->term_id ] ) : array(),
			);
		}

		return $results;
	}

	/**
	 * Determines whether a classic menu exists.
	 *
	 * @param int|string $menu_id Term ID.
	 * @return bool
	 */
	public function exists( $menu_id ) {
		$menu = wp_get_nav_menu_object( (int) $menu_id );

		return $menu instanceof \WP_Term;
	}

	/**
	 * Builds the normalized tree for a classic menu.
	 *
	 * @param int|string $menu_id Term ID.
	 * @return Menu_Tree|null
	 */
	public function get_tree( $menu_id ) {
		$menu = wp_get_nav_menu_object( (int) $menu_id );

		if ( ! $menu instanceof \WP_Term ) {
			return null;
		}

		$cache_key = self::cache_key( $menu->term_id );
		$cached    = wp_cache_get( $cache_key, 'menuverse' );

		if ( is_array( $cached ) ) {
			return new Menu_Tree( $this->id(), (int) $menu->term_id, $menu->name, $cached );
		}

		$items = wp_get_nav_menu_items(
			$menu->term_id,
			array(
				'update_post_term_cache' => false,
			)
		);

		if ( ! is_array( $items ) ) {
			$items = array();
		}

		_prime_post_caches( wp_list_pluck( $items, 'ID' ), false, true );

		$flat = array();

		foreach ( $items as $item ) {
			$flat[ (int) $item->ID ] = array_merge(
				Menu_Tree::item_defaults(),
				array(
					'id'      => (int) $item->ID,
					'parent'  => (int) $item->menu_item_parent,
					'title'   => (string) $item->title,
					'url'     => (string) $item->url,
					'target'  => (string) $item->target,
					'rel'     => (string) $item->xfn,
					'classes' => array_values( array_filter( (array) $item->classes ) ),
					'extras'  => Item_Extras::get( $item->ID ),
				)
			);
		}

		$tree = $this->nest( $flat );

		wp_cache_set( $cache_key, $tree, 'menuverse', HOUR_IN_SECONDS );

		return new Menu_Tree( $this->id(), (int) $menu->term_id, $menu->name, $tree );
	}

	/**
	 * Returns the object cache key for a menu's tree.
	 *
	 * @param int|string $menu_id Term ID.
	 * @return string
	 */
	private static function cache_key( $menu_id ) {
		return 'tree_classic_' . (int) $menu_id;
	}

	/**
	 * Drops the persisted tree for a menu.
	 *
	 * Called by the registry when a menu changes. The key is spelled here
	 * because this is the only class that writes it.
	 *
	 * @param int|string $menu_id Term ID.
	 * @return void
	 */
	public function flush_cache( $menu_id ) {
		wp_cache_delete( self::cache_key( $menu_id ), 'menuverse' );
	}

	/**
	 * Turns a flat parent referencing list into a nested tree.
	 *
	 * @param array[] $flat Flat items keyed by ID.
	 * @return array[]
	 */
	private function nest( array $flat ) {
		$children = array();

		foreach ( $flat as $id => $item ) {
			$children[ $item['parent'] ][] = $id;
		}

		return $this->branch( 0, $flat, $children );
	}

	/**
	 * Recursively builds one branch of the tree.
	 *
	 * @param int     $parent_id Parent item ID, zero for the root.
	 * @param array[] $flat      Flat items keyed by ID.
	 * @param array[] $children  Child IDs keyed by parent ID.
	 * @return array[]
	 */
	private function branch( $parent_id, array $flat, array $children ) {
		if ( empty( $children[ $parent_id ] ) ) {
			return array();
		}

		$branch = array();

		foreach ( $children[ $parent_id ] as $id ) {
			$item = $flat[ $id ];

			unset( $item['parent'] );

			$item['children'] = $this->branch( $id, $flat, $children );
			$branch[]         = $item;
		}

		return $branch;
	}
}
