<?php
/**
 * Extends the side by side renderer to add a checkbox to each row.
 *
 * @package EaLab\WP_Deployer
 */

declare(strict_types=1);

namespace EaLab\WP_Deployer\vendor\Jfcherng\Diff\Renderer\Html;

use EaLab\WP_Deployer\vendor\Jfcherng\Diff\SequenceMatcher;

/**
 * Interactive Side by Side HTML diff generator.
 * 
 * This generator REQUIRES JsonHtmlWithRaw renderer as the lines provider.
 */
final class InteractiveSideBySide extends AbstractHtml
{
	protected int $currentDiffLineNumber = 0;

    /**
     * {@inheritdoc}
     */
    public const INFO = [
        'desc' => 'Interactive side by side',
        'type' => 'Html',
    ];

	/**
	 * Builds an encoded value for the HTML attribute.
	 *
	 * @param string|null $value The value to build.
	 *
	 * @return string The built value.
	 */
	protected function buildValueForAttribute(?string $value): string {
		// Encapsulate the value into a structured array to support null value
		// in the input value.
		if ($value === null) {
			$jsonEncapsulatedValue = json_encode(['value' => null]);
		} else {
			$jsonEncapsulatedValue = json_encode(['value' => $value]);
		}

		// Encode the value for HTML attribute.
		return $this->encode($jsonEncapsulatedValue);
	}

	/**
	 * Encode string.
	 *
	 * @param string $string The string to encode.
	 * @param bool $forInputName (Optional) Whether the string is for input name. Defaults to false.
	 *
	 * @return string The encoded string.
	 */
	protected function encode(string $string, bool $forInputName = false): string {
		$encodeForInputName = function (string $input): string {
			// PHP converts the dot in the input name to underscore in the $_POST array,
			// so we need to convert the dot to something else.
			return str_replace(['.'], ['<dsc:dot>'], $input);
		};

		return htmlspecialchars(
			$forInputName ? $encodeForInputName($string) : $string,
			ENT_QUOTES,
			'UTF-8');
	}

    /**
     * {@inheritdoc}
     */
    protected function redererChanges(array $changes): string
    {
        if (empty($changes)) {
            return $this->getResultForIdenticals();
        }

        $wrapperClasses = [
            ...$this->options['wrapperClasses'],
            'diff', 'diff-html', 'diff-interactive-side-by-side',
        ];

	    $this->currentDiffLineNumber = 0;

        return
            '<table class="' . implode(' ', $wrapperClasses) . '">' .
                $this->renderTableHeader() .
                $this->renderTableHunks($changes) .
            '</table>' .
            $this->renderMetadata();
    }

	/**
	 * Renders metadata not directly related to the diff.
	 *
	 * Dev note: This can be moved out of this class, as metadata
	 * are not directly related to the diff.
	 * I.e. move the implementation to the caller which can then
	 * specify the metadata to render (by injecting callbacks).
	 *
	 * @return string The rendered metadata.
	 */
	protected function renderMetadata(): string {
		$metadata = '';

		if (!empty($this->options['uses_php_serialized_processor'])) {
			// Add a hidden input to tell that the field is reserialized
			// by the PHP serialized processor.
			$inputName = ($this->options['diff_id'] ?? '');

			if (!empty($inputName)) {
				$metadata .= '<input type="hidden" name="uses_php_serialized_processor[]" value="' . $this->encode( $inputName ) . '">';
			}
		}

		return $metadata;
	}

	/**
	 * Renders the radio input.
	 *
	 * @param string $inputName The input name.
	 * @param string|null $inputValue The input value. May be null.
	 * @param string|null $checkedValue (Optional) The checked value, if any.
	 *
	 * @return string The rendered radio input.
	 */
	protected function renderRadioInput(string $inputName, ?string $inputValue, ?string $checkedValue = null): string
	{
		$attrChecked = $inputValue === $checkedValue ? ' checked="checked"' : '';

		$attrName = ' name="merge[' . $this->encode($inputName, true) . ']"';

		$attrValue = ' value="' . $this->buildValueForAttribute($inputValue) . '"';

		return '<input type="radio"' . $attrName . $attrValue . $attrChecked . '/>';
	}

    /**
     * Renderer the table header.
     */
    protected function renderTableHeader(): string
    {
        if (!$this->options['showHeader']) {
            return '';
        }

        return
            '<thead>' .
                '<tr>' .
                    '<th>' . $this->_('old_version') . '</th>' .
                    ($this->options['lineNumbers'] ? '<th></th>' : '') .
                    '<th></th>' .
                    '<th></th>' .
                    '<th>' . $this->_('new_version') . '</th>' .
                    ($this->options['lineNumbers'] ? '<th></th>' : '') .
                '</tr>' .
            '</thead>';
    }

    /**
     * Renderer the table separate block.
     */
    protected function renderTableSeparateBlock(): string
    {
        $colspan = $this->options['lineNumbers'] ? '6' : '4';

        return
            '<tbody class="skipped">' .
                '<tr>' .
                    '<td colspan="' . $colspan . '"></td>' .
                '</tr>' .
            '</tbody>';
    }

    /**
     * Renderer table hunks.
     *
     * @param array[][] $hunks each hunk has many blocks
     */
    protected function renderTableHunks(array $hunks): string
    {
        $ret = '';

        foreach ($hunks as $i => $hunk) {
            if ($i > 0 && $this->options['separateBlock']) {
                $ret .= $this->renderTableSeparateBlock();
            }

            foreach ($hunk as $block) {
                $ret .= $this->renderTableBlock($block);
            }
        }

        return $ret;
    }

    /**
     * Renderer the table block.
     *
     * @param array $block the block
     */
    protected function renderTableBlock(array $block): string
    {
        switch ($block['tag']) {
            case SequenceMatcher::OP_EQ:
                $content = $this->renderTableBlockEqual($block);
                break;
            case SequenceMatcher::OP_INS:
                $content = $this->renderTableBlockInsert($block);
                break;
            case SequenceMatcher::OP_DEL:
                $content = $this->renderTableBlockDelete($block);
                break;
            case SequenceMatcher::OP_REP:
                $content = $this->renderTableBlockReplace($block);
                break;
            default:
                $content = '';
        }

        return '<tbody class="change change-' . self::TAG_CLASS_MAP[$block['tag']] . '">' . $content . '</tbody>';
    }

    /**
     * Renderer the table block: equal.
     *
     * @param array $block the block
     */
    protected function renderTableBlockEqual(array $block): string
    {
        $ret = '';

        $rowCount = \count($block['new']['lines']);

        for ($no = 0; $no < $rowCount; ++$no) {
            $ret .= $this->renderTableRow(
                $block['old']['lines'][$no],
                $block['new']['lines'][$no],
                $block['old']['offset'] + $no + 1,
                $block['new']['offset'] + $no + 1,
	            $block['old']['raw_lines'][$no] ?? null,
	            $block['new']['raw_lines'][$no] ?? null,
            );
        }

        return $ret;
    }

    /**
     * Renderer the table block: insert.
     *
     * @param array $block the block
     */
    protected function renderTableBlockInsert(array $block): string
    {
        $ret = '';

        foreach ($block['new']['lines'] as $no => $newLine) {
            $ret .= $this->renderTableRow(
                null,
                $newLine,
                null,
                $block['new']['offset'] + $no + 1,
	            null,
	            $block['new']['raw_lines'][$no] ?? null,
            );
        }

        return $ret;
    }

    /**
     * Renderer the table block: delete.
     *
     * @param array $block the block
     */
    protected function renderTableBlockDelete(array $block): string
    {
        $ret = '';

        foreach ($block['old']['lines'] as $no => $oldLine) {
            $ret .= $this->renderTableRow(
                $oldLine,
                null,
                $block['old']['offset'] + $no + 1,
                null,
	            $block['old']['raw_lines'][$no] ?? null,
	            null
            );
        }

        return $ret;
    }

    /**
     * Renderer the table block: replace.
     *
     * @param array $block the block
     */
    protected function renderTableBlockReplace(array $block): string
    {
        $ret = '';

        $lineCountMax = max(\count($block['old']['lines']), \count($block['new']['lines']));

        for ($no = 0; $no < $lineCountMax; ++$no) {
            if (isset($block['old']['lines'][$no])) {
                $oldLineNum = $block['old']['offset'] + $no + 1;
                $oldLine = $block['old']['lines'][$no];
				$oldLineRaw = $block['old']['raw_lines'][$no] ?? null;
            } else {
                $oldLineNum = $oldLine = $oldLineRaw = null;
            }

            if (isset($block['new']['lines'][$no])) {
                $newLineNum = $block['new']['offset'] + $no + 1;
                $newLine = $block['new']['lines'][$no];
				$newLineRaw = $block['new']['raw_lines'][$no] ?? null;
            } else {
                $newLineNum = $newLine = $newLineRaw = null;
            }

            $ret .= $this->renderTableRow($oldLine, $newLine, $oldLineNum, $newLineNum, $oldLineRaw, $newLineRaw);
        }

        return $ret;
    }

    /**
     * Renderer a content row of the output table.
     *
     * @param null|string $oldLine    the old line
     * @param null|string $newLine    the new line
     * @param null|int    $oldLineNum the old line number
     * @param null|int    $newLineNum the new line number
     * @param null|string $oldLineRaw the old line raw
     * @param null|string $newLineRaw the new line raw
     */
    protected function renderTableRow(
        ?string $oldLine,
        ?string $newLine,
        ?int $oldLineNum,
        ?int $newLineNum,
	    ?string $oldLineRaw,
	    ?string $newLineRaw
    ): string {
		$diffLineId = ($this->options['diff_id'] ?? '') . ':diff-line_' . ++$this->currentDiffLineNumber;

		if ( $oldLine === $newLine ) {
			// No change means we can include the value in a hidden input.
			$hiddenInput = '<input type="hidden" name="merge[' . $this->encode($diffLineId, true) . ']" value="' . $this->buildValueForAttribute($oldLineRaw) . '">';
			$middleColumns = $this->renderLineContentColumn('unchanged', $hiddenInput) . $this->renderLineContentColumn('empty', '');
		} else {
			// Provide the radio buttons for the user to select the value.
			$middleColumns =
				$this->renderLineContentColumn('radio-old', $this->renderRadioInput($diffLineId, $oldLineRaw)) .
			    $this->renderLineContentColumn('radio-new', $this->renderRadioInput($diffLineId, $newLineRaw, $newLineRaw));
		}

        return
            '<tr>' .
                (
                    $this->options['lineNumbers']
                        ? $this->renderLineNumberColumn('old', $oldLineNum)
                        : ''
                ) .
                $this->renderLineContentColumn('old', $oldLine) .
                $middleColumns .
                (
                    $this->options['lineNumbers']
                        ? $this->renderLineNumberColumn('new', $newLineNum)
                        : ''
                ) .
                $this->renderLineContentColumn('new', $newLine) .
            '</tr>';
    }

    /**
     * Renderer the line number column.
     *
     * @param string   $type    the diff type
     * @param null|int $lineNum the line number
     */
    protected function renderLineNumberColumn(string $type, ?int $lineNum): string
    {
        return isset($lineNum)
            ? '<th class="n-' . $type . '">' . $lineNum . '</th>'
            : '<th></th>';
    }

    /**
     * Renderer the line content column.
     *
     * @param string      $type    the diff type
     * @param null|string $content the line content
     */
    protected function renderLineContentColumn(string $type, ?string $content): string
    {
        return
            '<td class="' . $type . (isset($content) ? '' : ' none') . '">' .
                $content .
            '</td>';
    }
}
