<?php

namespace GravityKit\GravityExport\PdfRenderer;

use GFExcel\Addon\GravityExportAddon;
use GravityKit\GravityExport\Addon\GravityExportAddon as GravityExportPro;
use GravityKit\GravityExport\GravityExport;
use GravityKit\GravityExport\Mpdf\Config\ConfigVariables;
use GravityKit\GravityExport\Mpdf\Config\FontVariables;
use GravityKit\GravityExport\Mpdf\Mpdf;
use GravityKit\GravityExport\PdfRenderer\Renderer\PdfRenderer;
use GravityKit\GravityExport\PdfRenderer\Rendering\Colors;
use GravityKit\GravityExport\PdfRenderer\Rendering\HeaderImage;
use GravityKit\GravityExport\PdfRenderer\Rendering\InlineImages;
use GravityKit\GravityExport\PdfRenderer\Writer\PDF;
use GFExcel\Renderer\PHPExcelRenderer;
use GFExcel\Renderer\RendererInterface;
use GFExcel\Vendor\PhpOffice\PhpSpreadsheet\IOFactory;
use GFExcel\Vendor\PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;
use GFExcel\Vendor\PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;

/**
 * The entry point of the plugin.
 *
 * Configures the necessary hooks.
 *
 * @since 1.0
 */
class Plugin {
	/**
	 * Mapping of available PDF variables.
	 * @since $ver$
	 * @var string[]
	 */
	private static $mpdf_variables = [
		'page-number'        => '{PAGENO}',
		'total-page-numbers' => '{nb}',
	];

	/**
	 * Name of the orientation setting field.
	 *
	 * @since 1.0
	 * @var string
	 */
	private const SETTING_ORIENTATION = 'gfexcel_pdf_orientation';

	/**
	 * Name of the paper size setting field.
	 *
	 * @since 1.0
	 * @var string
	 */
	private const SETTING_PAPER_SIZE = 'gfexcel_pdf_paper_size';

	/**
	 * Name of the settings that shows the page number.
	 *
	 * @since 1.0
	 * @var string
	 */
	private const SETTING_SHOW_PAGE_NUMBER = 'gfexcel_pdf_show_page_number';

	/**
	 * Name of the settings that contains the page number html.
	 *
	 * @since $ver$
	 * @var string
	 */
	private const SETTING_PAGE_NUMBER_HTML = 'gfexcel_pdf_page_number_html';

	/**
	 * Name of the settings that shows the page title.
	 *
	 * @since 1.0
	 * @var string
	 */
	private const SETTING_SHOW_PAGE_TITLE = 'gfexcel_pdf_show_page_title';

	/**
	 * Name of the settings that holds the font family.
	 *
	 * @since $ver$
	 * @var string
	 */
	private const SETTING_FONT_FAMILY = 'pdf_font_family';

	/**
	 * Name of the settings that holds the font family.
	 *
	 * @since $ver$
	 * @var string
	 */
	private const SETTING_PAGE_NUMBER_ALIGNMENT = 'pdf_page_no_align';

	/**
	 * Page title setting representing no title.
	 *
	 * @since 1.0
	 * @var int
	 */
	private const PAGE_TITLE_NONE = 0;

	/**
	 * Page title setting representing a title on the first page only.
	 *
	 * @since 1.0
	 * @var int
	 */
	private const PAGE_TITLE_FIRST_PAGE = 1;

	/**
	 * Page title setting representing a title on every page.
	 *
	 * @since 1.0
	 * @var int
	 */
	private const PAGE_TITLE_ALL_PAGES = 2;

	/**
	 * Page title setting representing a title on every other page, starting at page 1.
	 *
	 * @since 1.0
	 * @var int
	 */
	private const PAGE_TITLE_ODD_PAGES = 3;

	/**
	 * GravityExport Lite plugin.
	 *
	 * @since 1.0
	 * @var GravityExportAddon
	 */
	private $plugin;

	/**
	 * Micro cache for plugin settings.
	 * @since $ver$
	 * @var array|null
	 */
	private $plugin_settings = null;

	/**
	 * Initialize constructor.
	 *
	 * @throws WriterException When the writer could not be created.
	 */
	public function __construct( $plugin ) {
		$this->plugin = $plugin;

		if ( ! class_exists( IOFactory::class ) ) {
			return;
		}

		// Register our custom PDF writer as a default Renderer.
		IOFactory::registerWriter( 'Pdf', PDF::class );

		add_filter(
			'gk/gravityexport/settings/sections',
			\Closure::fromCallable( [ $this, 'add_foundation_settings' ] )
		);
		add_filter( 'gfexcel_file_extensions', \Closure::fromCallable( [ $this, 'addPdfExtensionUrl' ] ) );
		add_filter( 'gfexcel_download_renderer', \Closure::fromCallable( [ $this, 'replaceRenderer' ] ) );
		add_filter( 'gfexcel_general_settings', \Closure::fromCallable( [ $this, 'addPdfSettingFields' ] ) );
		add_filter( 'gk/gravityexport/pdf/page/orientation', \Closure::fromCallable( [
			$this,
			'setPdfOrientation'
		] ), 10, 2 );
		add_filter( 'gk/gravityexport/pdf/page/paper-size', \Closure::fromCallable( [
			$this,
			'setPdfPaperSize'
		] ), 10, 2 );

		add_action( 'gk/gravityexport/pdf/generator/init', \Closure::fromCallable( [
			$this,
			'addHeaderFooter'
		] ), 10, 2 );

		add_action( 'gk/gravityexport/pdf/config', \Closure::fromCallable( [
			$this,
			'addFonts'
		] ), 10, 2 );

		add_action( 'gk/gravityexport/pdf/page/header', \Closure::fromCallable( [
			$this,
			'addHeaderFirstPage'
		] ), 10, 3 );

		add_action( 'gk/gravityexport/pdf/page/header', \Closure::fromCallable( [
			$this,
			'setFontStyles'
		] ), 10, 3 );

		add_filter( 'admin_init', \Closure::fromCallable( [ $this, 'admin_init' ] ) );

		new Colors( GravityExportPro::get_instance(), $this->plugin );
		new InlineImages( GravityExportPro::get_instance() );
		new HeaderImage( GravityExportPro::get_instance() );
	}

	/**
	 * Initializes values for backend purposes.
	 * @since $ver$
	 */
	private function admin_init(): void {
        if ( ! GravityExport::is_gravityexport_page() ) {
            return;
        }

		wp_enqueue_style(
			'gk-gravityexport-pdf',
			plugin_dir_url( GK_GRAVITYEXPORT_PLUGIN_FILE ) . 'assets/css/gravityexport-pdf.css'
		);
	}

	/**
	 * Adds .PDF as a valid extension for the download URL.
	 *
	 * @since 1.0
	 *
	 * @param array $extensions The add-ons
	 *
	 * @return string[] The new add-ons.
	 */
	private function addPdfExtensionUrl( array $extensions ): array {
		return array_merge( $extensions, [ 'pdf' ] );
	}

	/**
	 * Decorated a {@see PHPExcelRenderer} instance.
	 *
	 * @since 1.0
	 *
	 * @param RendererInterface $renderer The current renderer.
	 *
	 * @return RendererInterface The decorated renderer.
	 */
	private function replaceRenderer( RendererInterface $renderer ): RendererInterface {
		if (
			! $renderer instanceof PHPExcelRenderer
			|| $renderer instanceof PdfRenderer
		) {
			return $renderer;
		}

		return new PdfRenderer( $renderer, dirname( __FILE__, 2 ) . '/assets' );
	}

	/**
	 * Adds the PDF specific settings to the settings page.
	 *
	 * @since 1.0
	 *
	 * @param mixed[] $settings The settings.
	 *
	 * @return mixed[] The updated settings.
	 */
	private function addPdfSettingFields( array $settings ): array {
		$settings = array_merge( $settings, [
			[
				'id'     => 'gk-gravityexport-pdf-settings',
				'title'  => esc_html__( 'PDF Export Settings', 'gk-gravityexport' ),
				'fields' => [
					[
						'name'          => self::SETTING_ORIENTATION,
						'label'         => esc_html__( 'Page Orientation', 'gk-gravityexport' ),
						'type'          => 'radio',
						'horizontal'    => true,
						'default_value' => PageSetup::ORIENTATION_LANDSCAPE,
						'choices'       => [
							[
								'name'  => self::SETTING_ORIENTATION,
								'label' => esc_html__( 'Portrait', 'gk-gravityexport' ),
								'value' => PageSetup::ORIENTATION_PORTRAIT,
							],
							[
								'name'  => self::SETTING_ORIENTATION,
								'label' => esc_html__( 'Landscape', 'gk-gravityexport' ),
								'value' => PageSetup::ORIENTATION_LANDSCAPE,
							],
						],
					],
					[
						'name'          => self::SETTING_PAPER_SIZE,
						'label'         => esc_html__( 'Page Size', 'gk-gravityexport' ),
						'type'          => 'select',
						'default_value' => PageSetup::PAPERSIZE_LETTER,
						'choices'       => [
							[
								'name'  => self::SETTING_PAPER_SIZE,
								'label' => esc_html__( 'Letter', 'gk-gravityexport' ),
								'value' => PageSetup::PAPERSIZE_LETTER,
							],
							[
								'name'  => self::SETTING_PAPER_SIZE,
								'label' => esc_html__( 'A4', 'gk-gravityexport' ),
								'value' => PageSetup::PAPERSIZE_A4,
							],
						],
					],
					[
						'name'    => self::SETTING_SHOW_PAGE_TITLE,
						'label'   => esc_html__( 'Show Title', 'gk-gravityexport' ),
						'type'    => 'select',
						'choices' => [
							[
								'name'  => self::SETTING_SHOW_PAGE_TITLE,
								'label' => esc_html__( 'No title', 'gk-gravityexport' ),
								'value' => self::PAGE_TITLE_NONE,
							],
							[
								'name'  => self::SETTING_SHOW_PAGE_TITLE,
								'label' => esc_html__( 'On first page', 'gk-gravityexport' ),
								'value' => self::PAGE_TITLE_FIRST_PAGE,
							],
							[
								'name'  => self::SETTING_SHOW_PAGE_TITLE,
								'label' => esc_html__( 'On all pages', 'gk-gravityexport' ),
								'value' => self::PAGE_TITLE_ALL_PAGES,
							],
							[
								'name'  => self::SETTING_SHOW_PAGE_TITLE,
								'label' => esc_html__( 'On every other page (double sided printing)', 'gk-gravityexport' ),
								'value' => self::PAGE_TITLE_ODD_PAGES,
							],
						],
					],
					[
						'name'    => self::SETTING_SHOW_PAGE_NUMBER,
						'label'   => esc_html__( 'Show Page Number', 'gk-gravityexport' ),
						'type'    => 'checkbox',
						'choices' => [
							[
								'label'         => esc_html__( 'Yes, show page numbers', 'gk-gravityexport' ),
								'name'          => self::SETTING_SHOW_PAGE_NUMBER,
								'default_value' => 1,
							]
						],
					],
					[
						'name'        => self::SETTING_PAGE_NUMBER_HTML,
						'label'       => esc_html__( 'Page number content', 'gk-gravityexport' ),
						'type'        => 'text',
						'placeholder' => sprintf( '%s: %s', esc_html__( 'Default', 'gk-gravityexport' ), self::$mpdf_variables['page-number'] ),
						'description' => $this->getPageNumberToolTip(),
						'dependency'  => [
							'live'   => true,
							'fields' => [
								[
									'field'  => self::SETTING_SHOW_PAGE_NUMBER,
									'values' => [ 1 ],
								],
							],
						],
					],
				],
			],
		] );

		return $settings;
	}

	/**
	 * Sets the orientation from the settings page.
	 *
	 * @since 1.0
	 *
	 * @param string $orientation The current orientation.
	 * @param int    $form_id     The form ID.
	 *
	 * @return string The orientation.
	 */
	private function setPdfOrientation( string $orientation, int $form_id ): string {
		$settings = $this->plugin->get_form_settings( \GFAPI::get_form( $form_id ) );

		return $settings[ self::SETTING_ORIENTATION ] ?? $orientation;
	}

	/**
	 * Sets the paper size from the settings page.
	 *
	 * @since 1.0
	 *
	 * @param int $size    The current size.
	 * @param int $form_id The form ID.
	 *
	 * @return int The paper size.
	 */
	private function setPdfPaperSize( int $size, int $form_id ): int {
		$settings = $this->plugin->get_form_settings( \GFAPI::get_form( $form_id ) );

		return $settings[ self::SETTING_PAPER_SIZE ] ?? $size;
	}


	/**
	 * Registers the provided fonts.
	 * @since $ver$
	 *
	 * @param array $config  The PDF config.
	 * @param int   $form_id The form ID.
	 *
	 * @return array The updated config.
	 */
	private function addFonts( array $config, int $form_id ): array {
		$fonts_dir      = dirname( __FILE__, 2 ) . '/fonts';
		$default_config = ( new ConfigVariables )->getDefaults();
		$font_config    = ( new FontVariables )->getDefaults();

		$config['fontDir']     = array_merge( $default_config['fontDir'], [ $fonts_dir ] );
		$config['sans_fonts']  = [ 'sans', 'sans-seris', 'arial', 'verdana', 'freesans' ];
		$config['serif_fonts'] = [ 'sans-serif', 'serif', 'freeserif' ];
		$config['fontdata']    = array_merge( $font_config['fontdata'], [
			'arial'   => [
				'R'  => 'Arial.ttf',
				'B'  => 'Arial_Bold.ttf',
				'I'  => 'Arial_Italic.ttf',
				'BI' => 'Arial_Bold_Italic.ttf',
			],
			'verdana' => [
				'R'  => 'Verdana.ttf',
				'B'  => 'Verdana_Bold.ttf',
				'I'  => 'Verdana_Italic.ttf',
				'BI' => 'Verdana_Bold_Italic.ttf',
			]
		] );

		return $config;
	}

	/**
	 * Defines a header and a footer for the page.
	 *
	 * @since 1.0
	 *
	 * @param Mpdf $mpdf    The Mpdf instance.
	 * @param int  $form_id The form ID.
	 */
	private function addHeaderFooter( Mpdf $mpdf, int $form_id ): void {
		$form     = \GFAPI::get_form( $form_id );
		$settings = $this->plugin->get_form_settings( $form );

		$header = $footer = [ 'even' => '', 'odd' => '' ];

		if ( $settings[ self::SETTING_SHOW_PAGE_NUMBER ] ?? true ) {
			$alignment      = gf_apply_filters(
				[ 'gk/gravityexport/pdf/page-number-alignment', $form_id ],
				$this->get_plugin_settings( self::SETTING_PAGE_NUMBER_ALIGNMENT, 'center' ),
			);
			$footer['odd']  .= $this->getPageNumberHtml( $form, $alignment );
			$footer['even'] .= $this->getPageNumberHtml( $form, $alignment );
		}

		/**
		 * @filter `gk/gravityexport/pdf/footer` Modify the PDF footer HTML.
		 *
		 * @since  $ver$
		 *
		 * @param string[] $footer         The HTMLs.
		 * @param string[] $mpdf_variables Mapping of available PDF variables.
		 * @param array    $form           The form object.
		 */
		$footer = gf_apply_filters( [
			'gk/gravityexport/pdf/footer',
			$form['id']
		], $footer, self::$mpdf_variables, $form );

		if (
			( $show_page_title = (int) ( $settings[ self::SETTING_SHOW_PAGE_TITLE ] ?? 0 ) )
			&& $show_page_title !== self::PAGE_TITLE_FIRST_PAGE
		) {
			$header_html   = $this->getHeaderHtml( $form );
			$header['odd'] .= $header_html;
			if ( $show_page_title === self::PAGE_TITLE_ALL_PAGES ) {
				$header['even'] .= $header_html;
			}
		}

		/**
		 * @filter `gk/gravityexport/pdf/header` Modify the PDF header HTML.
		 *
		 * @since  $ver$
		 *
		 * @param string[] $header         The HTMLs.
		 * @param string[] $mpdf_variables Mapping of available PDF variables.
		 * @param array    $form           The form object.
		 */
		$header = gf_apply_filters( [
			'gk/gravityexport/pdf/header',
			$form['id']
		], $header, $form );

		foreach ( [ 'even', 'odd' ] as $side ) {
			if ( ! empty( $footer[ $side ] ?? '' ) ) {
				$mpdf->SetHTMLFooter( $footer[ $side ], $side === 'even' ? 'E' : 'O' );
			}

			if ( ! empty( $header[ $side ] ?? '' ) ) {
				$mpdf->SetHTMLHeader( $header[ $side ], $side === 'even' ? 'E' : 'O' );
			}
		}
	}

	/**
	 * Adds the header HTML to the first page if necessary.
	 *
	 * @since $ver$
	 *
	 * @param string $html    The PDF header HTML.
	 * @param mixed  $_       Unused Spreadsheet object.
	 * @param int    $form_id The form ID.
	 *
	 * @return string The header HTML.
	 */
	private function addHeaderFirstPage( string $html, $_, int $form_id ): string {
		$form     = \GFAPI::get_form( $form_id );
		$settings = $this->plugin->get_form_settings( $form );

		if ( self::PAGE_TITLE_FIRST_PAGE === (int) ( $settings[ self::SETTING_SHOW_PAGE_TITLE ] ?? 0 ) ) {
			return $html . $this->getHeaderHtml( $form ) . '<br>';
		}

		return $html;
	}

	/**
	 * Adds the font family CSS to the html.
	 * @since $ver$
	 *
	 * @param mixed  $_       Unused Spreadsheet object.
	 * @param int    $form_id The form ID.
	 *
	 * @param string $html    The PDF header HTML.
	 *
	 * @return string The header HTML.
	 */
	private function setFontStyles( string $html, $_, int $form_id ): string {
		$font_family = gf_apply_filters(
			[ 'gk/gravityexport/pdf/font-family', $form_id ],
			$this->get_plugin_settings( self::SETTING_FONT_FAMILY, 'sans-serif' )
		);

		return str_replace( '</style>', sprintf( "body { font-family: %s; }\n</style>", $font_family ), $html );
	}

	/**
	 * Returns the Header HTML for the PDF.
	 *
	 * @since  $ver$
	 *
	 * @param array $form The form object.
	 *
	 * @return string The HTML.
	 */
	private function getHeaderHtml( array $form ): string {
		$title = gf_apply_filters( [ 'gfexcel_renderer_title', $form['id'] ], $form['title'] ?? '', $form );
		$html  = sprintf( '<div class="page-title">%s</div>', $title );

		/**
		 * @filter `gk/gravityexport/pdf/header/title` Modify the PDF title HTML.
		 *
		 * @since  $ver$
		 *
		 * @param string $html  The HTML.
		 * @param string $title The page title.
		 * @param array  $form  The form object.
		 */
		return gf_apply_filters( [
			'gk/gravityexport/pdf/header/title',
			$form['id']
		], $html, $title, $form );
	}

	/**
	 * Returns the Header HTML for the PDf.
	 * @since  $ver$
	 *
	 * @param array $form The form object.
	 *
	 * @return string The HTML.
	 *
	 */
	private function getPageNumberHtml( array $form, string $alignment ): string {
		$settings    = $this->plugin->get_form_settings( $form );
		$page_number = rgar( $settings, self::SETTING_PAGE_NUMBER_HTML, self::$mpdf_variables['page-number'] );

		return sprintf( '<div class="page-number %s">%s</div>', $alignment, $page_number );
	}

	/**
	 * Creates and returns the tooltip for the page number field.
	 * @since $ver$
	 */
	private function getPageNumberToolTip(): string {
		$tooltip = "<code>[page-number]</code> will be replaced by the current page number, and
<code>[total-page-numbers]</code> by the total page count.";

		return preg_replace_callback( '/\[(?<tag>[^]]+)]/', function ( array $matches ) {
			return self::$mpdf_variables[ $matches['tag'] ] ?? $matches[0];
		}, __( $tooltip, 'gk-gravityexport' ) );
	}

	/**
	 * Adds the global settings to foundation.
	 * @since $ver$
	 *
	 * @param array $sections The setting sections.
	 *
	 * @return array The setting settings.
	 */
	private function add_foundation_settings( array $sections ): array {
		$sections[] = [
			'title'    => esc_html__( 'PDF settings', 'gk-gravityexport' ),
			'id'       => 'pdf-section',
			'settings' => [
				[
					'name'    => self::SETTING_FONT_FAMILY,
					'id'      => self::SETTING_FONT_FAMILY,
					'title'   => esc_html__( 'Font family', 'gk-gravityexport' ),
					'type'    => 'select',
					'value'   => $this->get_plugin_settings( self::SETTING_FONT_FAMILY ),
					'choices' => [
						[
							'title'   => 'Sans Serif',
							'choices' => [
								[
									'title' => 'Arial',
									'value' => 'arial',
								],
								[
									'title' => 'Free Sans',
									'value' => 'freesans',
								],
								[
									'title' => 'Verdana',
									'value' => 'verdana',
								],
							],
						],
						[
							'title'   => 'Serif',
							'choices' => [
								[
									'title' => 'Free Serif',
									'value' => 'freeserif',
								],
							]
						],
					],
				],
				[
					'name'    => self::SETTING_PAGE_NUMBER_ALIGNMENT,
					'id'      => self::SETTING_PAGE_NUMBER_ALIGNMENT,
					'title'   => esc_html__( 'Page number alignment', 'gk-gravityexport' ),
					'type'    => 'select',
					'value'   => $this->get_plugin_settings( self::SETTING_PAGE_NUMBER_ALIGNMENT ),
					'choices' => [
						[
							'title' => esc_html__( 'Left', 'gk-gravityexport' ),
							'value' => 'left',
						],
						[
							'title' => esc_html__( 'Center', 'gk-gravityexport' ),
							'value' => 'center',
						],
						[
							'title' => esc_html__( 'Right', 'gk-gravityexport' ),
							'value' => 'right',
						],
					],
				],
			],
		];

		return $sections;
	}

	/**
	 * Returns the default global settings for this plugin.
	 * @since $ver$
	 * @return array The default settings.
	 */
	private function get_default_plugin_settings(): array {
		$defaults = [
			self::SETTING_FONT_FAMILY           => 'arial',
			self::SETTING_PAGE_NUMBER_ALIGNMENT => 'center',
		];

		/**
		 * @filter `gk/gravityexport/pdf/settings/defaults` Filter default global settings.
		 *
		 * @param array $defaults The defaults settings.
		 */
		return apply_filters( 'gk/gravityexport/pdf/settings/defaults', $defaults );
	}

	/**
	 * Returns the setting for the provided key.
	 * @since $ver$
	 *
	 * @param string $key     The setting to retrieve.
	 * @param mixed  $default The default value to return
	 *
	 * @return mixed The setting.
	 */
	private function get_plugin_settings( string $key, $default = null ) {
		if ( $this->plugin_settings !== null ) {
			return $this->plugin_settings[ $key ] ?? $default;
		}

		$this->plugin_settings = array_merge(
			$this->get_default_plugin_settings(),
			GravityExportPro::get_instance()->get_plugin_settings(),
		);

		return $this->plugin_settings[ $key ] ?? $default;
	}
}
