<?php
/**
 * Plugin Name:       Blocksify – Gutenberg Blocks & Page Builder for the Block Editor
 * Plugin URI:        https://pressmaximum.com/blocksify
 * Description:       Performance-first Gutenberg blocks kit. Per-block asset loading, per-instance inline CSS, PageSpeed 90+ default.
 * Version:           0.3.0
 * Requires at least: 6.5
 * Requires PHP:      7.4
 * Author:            PressMaximum
 * Author URI:        https://pressmaximum.com
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       blocksify
 *
 * @package Blocksify
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

define( 'BLOCKSIFY_VERSION', '0.3.0' );
define( 'BLOCKSIFY_FILE', __FILE__ );
define( 'BLOCKSIFY_PATH', plugin_dir_path( __FILE__ ) );
define( 'BLOCKSIFY_URL', plugin_dir_url( __FILE__ ) );

/**
 * Design Library — Studio API base URL.
 *
 * Accepts two forms (see Blocksify_Design_Library::resolve_api_base()):
 *
 *   - Origin only:  `https://studio.pressmaximum.com/`
 *                   → plugin appends `/wp-json/blocksify-design-studio/v1` automatically.
 *   - Full REST base: `https://custom-proxy.example.com/api/v1`
 *                   → used as-is when the URL contains `/wp-json/` or points
 *                     at a non-WP proxy.
 *
 * Resolution order (first definition wins):
 *   1. wp-config.php — `define( 'BLOCKSIFY_DESIGN_LIBRARY_API_URL', ... )` for per-site override.
 *   2. The fallback below — production default (origin only).
 *
 * Runtime overrides also possible via the PHP filter
 * `blocksify_design_library_api_base` (theme / mu-plugin scope).
 *
 * NOTE: this constant lives PHP-side only and is never exposed to the editor
 * bundle. All catalog requests from the browser hit the plugin's proxy routes
 * at /wp-json/blocksify-design-library/v1/* (same-origin), and the proxy
 * controller forwards to this URL via wp_remote_get. See
 * docs/SPEC-design-library-rest-api.md §1.1 + §2.5.
 */
if ( ! defined( 'BLOCKSIFY_DESIGN_LIBRARY_API_URL' ) ) {
	define( 'BLOCKSIFY_DESIGN_LIBRARY_API_URL', 'https://design-library.pressmaximum.com/' );
}

if ( file_exists( BLOCKSIFY_PATH . 'vendor/autoload.php' ) ) {
	require_once BLOCKSIFY_PATH . 'vendor/autoload.php';
}

require_once BLOCKSIFY_PATH . 'includes/class-blocksify-attrs.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-icons.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-tokens.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-traits.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-background-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-color-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-spacing-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-border-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-size-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-overflow-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-position-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-zindex-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-visibility-trait.php';
require_once BLOCKSIFY_PATH . 'includes/traits/class-blocksify-typography-trait.php';
require_once BLOCKSIFY_PATH . 'includes/chrome/class-blocksify-styling-repeater.php';
require_once BLOCKSIFY_PATH . 'includes/controls/class-blocksify-background-render.php';
require_once BLOCKSIFY_PATH . 'includes/class-css-generator.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-css-pipeline.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-rollback.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-settings.php';
require_once BLOCKSIFY_PATH . 'includes/class-blocksify-shadows.php';
require_once BLOCKSIFY_PATH . 'includes/blocks/class-blocksify-cl-query.php';
require_once BLOCKSIFY_PATH . 'includes/blocks/class-blocksify-terms-query.php';
require_once BLOCKSIFY_PATH . 'includes/blocks/class-blocksify-content-loop-renderer.php';
require_once BLOCKSIFY_PATH . 'includes/blocks/class-blocksify-content-loop-rest.php';
require_once BLOCKSIFY_PATH . 'includes/blocksify-accordion-helpers.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/class-blocksify-dashboard-schema.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/class-blocksify-dashboard-changelog.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-settings-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-settings-schema-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-changelog-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-clear-cache-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-rollback-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/rest/class-onboarding-controller.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/class-blocksify-dashboard-rest.php';
require_once BLOCKSIFY_PATH . 'includes/dashboard/class-blocksify-dashboard.php';
require_once BLOCKSIFY_PATH . 'includes/design-library/class-blocksify-design-library.php';
Blocksify_Settings::boot();
Blocksify_Shadows::boot();
Blocksify_Traits::boot();
Blocksify_CSS_Pipeline::boot();
Blocksify_Rollback::boot();
register_deactivation_hook( __FILE__, array( 'Blocksify_CSS_Pipeline', 'on_deactivate' ) );
Blocksify_Content_Loop_REST::boot();
Blocksify_Dashboard::boot();
Blocksify_Dashboard_REST::boot();
Blocksify_Design_Library::boot();

/**
 * Register the "Blocksify" block category and pin it to the top of the
 * inserter list so plugin blocks are the first thing users see.
 */
function blocksify_register_block_category( $categories ) {
	$blocksify = array(
		'slug'  => 'blocksify',
		'title' => __( 'Blocksify', 'blocksify' ),
		'icon'  => null,
	);
	return array_merge( array( $blocksify ), $categories );
}
add_filter( 'block_categories_all', 'blocksify_register_block_category' );

/**
 * Register all Blocksify blocks from the build directory.
 */
function blocksify_register_blocks() {
	// Register order matters: parent BEFORE child so the child's
	// `parent: ['blocksify/accordion']` (or `['blocksify/tabs']`)
	// constraint resolves at register time without a deferred lookup.
	$blocks = array(
		'section',
		'container',
		'button',
		'text',
		'icon',
		'image',
		'tabs',
		'tab',
		'content-loop',
		'accordion',
		'accordion-item',
		'icon-list',
		'icon-list-item',
	);

	foreach ( $blocks as $block ) {
		$block_dir = BLOCKSIFY_PATH . 'build/blocks/' . $block;
		if ( file_exists( $block_dir . '/block.json' ) ) {
			register_block_type( $block_dir );
		}
	}
}
add_action( 'init', 'blocksify_register_blocks' );

/**
 * Skip blocksify/tab's individual server-render — its parent
 * blocksify/tabs walks `parsed_block['innerBlocks']` directly and
 * builds each tabpanel's DOM in one pass. Without
 * this filter, every Tab would also render its own
 * `<InnerBlocks.Content/>` save markup → children appear twice on the
 * frontend (once from Tab's static save, once from Tabs's manual
 * walk). Returning a non-null value from `pre_render_block` short-
 * circuits the render entirely AND prevents Tab's children from being
 * rendered into `$content` (saves CPU).
 *
 * @param string|null $pre  Pre-rendered content, or null to render normally.
 * @param array       $block Parsed block tree node.
 * @return string|null Empty string for blocksify/tab (skip), or `$pre` unchanged.
 */
function blocksify_skip_tab_render( $pre, $block ) {
	if ( isset( $block['blockName'] ) && 'blocksify/tab' === $block['blockName'] ) {
		return '';
	}
	return $pre;
}
add_filter( 'pre_render_block', 'blocksify_skip_tab_render', 10, 2 );

/**
 * Enqueue the shared Blocksify editor stylesheet.
 *
 * Carries rules that apply to every Blocksify block (hover outline, label
 * badge, icon color). Each block's own editor.css can still override via a
 * plain .wp-block-blocksify-{name} selector — see src/shared/editor-common.css
 * for the :where() trick that keeps those overrides specificity-safe.
 *
 * Hook choice matters: WP 6.5+ renders the block editor inside an iframe, and
 * `enqueue_block_editor_assets` loads into the outer admin page — styles
 * registered there never reach the block wrappers inside the iframe.
 * `enqueue_block_assets` is the hook that does fire inside the iframe (it
 * also fires on the frontend, hence the `is_admin()` guard so our editor-
 * only outline + badge don't leak onto published pages).
 *
 * Source lives in src/shared/editor-common.css; scripts/rename-css.js copies
 * it into build/ on every `npm run build` so the release pipeline only has
 * to ship one directory.
 */
function blocksify_enqueue_editor_common() {
	if ( ! is_admin() ) {
		return;
	}
	$path = BLOCKSIFY_PATH . 'build/editor-common.css';
	// filemtime for the version — see rationale in blocksify_enqueue_editor_script.
	$version = file_exists( $path ) ? (string) filemtime( $path ) : BLOCKSIFY_VERSION;
	wp_enqueue_style(
		'blocksify-editor-common',
		BLOCKSIFY_URL . 'build/editor-common.css',
		array(),
		$version
	);
}
add_action( 'enqueue_block_assets', 'blocksify_enqueue_editor_common' );

/**
 * Enqueue the main editor script — currently just the Blocksify Settings
 * plugin sidebar. Lives at the outer post-editor shell (not inside the
 * block iframe), so the hook is `enqueue_block_editor_assets`, not the
 * iframe-aware `enqueue_block_assets`.
 */
function blocksify_enqueue_editor_script() {
	$asset_path = BLOCKSIFY_PATH . 'build/index.asset.php';
	if ( ! file_exists( $asset_path ) ) {
		return;
	}
	$asset = require $asset_path;
	wp_enqueue_script(
		'blocksify-editor',
		BLOCKSIFY_URL . 'build/index.js',
		$asset['dependencies'],
		$asset['version'],
		true
	);
	wp_set_script_translations( 'blocksify-editor', 'blocksify' );

	$style_path = BLOCKSIFY_PATH . 'build/index.css';
	if ( file_exists( $style_path ) ) {
		// Use filemtime as the version so every rebuild gets a fresh URL —
		// BLOCKSIFY_VERSION is a release constant that doesn't change between
		// `npm run live` iterations, and JS gets a real content hash via
		// index.asset.php; CSS doesn't, so the hardcoded version was causing
		// the browser to serve stale styles even on hard refresh.
		wp_enqueue_style(
			'blocksify-editor',
			BLOCKSIFY_URL . 'build/index.css',
			array(),
			(string) filemtime( $style_path )
		);
	}
}
add_action( 'enqueue_block_editor_assets', 'blocksify_enqueue_editor_script' );

/**
 * Register the Section block's frontend Motion.dev scroll-effect runtime.
 *
 * Registered (not enqueued) at `init` so individual Section instances can
 * lazy-enqueue from their `render.php` only when they actually have
 * `backgroundScrollEffect.enabled === true`. Sections without scroll
 * animation pay zero JS cost — keeps no-JS performance default intact.
 *
 * Multiple Section instances on the same page enqueue the same handle —
 * `wp_enqueue_script` is no-op on subsequent calls so the script ships
 * exactly once per page.
 */
function blocksify_register_section_frontend() {
	$asset_path = BLOCKSIFY_PATH . 'build/blocks/section/frontend.asset.php';
	if ( ! file_exists( $asset_path ) ) {
		return;
	}
	$asset = require $asset_path;
	wp_register_script(
		'blocksify-section-scroll-effect',
		BLOCKSIFY_URL . 'build/blocks/section/frontend.js',
		$asset['dependencies'],
		$asset['version'],
		true
	);
}
add_action( 'init', 'blocksify_register_section_frontend' );

/**
 * Register the Content Loop frontend dispatcher script (Phase 4.4 —
 * SPEC §5.6). Lazy-enqueued by `src/blocks/content-loop/render.php` only
 * when the block needs runtime JS (non-grid `layoutType` by default; Pro
 * flips `blocksify.cl.view.needs_js` true for grid-layout features that
 * need JS — conditional visibility per card, scroll-triggered animations).
 *
 * Multiple Content Loop instances on the same page enqueue the same
 * handle — `wp_enqueue_script` is a no-op on subsequent calls so the
 * script ships exactly once per page.
 */
function blocksify_register_content_loop_view() {
	$asset_path = BLOCKSIFY_PATH . 'build/blocks/content-loop/view.asset.php';
	if ( ! file_exists( $asset_path ) ) {
		return;
	}
	$asset = require $asset_path;
	wp_register_script(
		'blocksify-content-loop-view',
		BLOCKSIFY_URL . 'build/blocks/content-loop/view.js',
		$asset['dependencies'],
		$asset['version'],
		true
	);
}
add_action( 'init', 'blocksify_register_content_loop_view' );

/**
 * Disable WordPress's native Inspector tabs for Blocksify blocks.
 *
 * WP 6.3+ auto-renders a Settings / Styles / Advanced tab strip whenever a
 * block has supports that populate multiple inspector groups (e.g. `color`
 * goes to the Styles group, `anchor` to Advanced). Blocksify blocks ship
 * their own <InspectorTabs> with General / Style / Advanced labels and
 * icons — leaving core tabs on renders two tab strips stacked in the
 * sidebar. Per-block `blockInspectorTabs: false` tells core to skip its
 * tab rendering for the named blocks without touching anyone else's UI.
 */
function blocksify_disable_native_inspector_tabs( $settings ) {
	$existing = isset( $settings['blockInspectorTabs'] ) && is_array( $settings['blockInspectorTabs'] )
		? $settings['blockInspectorTabs']
		: array( 'default' => true );
	$existing['blocksify/section']      = false;
	$existing['blocksify/container']    = false;
	$existing['blocksify/button']       = false;
	$existing['blocksify/tabs']         = false;
	$existing['blocksify/tab']          = false;
	$existing['blocksify/content-loop'] = false;
	$existing['blocksify/icon-list']      = false;
	$existing['blocksify/icon-list-item'] = false;
	$settings['blockInspectorTabs']  = $existing;
	return $settings;
}
add_filter( 'block_editor_settings_all', 'blocksify_disable_native_inspector_tabs' );

/**
 * Prevent WordPress from inlining Blocksify stylesheets.
 *
 * wp_maybe_inline_styles() inlines small <link> stylesheets when the registered
 * handle has a filesystem `path`. Clearing that path keeps the stylesheet as an
 * external <link>, which browsers cache cross-page and download in parallel.
 *
 * Both Section and Container ship per-instance CSS now (Phase 4 — Section
 * via traits + Container via FlexControl Layout panel). Both stylesheet
 * handles get the `path: false` treatment so inline-styles attached via
 * `wp_add_inline_style` survive without WP folding the parent stylesheet
 * into the page.
 */
function blocksify_disable_inline_styles() {
	$handles = array(
		'blocksify-section-style',
		'blocksify-container-style',
		'blocksify-button-style',
		'blocksify-text-style',
		'blocksify-icon-style',
		'blocksify-image-style',
		'blocksify-tabs-style',
		'blocksify-tab-style',
		'blocksify-content-loop-style',
		'blocksify-icon-list-style',
	);
	foreach ( $handles as $handle ) {
		if ( wp_style_is( $handle, 'registered' ) ) {
			wp_style_add_data( $handle, 'path', false );
		}
	}
}
add_action( 'wp_enqueue_scripts', 'blocksify_disable_inline_styles', 20 );

/**
 * Blocksify Playground — BLOCKSIFY_DEV-gated PluginSidebar.
 *
 * A development-only surface that renders one card per primitive / chrome
 * wrapper / composite control inside the post editor's 3-dot menu → sidebar
 * slot, alongside the Settings sidebar. The sidebar host (280px width,
 * PanelBody chrome, block editor context) matches the runtime environment
 * of a real Inspector — primitives tested here fail the same way they'd
 * fail in production, instead of passing in a wide admin page and breaking
 * when a user finally drops them into an Inspector.
 *
 * Registration happens on the JS side via registerPlugin(); PHP only
 * enqueues the bundle when BLOCKSIFY_DEV is true. When the constant is
 * unset or false, the bundle never loads, registerPlugin never runs, and
 * the 3-dot menu entry never appears — zero UI footprint on production
 * sites.
 *
 * Local development sites enable it in wp-config.php with
 * `define( 'BLOCKSIFY_DEV', true );`. Production builds never define it.
 */
function blocksify_enqueue_playground() {
	if ( ! defined( 'BLOCKSIFY_DEV' ) || ! BLOCKSIFY_DEV ) {
		return;
	}
	$asset_path = BLOCKSIFY_PATH . 'build/playground.asset.php';
	if ( ! file_exists( $asset_path ) ) {
		return;
	}
	$asset = require $asset_path;
	wp_enqueue_script(
		'blocksify-playground',
		BLOCKSIFY_URL . 'build/playground.js',
		$asset['dependencies'],
		$asset['version'],
		true
	);
	wp_set_script_translations( 'blocksify-playground', 'blocksify' );

	$style_path = BLOCKSIFY_PATH . 'build/playground.css';
	if ( file_exists( $style_path ) ) {
		wp_enqueue_style(
			'blocksify-playground',
			BLOCKSIFY_URL . 'build/playground.css',
			array( 'wp-components' ),
			(string) filemtime( $style_path )
		);
	}
}
add_action( 'enqueue_block_editor_assets', 'blocksify_enqueue_playground' );
