<?php
/**
 * Shared HTML/PDF template view.
 *
 * @package EntryFoundry
 */

namespace EntryFoundry\Core\Views\Render;

use EntryFoundry\Core\Services\Assets\AssetDataUri;

/**
 * Presents validated template data as the shared renderer markup.
 */
final class TemplateView {
	/**
	 * Local asset service.
	 *
	 * @var AssetDataUri
	 */
	private $assets;

	/**
	 * Initialize the template view.
	 *
	 * @param AssetDataUri $assets Asset service.
	 */
	public function __construct( AssetDataUri $assets ) {
		$this->assets = $assets;
	}

	/**
	 * Render the complete portable document.
	 *
	 * @param array<string, mixed> $template Validated template.
	 * @return string
	 */
	public function render( array $template ) {
		$page       = $template['page'];
		$width_mm   = $this->number( $page['width_mm'] );
		$height_mm  = $this->number( $page['height_mm'] );
		$elements   = $this->ordered_elements( $template['elements'] );
		$is_flow    = ! empty( $page['flow'] );
		$page_count = max( 1, (int) ( $template['page_count'] ?? 1 ) );

		$markup = '<div class="entryfoundry-render' . ( $is_flow ? ' entryfoundry-flow-document' : '' ) . '"';
		if ( $is_flow ) {
			$document_height = ( $page_count * (float) $page['height_mm'] ) + ( max( 0, $page_count - 1 ) * 10 );
			$markup         .= ' style="--entryfoundry-document-height:' . $this->number( $document_height ) . 'mm"';
		}
		$markup .= '>';
		if ( $is_flow ) {
			for ( $page_index = 0; $page_index < $page_count; ++$page_index ) {
				$screen_page_top = $page_index * ( (float) $page['height_mm'] + 10 );
				$markup         .= '<div class="entryfoundry-flow-page" data-page="' . ( $page_index + 1 ) . '" style="--entryfoundry-screen-page-top:' . $this->number( $screen_page_top ) . 'mm"></div>';
				foreach ( $elements as $element ) {
					if ( (int) ( $element['page_index'] ?? 0 ) === $page_index ) {
						$screen_top = $screen_page_top + (float) $element['y_mm'];
						$markup    .= $this->render_element( $element, $screen_top );
					}
				}
				if ( $page_index + 1 < $page_count ) {
					$markup .= '<pagebreak />';
				}
			}
		} else {
			$markup .= '<div class="entryfoundry-page" data-width-mm="' . $width_mm . '" data-height-mm="' . $height_mm . '">';
			foreach ( $elements as $element ) {
				$markup .= $this->render_element( $element );
			}
			$markup .= '</div>';
		}
		return $markup . '</div>';
	}

	/**
	 * Render elements as boxes for mPDF's fixed-positioning API.
	 *
	 * @param array<string, mixed> $template Validated template.
	 * @return array<int, array{html:string,x_mm:float,y_mm:float,w_mm:float,h_mm:float,page_index:int}>
	 */
	public function render_fixed_boxes( array $template ) {
		$boxes = array();
		foreach ( $this->ordered_elements( $template['elements'] ) as $element ) {
			$boxes[] = array(
				'html'       => $this->render_element( $element, null, true ),
				'x_mm'       => (float) $element['x_mm'],
				'y_mm'       => (float) $element['y_mm'],
				'w_mm'       => (float) $element['w_mm'],
				'h_mm'       => (float) $element['h_mm'],
				'page_index' => max( 0, (int) ( $element['page_index'] ?? 0 ) ),
			);
		}
		return $boxes;
	}

	/**
	 * Resolve the full-page background used by positioned PDF pages.
	 *
	 * @param array<string, mixed> $template Validated template.
	 * @return array{color:string,image:string,image_fit:string,image_width_px:int,image_height_px:int}
	 */
	public function fixed_background( array $template ) {
		$page             = $template['page'];
		$background       = isset( $page['background'] ) && is_array( $page['background'] ) ? $page['background'] : array();
		$image            = isset( $background['image_id'] ) ? $this->assets->from_attachment_id( (int) $background['image_id'] ) : '';
		$image_dimensions = '' !== $image ? getimagesize( $image ) : false;

		return array(
			'color'           => isset( $background['color'] ) ? $this->css_color( $background['color'], '#ffffff' ) : '#ffffff',
			'image'           => $image,
			'image_fit'       => isset( $background['image_fit'] ) && in_array( $background['image_fit'], array( 'cover', 'contain' ), true ) ? $background['image_fit'] : 'cover',
			'image_width_px'  => is_array( $image_dimensions ) ? (int) $image_dimensions[0] : 0,
			'image_height_px' => is_array( $image_dimensions ) ? (int) $image_dimensions[1] : 0,
		);
	}

	/**
	 * Build the shared layout stylesheet for browser and PDF renderers.
	 *
	 * Callers must enqueue this string with wp_add_inline_style() for browser output.
	 *
	 * @param array<string, mixed> $template Validated template.
	 * @param bool                 $include_screen Include browser-only presentation rules.
	 * @return string
	 */
	public function render_css( array $template, $include_screen = true ) {
		$page           = $template['page'];
		$width_mm       = $this->number( $page['width_mm'] );
		$height_mm      = $this->number( $page['height_mm'] );
		$background_css = $this->background_css( $page );
		$css            = '@page{size:' . $width_mm . 'mm ' . $height_mm . 'mm;margin:0;' . $background_css . '}'
			. '.entryfoundry-render{position:relative;margin:0;padding:0}'
			. '.entryfoundry-page{position:fixed;left:0;top:0;overflow:hidden;box-sizing:border-box;width:' . $width_mm . 'mm;height:' . $height_mm . 'mm;' . $background_css . 'z-index:0}'
			. '.entryfoundry-element{position:absolute;box-sizing:border-box;overflow:hidden}'
			. '.entryfoundry-flow-document>.entryfoundry-element{position:fixed}'
			. '.entryfoundry-flow-page{display:none}'
			. '.entryfoundry-element img{display:block;width:100%;height:100%;object-fit:contain}'
			. '.entryfoundry-shape{height:100%;width:100%}.entryfoundry-shape--circle{border-radius:50%}'
			. '.entryfoundry-table{border-collapse:collapse;height:100%;table-layout:fixed;width:100%}.entryfoundry-table th,.entryfoundry-table td{box-sizing:border-box;padding:1.8mm 2mm;vertical-align:top;word-break:break-word}.entryfoundry-table th{font-weight:bold}.entryfoundry-table--entries td:first-child{font-weight:bold;width:38%}';
		if ( $include_screen ) {
			$font_normal = $this->assets->from_relative_path( 'vendor/mpdf/mpdf/ttfonts/DejaVuSans.ttf' );
			$font_bold   = $this->assets->from_relative_path( 'vendor/mpdf/mpdf/ttfonts/DejaVuSans-Bold.ttf' );
			$font_serif  = $this->assets->from_relative_path( 'vendor/mpdf/mpdf/ttfonts/DejaVuSerif.ttf' );
			$css         = '@font-face{font-family:"DejaVu Sans";src:url("' . $font_normal . '") format("truetype");font-style:normal;font-weight:400}'
				. '@font-face{font-family:"DejaVu Sans";src:url("' . $font_bold . '") format("truetype");font-style:normal;font-weight:700}'
				. '@font-face{font-family:"DejaVu Serif";src:url("' . $font_serif . '") format("truetype");font-style:normal;font-weight:400}' . $css
				. '@media screen{.entryfoundry-render{width:' . $width_mm . 'mm;min-height:' . $height_mm . 'mm;margin:0 auto;padding:10mm}.entryfoundry-page{position:absolute;left:10mm;top:10mm;box-shadow:0 2mm 8mm rgba(0,0,0,.22)}.entryfoundry-page>.entryfoundry-element{transform:none}.entryfoundry-flow-document{min-height:var(--entryfoundry-document-height)}.entryfoundry-flow-page{display:block;position:absolute;left:0;top:var(--entryfoundry-screen-page-top);overflow:hidden;box-sizing:border-box;width:' . $width_mm . 'mm;height:' . $height_mm . 'mm;' . $background_css . 'z-index:0;box-shadow:0 2mm 8mm rgba(0,0,0,.22);transform:translate(10mm,10mm)}.entryfoundry-element{position:absolute;transform:translate(10mm,10mm)}.entryfoundry-flow-document>.entryfoundry-element{top:var(--entryfoundry-screen-top)!important}pagebreak{display:none}}';
		}
		return $css . '@media print{.entryfoundry-page{box-shadow:none}}';
	}

	/**
	 * Build the validated background declarations shared by HTML and PDF layers.
	 *
	 * @param array<string, mixed> $page Validated page configuration.
	 * @return string
	 */
	private function background_css( array $page ) {
		$background       = isset( $page['background']['color'] ) ? $this->css_color( $page['background']['color'], '#ffffff' ) : '#ffffff';
		$background_image = isset( $page['background']['image_id'] ) ? $this->assets->from_attachment_id( (int) $page['background']['image_id'] ) : '';
		$background_fit   = isset( $page['background']['image_fit'] ) && in_array( $page['background']['image_fit'], array( 'cover', 'contain' ), true ) ? $page['background']['image_fit'] : 'cover';
		$background_css   = 'background-color:' . $background . ';';
		if ( '' !== $background_image ) {
			$background_css .= 'background-image:url("' . $background_image . '");background-position:center;background-repeat:no-repeat;background-size:' . $background_fit . ';';
		}
		return $background_css;
	}

	/**
	 * Render one positioned element.
	 *
	 * @param array<string, mixed> $element       Element data.
	 * @param float|null           $screen_top_mm Browser-only vertical offset for a flow page.
	 * @param bool                 $external_box  Whether mPDF owns the outer position and dimensions.
	 * @return string
	 */
	private function render_element( array $element, $screen_top_mm = null, $external_box = false ) {
		$style  = $external_box
			? 'position:relative;left:0;top:0;width:' . $this->number( $element['w_mm'] ) . 'mm;height:' . $this->number( $element['h_mm'] ) . 'mm;'
			: 'left:' . $this->number( $element['x_mm'] ) . 'mm;top:' . $this->number( $element['y_mm'] ) . 'mm;'
				. 'width:' . $this->number( $element['w_mm'] ) . 'mm;height:' . $this->number( $element['h_mm'] ) . 'mm;';
		$style .= 'z-index:' . (int) ( $element['z'] ?? 0 ) . ';'
			. $this->typography( isset( $element['style'] ) && is_array( $element['style'] ) ? $element['style'] : array() );
		if ( null !== $screen_top_mm ) {
			$style .= '--entryfoundry-screen-top:' . $this->number( $screen_top_mm ) . 'mm;';
		}
		$classes = 'entryfoundry-element entryfoundry-element--' . preg_replace( '/[^a-z0-9_-]/', '', strtolower( $element['type'] ) );

		if ( 'image' === $element['type'] ) {
			$image_source = ! empty( $element['image_id'] ) ? $this->assets->from_attachment_id( (int) $element['image_id'] ) : '';
			if ( '' === $image_source && ! empty( $element['src'] ) ) {
				$image_source = $this->assets->from_relative_path( (string) $element['src'] );
			}
			$image_fit = isset( $element['image_fit'] ) && in_array( $element['image_fit'], array( 'cover', 'contain' ), true ) ? $element['image_fit'] : 'contain';
			$content   = '<img src="' . $this->escape( $image_source ) . '" alt="" style="object-fit:' . $image_fit . '">';
		} elseif ( 'shape' === $element['type'] ) {
			$style   .= $this->shape_style( $element );
			$content  = '';
			$classes .= isset( $element['shape'] ) && 'circle' === $element['shape'] ? ' entryfoundry-shape entryfoundry-shape--circle' : ' entryfoundry-shape entryfoundry-shape--rectangle';
		} elseif ( 'table' === $element['type'] ) {
			$content = $this->render_table( $element );
		} else {
			$text_style = $this->text_content_style( isset( $element['style'] ) && is_array( $element['style'] ) ? $element['style'] : array() );
			$content    = '<div class="entryfoundry-text-content" style="' . $text_style . '">' . nl2br( $this->escape( isset( $element['content'] ) ? (string) $element['content'] : '' ) ) . '</div>';
		}
		return '<div id="' . $this->escape( $element['id'] ) . '" class="' . $classes . '" style="' . $style . '">' . $content . '</div>';
	}

	/**
	 * Sort elements into stable visual stacking order.
	 *
	 * @param array<int, array<string, mixed>> $elements Template elements.
	 * @return array<int, array<string, mixed>>
	 */
	private function ordered_elements( array $elements ) {
		usort(
			$elements,
			static function ( $left, $right ) {
				return (int) ( $left['z'] ?? 0 ) <=> (int) ( $right['z'] ?? 0 );
			}
		);
		return $elements;
	}

	/**
	 * Render a vector-like shape with CSS.
	 *
	 * @param array<string, mixed> $element Shape element.
	 * @return string
	 */
	private function shape_style( array $element ) {
		$shape        = isset( $element['shape'] ) && 'circle' === $element['shape'] ? 'circle' : 'rectangle';
		$fill         = $this->css_color( $element['fill'] ?? 'transparent', 'transparent' );
		$stroke       = isset( $element['stroke'] ) && is_array( $element['stroke'] ) ? $element['stroke'] : array();
		$stroke_color = $this->css_color( $stroke['color'] ?? ( is_string( $element['stroke'] ?? null ) ? $element['stroke'] : 'transparent' ), 'transparent' );
		$stroke_width = isset( $stroke['width_mm'] ) && is_numeric( $stroke['width_mm'] ) ? max( 0, (float) $stroke['width_mm'] ) : 0;
		$style        = 'background:' . $fill . ';border:' . $this->number( $stroke_width ) . 'mm solid ' . $stroke_color . ';';
		return 'circle' === $shape ? $style . 'border-radius:50%;' : $style;
	}

	/**
	 * Render a table element.
	 *
	 * @param array<string, mixed> $element Element data.
	 * @return string
	 */
	private function render_table( array $element ) {
		$mode        = isset( $element['table_mode'] ) && 'entries' === $element['table_mode'] ? 'entries' : 'manual';
		$rows        = isset( $element['rows'] ) && is_array( $element['rows'] ) ? $element['rows'] : array();
		$columns     = isset( $element['columns'] ) && is_array( $element['columns'] ) ? $element['columns'] : ( 'entries' === $mode ? array( array( 'label' => 'Field' ), array( 'label' => 'Value' ) ) : array() );
		$table_style = isset( $element['table_style'] ) && is_array( $element['table_style'] ) ? $element['table_style'] : array();
		$class       = 'entryfoundry-table entryfoundry-table--' . $mode;
		if ( ! empty( $table_style['striped'] ) ) {
			$class .= ' entryfoundry-table--striped';
		}
		$border      = $this->css_color( $table_style['border_color'] ?? '#d8e0ea', '#d8e0ea' );
		$border_size = isset( $table_style['border_width_px'] ) && is_numeric( $table_style['border_width_px'] ) ? max( 0, (float) $table_style['border_width_px'] ) : 1;
		$cell_bg     = $this->css_color( $table_style['cell_bg'] ?? '#ffffff', '#ffffff' );
		$cell_style  = 'border:' . $this->number( $border_size ) . 'px solid ' . $border . ';background:' . $cell_bg . ';height:' . $this->number( $table_style['row_height_mm'] ?? 10 ) . 'mm;';
		$html        = '<table class="' . $class . '">';
		if ( $columns ) {
			$html .= '<colgroup>';
			foreach ( $columns as $column ) {
				$width = is_array( $column ) && isset( $column['width_pct'] ) && is_numeric( $column['width_pct'] ) ? max( 0, min( 100, (float) $column['width_pct'] ) ) : 0;
				$html .= $width > 0 ? '<col style="width:' . $this->number( $width ) . '%">' : '<col>';
			}
			$html .= '</colgroup>';
		}
		if ( false !== ( $table_style['show_header'] ?? true ) ) {
			$header_bg    = $this->css_color( $table_style['header_bg'] ?? '#f4f7fb', '#f4f7fb' );
			$header_color = $this->css_color( $table_style['header_color'] ?? '#18324a', '#18324a' );
			$header_style = 'border:' . $this->number( $border_size ) . 'px solid ' . $border . ';background:' . $header_bg . ';color:' . $header_color . ';height:' . $this->number( $table_style['header_height_mm'] ?? 9 ) . 'mm;';
			$html        .= '<thead><tr>';
			foreach ( $columns as $index => $column ) {
				$label = is_array( $column ) && isset( $column['label'] ) ? (string) $column['label'] : 'Column ' . ( $index + 1 );
				$html .= '<th style="' . $header_style . '">' . nl2br( $this->escape( $label ) ) . '</th>';
			}
			$html .= '</tr></thead>';
		}
		$html .= '<tbody>';
		foreach ( $rows as $row ) {
			if ( ! is_array( $row ) || ! empty( $row['is_hidden'] ) ) {
				continue;
			}
			$cells = isset( $row['cells'] ) && is_array( $row['cells'] ) ? $row['cells'] : array();
			if ( 'entries' === $mode ) {
				$cells = array(
					isset( $row['label'] ) ? (string) $row['label'] : '',
					isset( $row['value'] ) ? (string) $row['value'] : '',
				);
			}
			$row_style = ! empty( $row['is_calculation'] ) ? 'font-weight:bold;' : '';
			$html     .= '<tr' . ( $row_style ? ' style="' . $row_style . '"' : '' ) . '>';
			foreach ( $cells as $cell ) {
				$html .= '<td style="' . $cell_style . '">' . nl2br( $this->escape( $this->cell_value( $cell ) ) ) . '</td>';
			}
			$html .= '</tr>';
		}
		return $html . '</tbody></table>';
	}

	/**
	 * Resolve a table cell to printable text.
	 *
	 * @param mixed $cell Cell data.
	 * @return string
	 */
	private function cell_value( $cell ) {
		if ( is_array( $cell ) ) {
			if ( isset( $cell['type'] ) && 'field' === $cell['type'] ) {
				return isset( $cell['value'] ) ? (string) $cell['value'] : '';
			}
			return isset( $cell['text'] ) ? (string) $cell['text'] : '';
		}
		return (string) $cell;
	}

	/**
	 * Convert supported typography values to CSS.
	 *
	 * @param array<string, mixed> $style Style data.
	 * @return string
	 */
	private function typography( array $style ) {
		$css = '';
		if ( isset( $style['font_family'] ) ) {
			$css .= 'font-family:' . $this->font_family( $style['font_family'] ) . ';';
		}
		if ( isset( $style['font_size_pt'] ) && is_numeric( $style['font_size_pt'] ) ) {
			$css .= 'font-size:' . $this->number( $style['font_size_pt'] ) . 'pt;';
		}
		if ( isset( $style['color'] ) ) {
			$css .= 'color:' . $this->css_color( $style['color'], '#000000' ) . ';';
		}
		if ( isset( $style['align'] ) && in_array( $style['align'], array( 'left', 'center', 'right', 'justify' ), true ) ) {
			$css .= 'text-align:' . $style['align'] . ';';
		}
		$css .= ! empty( $style['bold'] ) ? 'font-weight:bold;' : '';
		$css .= ! empty( $style['italic'] ) ? 'font-style:italic;' : '';
		if ( isset( $style['line_height'] ) && is_numeric( $style['line_height'] ) ) {
			$css .= 'line-height:' . $this->number( $style['line_height'] ) . ';';
		}
		return $css;
	}

	/**
	 * Render text alignment on an inner block for mPDF.
	 *
	 * @param array<string, mixed> $style Style data.
	 * @return string
	 */
	private function text_content_style( array $style ) {
		$css = 'width:100%;';
		if ( isset( $style['align'] ) && in_array( $style['align'], array( 'left', 'center', 'right', 'justify' ), true ) ) {
			$css .= 'text-align:' . $style['align'] . ';';
		}
		return $css;
	}

	/**
	 * Escape HTML text and attribute content.
	 *
	 * @param string $value Value.
	 * @return string
	 */
	private function escape( $value ) {
		return htmlspecialchars( $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8' );
	}

	/**
	 * Normalize a CSS number.
	 *
	 * @param int|float|string $value Number.
	 * @return string
	 */
	private function number( $value ) {
		return rtrim( rtrim( number_format( (float) $value, 4, '.', '' ), '0' ), '.' );
	}

	/**
	 * Accept only hexadecimal CSS colors.
	 *
	 * @param string $value Color.
	 * @param string $fallback Fallback.
	 * @return string
	 */
	private function css_color( $value, $fallback ) {
		return preg_match( '/^#[0-9a-f]{3}([0-9a-f]{3})?$/i', (string) $value ) ? (string) $value : $fallback;
	}

	/**
	 * Sanitize a CSS font family list.
	 *
	 * @param string $value Font family.
	 * @return string
	 */
	private function font_family( $value ) {
		if ( 'df-serif' === $value ) {
			return '"DejaVu Serif",serif';
		}
		if ( 'df-sans' === $value ) {
			return '"DejaVu Sans",sans-serif';
		}
		$value = preg_replace( '/[^a-z0-9 ,_-]/i', '', (string) $value );
		return '' !== trim( $value ) ? $value : 'sans-serif';
	}
}
