<?php
namespace WPDRMS\ASP\Media;

/* Prevent direct access */

use Exception;
use WPDRMS\ASP\Media\RemoteService\License;
use WPDRMS\ASP\Utils\FileManager;
use WPDRMS\ASP\Utils\Str;

defined( 'ABSPATH' ) or die( "You can't access this file directly." );

class Parser {
	/**
	 * Mime groups array
	 *
	 * @var array
	 */
	private static $mimes = array(
		'image'          => array(
			'image/jpeg',
			'image/gif',
			'image/png',
			'image/bmp',
			'image/tiff',
			'image/x-icon',
		),
		'pdf'            => array(
			'application/pdf',
		),
		'text'           => array(
			'text/plain',
			'text/csv',
			'text/tab-separated-values',
			'text/calendar',
			'text/css',
			'text/vtt',
			'text/html',
		),
		'richtext'       => array(
			'text/richtext',
			'application/rtf',
		),
		'mso_word'       => array(
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
			'application/vnd.ms-word.document.macroEnabled.12',
			'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
			'application/vnd.ms-word.template.macroEnabled.12',
			'application/vnd.oasis.opendocument.text',
		),
		'mso_excel'      => array(
			'application/vnd.ms-excel',
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
			'application/vnd.ms-excel.sheet.macroEnabled.12',
			'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
			'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
			'application/vnd.ms-excel.template.macroEnabled.12',
			'application/vnd.ms-excel.addin.macroEnabled.12',
			'application/vnd.oasis.opendocument.spreadsheet',
			'application/vnd.oasis.opendocument.chart',
			'application/vnd.oasis.opendocument.database',
			'application/vnd.oasis.opendocument.formula',
		),
		'mso_powerpoint' => array(
			'application/vnd.ms-powerpoint',
			'application/vnd.openxmlformats-officedocument.presentationml.presentation',
			'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
			'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
			'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
			'application/vnd.openxmlformats-officedocument.presentationml.template',
			'application/vnd.ms-powerpoint.template.macroEnabled.12',
			'application/vnd.ms-powerpoint.addin.macroEnabled.12',
			'application/vnd.openxmlformats-officedocument.presentationml.slide',
			'application/vnd.ms-powerpoint.slide.macroEnabled.12',
			'application/vnd.oasis.opendocument.presentation',
			'application/vnd.oasis.opendocument.graphics',
		),
	);
	private $args, $post, $filepath;


	/**
	 * Constructor.
	 *
	 * @param \WP_Post $post
	 * @param array    $args
	 */
	function __construct( $post, $args ) {
		$defaults = array(
			'media_service_send_file' => false,
			'pdf_parser'              => 'auto', // auto, smalot, pdf2txt
			'text_content'            => true,
			'richtext_content'        => true,
			'pdf_content'             => true,
			'msword_content'          => true,
			'msexcel_content'         => true,
			'msppt_content'           => true,
		);

		$this->post = $post;
		$this->args = wp_parse_args($args, $defaults);
		$this->args = apply_filters('asp_media_parser_args', $this->args, $defaults);

		// To allow adding mimes programmatically
		self::$mimes = apply_filters('asp/media/parser/mimes', self::$mimes);
	}

	/**
	 * Parses the file contents
	 *
	 * @return string|\WP_Error
	 */
	function parse( $remote = true ) {
		$this->filepath = get_attached_file( $this->post->ID );
		if ( is_wp_error($this->filepath) || empty($this->filepath) || !file_exists($this->filepath) ) {
			return '';
		}

		$text = '';
		if ( $this->isThisParseEnabled() ) {
			if ( $remote ) {
				$text = $this->parseRemote();
			} else {
				$text = $this->parseLocal();
			}
			/**
			 * In case of PDF there are many cases of gibberish in text when using OCR, such as
			 * "my text a<bc my other text" -> where "<bc" is treated as an opening HTML tag.
			 * Then running strip_tags("my text a<bc my other text") = "my text a" -> will remove everything
			 * after the tag starting bracket, because the "my other text" is considered as tag attributes.
			 * The best course of action here is to simply remove these symbols.
			 */
			if ( !is_wp_error($text) && $this->isThis('pdf') ) {
				$text = str_replace(array( '<', '>' ), ' ', $text);
			}
		}

		return $text;
	}

	function parseRemote() {
		$text    = '';
		$license = License::getInstance();

		if ( $license->active() && $license->valid() ) {
			if ( !in_array('_asp_attachment_text', get_post_custom_keys($this->post->ID)) ) {
				$license_data         = $license->getData();
				$license_max_filesize =
					isset($license_data['stats'], $license_data['stats']['max_filesize']) ?
						$license_data['stats']['max_filesize'] : 0;
				$parser               = new RemoteService\Parser(
					$this->filepath,
					wp_get_attachment_url($this->post->ID),
					$license->get(),
					$this->args['media_service_send_file'],
					$license_max_filesize
				);
				$text                 = $parser->request();
				$license->setStats($parser->getStats());

				if ( !is_wp_error($text) ) {
					update_post_meta($this->post->ID, '_asp_attachment_text', $text);
				} else {
					return $text;
				}
			} else {
				$text = get_post_meta($this->post->ID, '_asp_attachment_text', true);
			}
		}

		return $text;
	}

	function parseLocal() {
		if ( $this->isThis('text') && $this->args['text_content'] ) {
			$text = $this->parseTXT();
		} elseif ( $this->isThis('richtext') && $this->args['richtext_content'] ) {
			$text = $this->parseRTF();
		} elseif ( $this->isThis('pdf') && $this->args['pdf_content'] ) {
			$text = $this->parsePDF();
		} elseif ( $this->isThis('mso_word') && $this->args['msword_content'] ) {
			$text = $this->parseMSOWord();
		} elseif ( $this->isThis('mso_excel') && $this->args['msexcel_content'] ) {
			$text = $this->parseMSOExcel();
		} elseif ( $this->isThis('mso_powerpoint') && $this->args['msppt_content'] ) {
			$text = $this->parseMSOPpt();
		} else {
			$text = '';
		}
		return $text;
	}

	/**
	 * Checks if a mime type belongs to a certain mime group (text, richtext etc..)
	 *
	 * @param string $type
	 * @return bool
	 */
	public function isThis( string $type = 'text' ): bool {
		return ( isset(self::$mimes[ $type ]) && in_array($this->post->post_mime_type, self::$mimes[ $type ]) );
	}

	public function isThisParseEnabled(): bool {
		return ( $this->isThis('text') && $this->args['text_content'] ) ||
			( $this->isThis('richtext') && $this->args['richtext_content'] ) ||
			( $this->isThis('pdf') && $this->args['pdf_content'] ) ||
			( $this->isThis('mso_word') && $this->args['msword_content'] ) ||
			( $this->isThis('mso_excel') && $this->args['msexcel_content'] ) ||
			( $this->isThis('mso_powerpoint') && $this->args['msppt_content'] ) ||
			( $this->isThis('image') && License::instance()->active() && !License::instance()->isFree() );
	}

	/**
	 * Gets contents from a text based file
	 *
	 * @return string
	 */
	function parseTXT() {
		$contents = FileManager::instance()->read($this->filepath);
		// CSV files often store the values in quotes. We don't need those in this case.
		if ( $this->post->post_mime_type == 'text/csv' ) {
			$contents = str_replace(array( '"', "'" ), ' ', $contents);
		}

		return $contents;
	}

	/**
	 * Gets contents from a richtext file
	 *
	 * @return string
	 */
	function parseRTF() {
		$rtf = FileManager::instance()->read($this->filepath);
		if ( $rtf != '' ) {
			include_once ASP_EXTERNALS_PATH . 'class.rtf-html-php.php';
			$reader = new \ASP_RtfReader();
			$reader->Parse($rtf);
			$formatter = new \ASP_RtfHtml();

			return html_entity_decode(strip_tags($formatter->Format($reader->root)));
		}

		return '';
	}

	/**
	 * Gets contents from a PDF file
	 *
	 * @return string
	 */
	function parsePDF() {
		$args     = $this->args;
		$contents = '';

		// PDF Parser for php 5.3 and above
		if ( $args['pdf_parser'] == 'auto' || $args['pdf_parser'] == 'smalot' ) {
			if ( version_compare(PHP_VERSION, '5.3', '>=') ) {
				include_once ASP_EXTERNALS_PATH . 'class.pdfsmalot.php';
				$parser = new \ASP_PDFSmalot();
				$parser = $parser->getObj();
				try {
					$pdf      = $parser->parseFile($this->filepath);
					$contents = $pdf->getText();
				} catch ( Exception $e ) {
					echo 'Caught exception: ', $e->getMessage(), "\n";
				}
			}
		}

		// Different method maybe?
		if ( $args['pdf_parser'] == 'auto' || $args['pdf_parser'] == 'pdf2txt' ) {
			if ( $contents == '' ) {
				include_once ASP_EXTERNALS_PATH . 'class.pdf2txt.php';

				$pdfParser = new \ASP_PDF2Text();
				$pdfParser->setFilename($this->filepath);
				$pdfParser->decodePDF();
				$contents = $pdfParser->output();
			}
		}

		return $contents;
	}

	/**
	 * Gets contents from an Office Word file
	 *
	 * @return string
	 */
	function parseMSOWord() {
		if ( false !== strpos( $this->post->post_mime_type, 'opendocument' ) ) {
			$o = $this->getFileFromArchive('content.xml', $this->filepath);
		} else {
			$o = $this->getFileFromArchive('word/document.xml', $this->filepath);
		}
		return $o;
	}

	/**
	 * Gets contents from an Office Excel file
	 *
	 * @return string
	 */
	function parseMSOExcel() {
		if ( false !== strpos($this->post->post_mime_type, 'opendocument') ) {
			$o = $this->getFileFromArchive('content.xml', $this->filepath);
		} elseif ( substr_compare($this->filepath, '.xls', -strlen('.xls')) === 0 ) {
			$o = '';
			include_once ASP_EXTERNALS_PATH . 'php-excel/autoload.php';
			$reader = \Asan\PHPExcel\Excel::load(
				$this->filepath,
				function ( \Asan\PHPExcel\Reader\Xls $reader ) use ( $o ) {
					$reader->ignoreEmptyRow(true);
				}
			);
			foreach ( $reader->sheets() as $shk => $sheet ) {
				$reader->setSheetIndex($shk);
				foreach ( $reader as $row ) {
					$o .= ' ' . Str::anyToString($row);
				}
			}
		} else {
			$o = $this->getFileFromArchive('xl/sharedStrings.xml', $this->filepath);
		}
		return $o;
	}

	/**
	 * Gets contents from an Office Powerpoint file
	 *
	 * @return string
	 */
	function parseMSOPpt() {
		$out = '';
		if ( class_exists( '\\ZipArchive' ) ) {
			if ( false !== strpos($this->post->post_mime_type, 'opendocument') ) {
				$out = $this->getFileFromArchive('content.xml', $this->filepath);
			} else {
				$zip = new \ZipArchive();
				if ( true === $zip->open($this->filepath) ) {
					$slide_num = 1;
					while ( false !== ( $xml_index = $zip->locateName('ppt/slides/slide' . absint($slide_num) . '.xml') ) ) {
						$xml  = $zip->getFromIndex($xml_index);
						$out .= ' ' . $this->getXMLContent($xml);
						++$slide_num;
					}
					$zip->close();
				} elseif ( substr_compare($this->filepath, '.ppt', -strlen('.ppt')) === 0 ) {
					// This approach uses detection of the string "chr(0f).Hex_value.chr(0x00).chr(0x00).chr(0x00)" to find text strings, which are then terminated by another NUL chr(0x00). [1] Get text between delimiters [2]
					$fileHandle = fopen($this->filepath, 'r');
					$line       = @fread($fileHandle, filesize($this->filepath));
					$lines      = explode(chr(0x0f), $line);
					$outtext    = '';

					foreach ( $lines as $thisline ) {
						if ( strpos($thisline, chr(0x00) . chr(0x00) . chr(0x00)) == 1 ) {
							$text_line = substr($thisline, 4);
							$end_pos   = strpos($text_line, chr(0x00));
							$text_line = substr($text_line, 0, $end_pos);
							$text_line = preg_replace("/[^a-zA-Z0-9\s\,\.\-\n\r\t@\/\_\(\)]/", '', $text_line);
							if ( strlen($text_line) > 1 ) {
								$outtext .= substr($text_line, 0, $end_pos) . "\n";
							}
						}
					}
					$out = $outtext;
				}
			}
		}

		return $out;
	}

	/**
	 * Gets the content from an XML string
	 *
	 * @param $xml
	 * @return string
	 */
	private function getXMLContent( $xml ) {
		if ( class_exists('\\DOMDocument') ) {
			$dom = new \DOMDocument();
			$dom->loadXML($xml, LIBXML_NOENT | LIBXML_XINCLUDE | LIBXML_NOERROR | LIBXML_NOWARNING);
			return $dom->saveXML();
		}
		return '';
	}

	/**
	 * Gets a file from an archive, based on the xml file name
	 *
	 * @param $xml
	 * @param $filename
	 * @return string
	 */
	private function getFileFromArchive( $xml, $filename ) {
		if ( class_exists('\\ZipArchive') && class_exists('\\DOMDocument') ) {
			$output_text = '';
			$zip         = new \ZipArchive();

			if ( true === $zip->open($filename) ) {
				if ( false !== ( $xml_index = $zip->locateName($xml) ) ) {
					$xml_data = $zip->getFromIndex($xml_index);
					$dom      = new \DOMDocument();
					$dom->loadXML( $xml_data, LIBXML_NOENT | LIBXML_XINCLUDE | LIBXML_NOERROR | LIBXML_NOWARNING );
					$output_text = $dom->saveXML();
				}
				$zip->close();
			} else {
				// File open error
				return '';
			}

			return $output_text;
		}

		// The ZipArchive class is missing
		return '';
	}
}
