<?php

/**
 * Base class that gets extended by all module classes.
 *
 * @since 1.0
 */
class FLBuilderModule {

	/**
	 * A unique ID for the module.
	 *
	 * @since 1.0
	 * @var string $node
	 */
	public $node;

	/**
	 * A unique ID for the module's parent.
	 *
	 * @since 1.0
	 * @var int $parent
	 */
	public $parent;

	/**
	 * The sort order for this module.
	 *
	 * @since 1.0
	 * @var int $position
	 */
	public $position;

	/**
	 * A display name for the module.
	 *
	 * @since 1.0
	 * @var string $name
	 */
	public $name;

	/**
	 * A description to display for the module.
	 *
	 * @since 1.0
	 * @var string $description
	 */
	public $description;

	/**
	 * The category this module belongs to.
	 *
	 * @since 1.0
	 * @var string $category
	 */
	public $category;

	/**
	 * Must be the module's folder name.
	 *
	 * @since 1.0
	 * @var string $slug
	 */
	public $slug;

	/**
	 * The module's directory path.
	 *
	 * @since 1.0
	 * @var string $dir
	 */
	public $dir;

	/**
	 * The module's directory url.
	 *
	 * @since 1.0
	 * @var string $url
	 */
	public $url;

	/**
	 * An array of form settings.
	 *
	 * @since 1.0
	 * @var array $form
	 */
	public $form = array();

	/**
	 * Whether this module is enabled on the
	 * frontend or not.
	 *
	 * @since 1.0
	 * @var boolean $enabled
	 */
	public $enabled = true;

	/**
	 * Whether this module's content should
	 * be exported to the WP editor or not.
	 *
	 * @since 1.0
	 * @var boolean $editor_export
	 */
	public $editor_export = true;

	/**
	 * Whether partial refresh should be enabled
	 * for this module or not.
	 *
	 * @since 1.7
	 * @var boolean $partial_refresh
	 */
	public $partial_refresh = false;

	/**
	 * The module settings object.
	 *
	 * @since 1.0
	 * @var object $settings
	 */
	public $settings;

	/**
	 * Additional CSS to enqueue.
	 *
	 * @since 1.0
	 * @var array $css
	 */
	public $css = array();

	/**
	 * Additional JS to enqueue.
	 *
	 * @since 1.0
	 * @var array $js
	 */
	public $js = array();

	/**
	 * Whether to include the module wrapper divs or not. If excluded,
	 * output MUST include a single root node with the required attributes.
	 *
	 * @var boolean $include_wrapper
	 */
	public $include_wrapper = true;

	/**
	 * Whether this module supports the Container Element setting
	 * in the Advanced tab or not. If set to false, that setting
	 * will not be displayed in the UI.
	 *
	 * @var boolean $element_setting
	 */
	public $element_setting = true;

	/**
	 * An array of module keys this module accepts as children.
	 * Set to 'all' for all children. Leave empty for none.
	 *
	 * @var array|string $accepts
	 */
	public $accepts = [];

	/**
	 * An array of container module keys this module accepts as parents.
	 * Set to 'all' for all parents.
	 *
	 * @var array|string $parents
	 */
	public $parents = 'all';

	/**
	 * An array of module keys and settings to use as a template
	 * for modules that accept children.
	 *
	 * @var array $template
	 */
	public $template = [];

	/**
	 * The version for this module instance. Used internally to
	 * deprecate config, files, and defaults.
	 *
	 * @since 2.9
	 * @var int $version
	 */
	public $version = null;

	/**
	 * The class of the font icon for this module.
	 *
	 * @since 2.0
	 */
	public $icon = '';

	public $group = '';

	public $type = '';

	public $kind = '';

	public $isWidget = ''; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase

	public $isAlias = ''; //phpcs:ignore WordPress.NamingConventions.ValidVariableName.PropertyNotSnakeCase

	public $template_root_node;

	public $template_node_id;

	public $template_id;

	/**
	 * Whether this module is acting as a block or not.
	 *
	 * @var boolean $is_block
	 */
	public $is_block = false;

	/**
	 * Whether to include this module as a block in the
	 * WordPress block editor.
	 *
	 * @var boolean $block_editor
	 */
	public $block_editor = false;

	/**
	 * Inner blocks to render for this module if it accepts children
	 * and is acting as a block in the block editor.
	 *
	 * @var string $block_editor_children
	 */
	public $block_editor_children = null;

	/**
	 * Does the module support the auto_style tab?
	 *
	 * @var Boolean $auto_style
	 */
	public $auto_style = false;

	/**
	 * Module constructor.
	 *
	 * @since 1.0
	 */
	public function __construct( $params ) {
		$class_info            = new ReflectionClass( $this );
		$class_path            = $class_info->getFileName();
		$dir_path              = dirname( $class_path );
		$this->slug            = isset( $params['slug'] ) ? $params['slug'] : basename( $class_path, '.php' );
		$this->enabled         = isset( $params['enabled'] ) ? $params['enabled'] : true;
		$this->editor_export   = isset( $params['editor_export'] ) ? $params['editor_export'] : true;
		$this->partial_refresh = isset( $params['partial_refresh'] ) ? $params['partial_refresh'] : false;
		$this->include_wrapper = isset( $params['include_wrapper'] ) ? $params['include_wrapper'] : true;
		$this->element_setting = isset( $params['element_setting'] ) ? $params['element_setting'] : true;
		$this->accepts         = isset( $params['accepts'] ) ? $params['accepts'] : [];
		$this->parents         = isset( $params['parents'] ) ? $params['parents'] : 'all';
		$this->template        = isset( $params['template'] ) ? $params['template'] : [];
		$this->block_editor    = isset( $params['block_editor'] ) ? $params['block_editor'] : false;
		$this->auto_style      = isset( $params['auto_style'] ) ? $params['auto_style'] : false;

		// We need to normalize the paths here since path comparisons
		// break on Windows because they use backslashes.
		$abspath                  = str_replace( '\\', '/', ABSPATH );
		$fl_builder_dir           = str_replace( '\\', '/', FL_BUILDER_DIR );
		$dir_path                 = str_replace( '\\', '/', $dir_path );
		$stylesheet_directory     = str_replace( '\\', '/', get_stylesheet_directory() );
		$stylesheet_directory_uri = str_replace( '\\', '/', get_stylesheet_directory_uri() );
		$template_directory       = str_replace( '\\', '/', get_template_directory() );
		$template_directory_uri   = str_replace( '\\', '/', get_template_directory_uri() );

		// Find the right paths.
		if ( is_child_theme() && stristr( $dir_path, $stylesheet_directory ) ) {
			$this->url = trailingslashit( str_replace( $stylesheet_directory, $stylesheet_directory_uri, $dir_path ) );
			$this->dir = trailingslashit( $dir_path );
		} elseif ( stristr( $dir_path, $template_directory ) ) {
			$this->url = trailingslashit( str_replace( $template_directory, $template_directory_uri, $dir_path ) );
			$this->dir = trailingslashit( $dir_path );
		} elseif ( isset( $params['url'] ) && isset( $params['dir'] ) ) {
			$this->url = trailingslashit( $params['url'] );
			$this->dir = trailingslashit( $params['dir'] );
		} elseif ( ! stristr( $dir_path, $fl_builder_dir ) ) {
			$this->url = trailingslashit( str_replace( trailingslashit( $abspath ), trailingslashit( home_url() ), $dir_path ) );
			$this->dir = trailingslashit( $dir_path );
		} else {
			$this->url = trailingslashit( FLBuilder::plugin_url() . 'modules/' . $this->slug );
			$this->dir = trailingslashit( FL_BUILDER_DIR . 'modules/' . $this->slug );
		}
		// Icon requires dir be defined before calling get_icon()
		$this->icon = isset( $params['icon'] ) ? $this->get_icon( $params['icon'] ) : $this->get_icon();

		$details = apply_filters( 'fl_builder_module_details', array(
			'name'        => $params['name'],
			'description' => $params['description'],
			'category'    => $this->normalize_category_name( $params['category'] ),
			'group'       => isset( $params['group'] ) ? $params['group'] : false,
			'icon'        => $this->icon,
		), $this->slug );

		$this->name        = $details['name'];
		$this->description = $details['description'];
		$this->category    = $details['category'];
		$this->group       = $details['group'];
		$this->icon        = $details['icon'];
	}

	/**
	 * Returns the path for a module file taking deprecations
	 * into account. Since 2.9, this is the only way you should
	 * be accessing module paths.
	 *
	 * @since 2.9
	 * @param string $base
	 * @return string
	 */
	public function path( $base ) {
		return FLBuilderModuleDeprecations::get_module_path( $this, $base );
	}

	/**
	 * Returns the url for a module file taking deprecations
	 * into account. Since 2.9, this is the only way you should
	 * be accessing module urls.
	 *
	 * @since 2.9
	 * @param string $base
	 * @return string
	 */
	public function url( $base ) {
		$path = $this->path( $base );

		return str_replace( $this->dir, $this->url, $path );
	}

	/**
	 * Returns a single config value for a module taking deprecations
	 * into account. Since 2.9, this is the only way you should
	 * be accessing module config that can be deprecated.
	 *
	 * @since 2.9
	 * @param string $key
	 * @return mixed
	 */
	public function config( $key ) {
		return FLBuilderModuleDeprecations::get_module_config( $this, $key );
	}

	/**
	 * Used to enqueue additional frontend styles. Do not enqueue
	 * frontend.css or frontend.responsive.css as those will be
	 * enqueued automatically. Params are the same as those used in
	 * WordPress' wp_enqueue_style function.
	 *
	 * @since 1.0
	 * @param string $handle
	 * @param string $src
	 * @param array $deps
	 * @param string $ver
	 * @param string $media
	 * @return void
	 */
	public function add_css( $handle, $src = null, $deps = null, $ver = null, $media = null ) {
		$this->css[ $handle ] = array( $src, $deps, $ver, $media );
	}

	/**
	 * Used to enqueue additional frontend scripts. Do not enqueue
	 * frontend.js as that will be enqueued automatically. Params
	 * are the same as those used in WordPress' wp_enqueue_script function.
	 *
	 * @since 1.0
	 * @param string $handle
	 * @param string $src
	 * @param array $deps
	 * @param string $ver
	 * @param bool $in_footer
	 * @return void
	 */
	public function add_js( $handle, $src = null, $deps = null, $ver = null, $in_footer = null ) {
		$this->js[ $handle ] = array( $src, $deps, $ver, $in_footer );
	}

	/**
	 * Enqueues the needed styles for any icon fields
	 * in this module.
	 *
	 * @since 1.4.6
	 * @return void
	 */
	public function enqueue_icon_styles() {
		FLBuilderIcons::enqueue_styles_for_module( $this );
	}

	/**
	 * Enqueues the needed styles for any font fields
	 * in this module.
	 *
	 * @since 1.6.3
	 * @return void
	 */
	public function enqueue_font_styles() {
		FLBuilderFonts::add_fonts_for_module( $this );
	}

	/**
	 * Should be overridden by subclasses to enqueue
	 * additional css/js using the add_css and add_js methods.
	 *
	 * @since 1.0
	 * @return void
	 */
	public function enqueue_scripts() {
	}

	/**
	 * Should be overridden by subclasses to enqueue
	 * css/js for the parent document in the iframe UI.
	 *
	 * @since 2.9
	 * @return void
	 */
	public function enqueue_ui_scripts() {}

	/**
	 * Should be overridden by subclasses to
	 * work with settings data _before it is saved_.
	 *
	 * @since 1.0
	 * @param object $settings A settings object that is going to be saved.
	 * @return object
	 */
	public function update( $settings ) {
		return $settings;
	}

	/**
	 * Should be overridden by subclasses to work with raw settings data
	 * _before defaults are merged in_. This is mainly used to ensure
	 * backwards compatibility with old module settings.
	 *
	 * @since 2.6.0.1
	 * @param object $settings A raw settings object.
	 * @param object $defaults The default settings for this module.
	 * @return object
	 */
	public function filter_raw_settings( $settings, $defaults ) {
		return $settings;
	}

	/**
	 * Should be overridden by subclasses to
	 * work with settings data _before it is used to display a module_.
	 *
	 * @since 2.0.3
	 * @param object $settings A settings object.
	 * @param object $helper A settings compatibility helper.
	 * @return object
	 */
	public function filter_settings( $settings, $helper ) {
		return $settings;
	}

	/**
	 * Should be overridden by subclasses to work with a module before
	 * it is deleted. Please note, this method is called when a module
	 * is updated and when it's actually removed from the page and should
	 * be used for things like clearing photo cache from the builder's
	 * cache directory. If only need to run logic when a module is
	 * actually removed from the page, use the remove method instead.
	 *
	 * @since 1.0
	 * @return void
	 */
	public function delete() {
	}

	/**
	 * Should be overridden by subclasses to work with a module when
	 * it is actually removed from the page.
	 *
	 * @since 1.0
	 * @return void
	 */
	public function remove() {
	}

	/**
	 * Get svg icon string
	 *
	 * @since 2.0
	 * @return String
	 */
	public function get_icon( $icon = '' ) {

		// Inline svg icon
		if ( 0 === strpos( trim( $icon ), '<svg' ) ) {
			return $icon;
		}

		// Icon is referencing an included icon or dashicon
		if ( '' !== $icon ) {
			if ( file_exists( FL_BUILDER_DIR . 'img/svg/' . $icon ) ) {
				return file_get_contents( FL_BUILDER_DIR . 'img/svg/' . $icon );
			} else {
				return "<span class='dashicon dashicons dashicons-$icon'></span>";
			}
		}

		// Module directory includes an icon.svg file
		if ( file_exists( $this->dir . 'icon.svg' ) ) {
			return file_get_contents( $this->dir . 'icon.svg' );
		}

		// Default to insert icon
		return file_get_contents( FL_BUILDER_DIR . 'img/svg/insert.svg' );
	}

	/**
	 * Normalizes category names to support 2.0 since the default
	 * category names changed.
	 *
	 * @since 2.0
	 * @access private
	 * @param string $cat
	 * @return string
	 */
	private function normalize_category_name( $cat ) {
		if ( __( 'Basic Modules', 'fl-builder' ) === $cat ) {
			$cat = __( 'Basic', 'fl-builder' );
		} elseif ( __( 'Advanced Modules', 'fl-builder' ) === $cat ) {
			$cat = __( 'Advanced', 'fl-builder' );
		}
		return $cat;
	}

	/**
	 * Get the default svg icon
	 *
	 * @since 2.0
	 * @return String
	 */
	static public function get_default_icon() {
		$path = FL_BUILDER_DIR . 'img/svg/insert.svg';
		return file_get_contents( $path );
	}

	/**
	 * Get the widget icon
	 *
	 * @since 2.0
	 * @return String
	 */
	static public function get_widget_icon() {
		$path = FL_BUILDER_DIR . 'img/svg/wordpress-alt.svg';
		return file_get_contents( $path );
	}

	/**
	 * Filter root element classes before render.
	 *
	 * @param array $classes
	 * @return array
	 */
	public function filter_classes( $classes = [] ) {
		return $classes;
	}

	/**
	 * Filter root element attributes before render.
	 *
	 * @param array $attrs
	 * @return array
	 */
	public function filter_attributes( $attrs = [] ) {
		return $attrs;
	}

	/**
	 * Renders the root element attributes for this module.
	 *
	 * @param array $attrs
	 * @return void
	 */
	public function render_attributes( $attrs = [] ) {
		echo FLBuilder::render_module_attributes( $this, $attrs );
	}

	/**
	 * Check if this module accepts child nodes.
	 *
	 * @return bool
	 */
	public function accepts_children() {
		return ! empty( $this->accepts );
	}

	/**
	 * Checks if this module has child nodes.
	 *
	 * @return bool
	 */
	public function has_children() {
		$children = FLBuilderModel::get_nodes( null, $this );

		return count( $children ) ? true : false;
	}

	/**
	 * Renders the child nodes for this module.
	 *
	 * @return void
	 */
	public function render_children() {
		if ( null !== $this->block_editor_children ) {
			echo $this->block_editor_children;
		} else {

			$children = FLBuilderModel::get_nodes( null, $this );
			foreach ( $children as $child ) {
				if ( 'module' === $child->type ) {
					FLBuilder::render_module( $child );
				}
			}
		}
	}

	/**
	 * Renders the child nodes for this module with a wrapper. It must
	 * be done this way so we know what element to sort in. This also
	 * allows child nodes to be added when other static elements are
	 * present in the module.
	 *
	 *
	 * @param string $tag
	 * @param array $attrs
	 * @return void
	 */
	public function render_children_with_wrapper( $tag = 'div', $attrs = [] ) {
		if ( FLBuilderModel::is_builder_active() ) {
			$attrs['data-children-wrapper'] = 'true';
		}

		echo "<$tag ";
		FLBuilder::render_node_attributes( $attrs );
		echo '>';
		$this->render_children();
		echo "</$tag>";
	}

	/**
	 * Whether a module has a block.js template for instant
	 * rendering in the builder or block editor.
	 *
	 * @return bool
	 */
	public function is_js_block() {
		return fl_builder_filesystem()->file_exists( $this->dir . 'js/block.js' );
	}
}
