<?php
/**
 * Abstract base class for WPBakery elements.
 *
 * Handles vc_map registration, the shortcode callback and a unique
 * per-instance element ID.
 *
 * @package CraftAddonsForWPBakery
 */

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

/**
 * Class CAFW_Element_Base
 */
abstract class CAFW_Element_Base {

	/**
	 * Instance counter used to build unique element IDs.
	 *
	 * @var int
	 */
	private static $counter = 0;

	/**
	 * The shortcode tag for this element (e.g. "cafw_icon_box").
	 *
	 * @return string
	 */
	abstract public function get_shortcode_tag();

	/**
	 * Human-readable element name (shown in the editor and admin panel).
	 *
	 * @return string
	 */
	abstract public function get_label();

	/**
	 * Short description of the element.
	 *
	 * @return string
	 */
	abstract public function get_description();

	/**
	 * The vc_map() params array for this element (without the shared params).
	 *
	 * @return array
	 */
	abstract public function get_params();

	/**
	 * Render the element's inner HTML.
	 *
	 * @param array  $atts    Sanitized shortcode attributes.
	 * @param string $content Shortcode inner content.
	 * @param string $el_id   Unique element ID for scoping.
	 * @return string Escaped HTML output.
	 */
	abstract public function render( $atts, $content, $el_id );

	/**
	 * Dashicon-style icon slug for the admin panel card. Override per element.
	 *
	 * @return string
	 */
	public function get_admin_icon() {
		return 'dashicons-screenoptions';
	}

	/**
	 * CSS class used for this element's icon in the WPBakery editor.
	 *
	 * Styled in admin/css/cafw-editor-icons.css. Override per element.
	 *
	 * @return string
	 */
	public function get_editor_icon() {
		return 'cafw-vc-icon cafw-vc-icon-default';
	}

	/**
	 * Whether this element belongs to the Pro tier. Override per element.
	 *
	 * Pro elements are labelled in the admin dashboard; there is no licence
	 * gate yet, so they still register and work.
	 *
	 * @return bool
	 */
	public function is_pro() {
		return false;
	}

	/**
	 * Default attribute values for this element. Override in child.
	 *
	 * @return array
	 */
	protected function get_defaults() {
		return array();
	}

	/**
	 * Register the element with WordPress and WPBakery.
	 *
	 * Called by the loader only for enabled elements, so instantiating an
	 * element for metadata (in the admin panel) has no side effects.
	 *
	 * The loader runs this from within the vc_before_init action, so we call
	 * map() directly here - deferring it to another vc_before_init callback
	 * would be too late (the action is already firing) and the element would
	 * never appear in the editor.
	 *
	 * @return void
	 */
	public function register() {
		add_shortcode( $this->get_shortcode_tag(), array( $this, 'shortcode' ) );
		$this->map();
	}

	/**
	 * Register the element with WPBakery via vc_map().
	 *
	 * @return void
	 */
	public function map() {
		if ( ! function_exists( 'vc_map' ) ) {
			return;
		}

		$settings = array(
			'name'        => $this->get_label(),
			'base'        => $this->get_shortcode_tag(),
			'category'    => esc_html__( 'Craft Addons', 'craft-addons-for-wpbakery' ),
			'description' => $this->get_description(),
			'icon'        => $this->get_editor_icon(),
			'params'      => array_merge( $this->get_params(), $this->get_shared_params() ),
		);

		if ( function_exists( 'vc_config' ) && method_exists( vc_config(), 'get_advanced_sections' ) ) {
			$settings['sections'] = vc_config()->get_advanced_sections();
		}

		vc_map( $settings );
	}

	/**
	 * Shared params appended to every element.
	 *
	 * The Element ID, Extra class name and Design Options ("CSS box") fields are
	 * built by WPBakery's own config helper, so they look and behave exactly like
	 * the native elements (the 50/50 ID + class row in the General tab, the full
	 * Design Options panel with margin, padding, border, background and
	 * box-shadow). WPBakery aggregates and prints the css_editor output
	 * automatically; we only add its class to the wrapper (see open_root()).
	 *
	 * @return array
	 */
	protected function get_shared_params() {
		if ( function_exists( 'vc_config' ) && method_exists( vc_config(), 'get_general_advanced_settings' ) ) {
			return array_merge(
				vc_config()->get_general_advanced_settings(),
				vc_config()->get_design_options_tab()
			);
		}

		return array(
			array(
				'type'       => 'el_id',
				'heading'    => esc_html__( 'Element ID', 'craft-addons-for-wpbakery' ),
				'param_name' => 'el_id',
			),
			array(
				'type'       => 'textfield',
				'heading'    => esc_html__( 'Extra class name', 'craft-addons-for-wpbakery' ),
				'param_name' => 'el_class',
			),
			array(
				'type'       => 'css_editor',
				'heading'    => esc_html__( 'CSS box', 'craft-addons-for-wpbakery' ),
				'param_name' => 'css',
				'group'      => esc_html__( 'Design options', 'craft-addons-for-wpbakery' ),
			),
		);
	}

	/**
	 * Shortcode callback. Normalizes atts and delegates to render().
	 *
	 * @param array  $atts    Raw shortcode attributes.
	 * @param string $content Inner content.
	 * @return string
	 */
	public function shortcode( $atts, $content = null ) {
		$defaults = array_merge(
			$this->get_defaults(),
			array(
				'el_id'    => '',
				'el_class' => '',
				'css'      => '',
			)
		);

		$atts = shortcode_atts( $defaults, is_array( $atts ) ? $atts : array(), $this->get_shortcode_tag() );

		$el_id = self::resolve_el_id( $atts );

		return $this->render( $atts, $content, $el_id );
	}

	/**
	 * Resolve the element's DOM id: honour a user-supplied Element ID, otherwise
	 * fall back to an auto-generated unique one.
	 *
	 * Static so the WPBakery container classes (which cannot extend this base)
	 * share the same id scheme.
	 *
	 * @param array $atts Shortcode atts (may contain "el_id").
	 * @return string
	 */
	public static function resolve_el_id( $atts ) {
		$custom = isset( $atts['el_id'] ) ? sanitize_html_class( $atts['el_id'] ) : '';
		if ( '' !== $custom ) {
			return $custom;
		}
		return 'cafw-el-' . ++self::$counter;
	}

	/**
	 * The WPBakery "Design Options" CSS class for an element instance, derived
	 * from the css_editor "css" param. Returns '' when no design options are set.
	 *
	 * @param array $atts Shortcode atts (may contain "css").
	 * @return string
	 */
	public static function design_options_class( $atts ) {
		if ( empty( $atts['css'] ) || ! function_exists( 'vc_shortcode_custom_css_class' ) ) {
			return '';
		}
		return vc_shortcode_custom_css_class( $atts['css'] );
	}

	/**
	 * Resolve the typography (tag + inline styles) for one text part, combining
	 * its font_container value with its native Google font picker. Mirrors
	 * WPBakery's Custom Heading logic and enqueues the chosen Google font.
	 *
	 * Call this from render() with the same prefix passed to typography_params().
	 *
	 * @param array  $atts   Shortcode atts.
	 * @param string $prefix Param name prefix (e.g. "title").
	 * @return array{tag:string,styles:array<string,string>}
	 */
	protected function part_typography( $atts, $prefix ) {
		if ( ! function_exists( 'vc_parse_multi_attribute' ) ) {
			return array(
				'tag'    => '',
				'styles' => array(),
			);
		}

		$styles = array();
		$tag    = '';

		$font_container = isset( $atts[ $prefix . '_font' ] ) ? (string) $atts[ $prefix . '_font' ] : '';
		if ( '' !== $font_container ) {
			foreach ( vc_parse_multi_attribute( $font_container ) as $key => $value ) {
				$value = trim( (string) $value );
				if ( '' === $value || preg_match( '/description/', $key ) ) {
					continue;
				}
				if ( 'tag' === $key ) {
					$tag = $value;
					continue;
				}
				if ( 'font_size' === $key ) {
					$value = function_exists( 'wpb_format_with_css_unit' ) ? wpb_format_with_css_unit( $value ) : $value;
				} elseif ( 'line_height' === $key ) {
					$value = preg_replace( '/\s+/', '', $value );
				}
				$styles[ str_replace( '_', '-', $key ) ] = $value;
			}
		}

		$google_fonts = isset( $atts[ $prefix . '_google_font' ] ) ? (string) $atts[ $prefix . '_google_font' ] : '';
		if ( '' !== $google_fonts ) {
			$gf = vc_parse_multi_attribute( $google_fonts );
			if ( ! empty( $gf['font_family'] ) ) {
				$family                = explode( ':', $gf['font_family'] );
				$styles['font-family'] = $family[0];
				if ( ! empty( $gf['font_style'] ) ) {
					$fs = explode( ':', $gf['font_style'] );
					if ( isset( $fs[1] ) && '' !== $fs[1] ) {
						$styles['font-weight'] = $fs[1];
					}
					if ( isset( $fs[2] ) && '' !== $fs[2] ) {
						$styles['font-style'] = $fs[2];
					}
				}
				$this->enqueue_google_font( $gf['font_family'] );
			}
		}

		return array(
			'tag'    => $tag,
			'styles' => $styles,
		);
	}

	/**
	 * Enqueue a Google font stylesheet for a parsed font_family value, the same
	 * way WPBakery's own elements do.
	 *
	 * @param string $font_family The font_family portion of a google_fonts value.
	 * @return void
	 */
	protected function enqueue_google_font( $font_family ) {
		$font_family = trim( (string) $font_family );
		if ( '' === $font_family ) {
			return;
		}
		wp_enqueue_style(
			'cafw-font-' . sanitize_title( $font_family ),
			esc_url_raw( 'https://fonts.googleapis.com/css?family=' . str_replace( ' ', '+', $font_family ) ),
			array(),
			CAFW_VERSION
		);
	}

	/**
	 * Build an escaped inline style attribute from a property => value map.
	 *
	 * @param array $styles CSS declarations keyed by property.
	 * @return string Style attribute (with leading space) or ''.
	 */
	protected function style_attr( array $styles ) {
		if ( empty( $styles ) ) {
			return '';
		}
		$decls = array();
		foreach ( $styles as $prop => $value ) {
			$decls[] = $prop . ':' . $value;
		}
		return ' style="' . esc_attr( implode( ';', $decls ) ) . '"';
	}

	/**
	 * Build the font controls for a piece of text.
	 *
	 * All parts share one "Styles" tab. Each part (Title, Text, Number, Quote ...)
	 * is its own labelled group/card inside that tab (via the "section" key), so
	 * the controls for each are clearly separated. The font_container heading is
	 * the card title.
	 *
	 * @param string $prefix Param name prefix (e.g. "title").
	 * @param string $label  Human label for the field (e.g. "Title").
	 * @param array  $args   Optional flags: 'tag' (show HTML tag selector),
	 *                       'color' (show colour control; default true),
	 *                       'sep' (draw a separator line above; default false).
	 * @return array
	 */
	protected function typography_params( $prefix, $label, $args = array() ) {
		$args = wp_parse_args(
			$args,
			array(
				'tag'   => false,
				'color' => true,
				'sep'   => false,
			)
		);

		$group   = esc_html__( 'Styles', 'craft-addons-for-wpbakery' );
		$section = 'cafw_style_' . $prefix;
		$fields  = array( 'font_size', 'line_height', 'text_align' );
		if ( $args['color'] ) {
			$fields[] = 'color';
		}
		if ( $args['tag'] ) {
			array_unshift( $fields, 'tag' );
		}

		$font = array(
			'type'       => 'font_container',
			'param_name' => $prefix . '_font',
			'heading'    => $label,
			'group'      => $group,
			'section'    => $section,
			'settings'   => array( 'fields' => $fields ),
		);
		if ( $args['sep'] ) {
			$font['edit_field_class'] = 'vc_col-xs-12 vc_column cafw-bg-sep';
		}

		return array(
			$font,
			array(
				'type'        => 'google_fonts',
				'param_name'  => $prefix . '_google_font',
				'group'       => $group,
				'section'     => $section,
				'settings'    => array(
					'fields' => array(
						'font_family_description' => '',
						'font_style_description'  => '',
					),
				),
			),
		);
	}

	/**
	 * Enqueue a block of generated, per-instance CSS via the inline-style API.
	 *
	 * Uses a registered placeholder stylesheet handle so WordPress prints the
	 * rules with wp_add_inline_style() instead of the element echoing a raw
	 * <style> tag. Callers sanitize the dynamic values they interpolate.
	 *
	 * @param string $css Generated CSS.
	 * @return void
	 */
	public static function add_inline_style( $css ) {
		$css = trim( (string) $css );
		if ( '' === $css ) {
			return;
		}
		if ( ! wp_style_is( 'cafw-inline', 'registered' ) ) {
			wp_register_style( 'cafw-inline', false, array(), CAFW_VERSION );
		}
		wp_enqueue_style( 'cafw-inline' );
		wp_add_inline_style( 'cafw-inline', $css );
	}

	/**
	 * Helper: build the root element open tag with id + base/extra classes.
	 *
	 * @param array  $atts       Shortcode atts (for el_class).
	 * @param string $el_id      Unique element ID.
	 * @param string $base_class Base class(es) for this element type.
	 * @param array  $styles     Optional inline styles (property => value),
	 *                           e.g. CSS custom properties driving the skin.
	 * @return string
	 */
	protected function open_root( $atts, $el_id, $base_class, array $styles = array() ) {
		$classes = array_filter(
			array(
				$base_class,
				self::design_options_class( $atts ),
				isset( $atts['el_class'] ) ? $atts['el_class'] : '',
			)
		);

		return sprintf(
			'<div id="%1$s" class="%2$s"%3$s>',
			esc_attr( $el_id ),
			esc_attr( trim( implode( ' ', $classes ) ) ),
			$this->style_attr( $styles )
		);
	}
}
