<?php

declare(strict_types=1);

namespace Jfcherng\Diff\Renderer\Text;

use Jfcherng\Diff\Differ;
use Jfcherng\Diff\SequenceMatcher;

/**
 * Context diff generator.
 *
 * @see https://en.wikipedia.org/wiki/Diff#Context_format
 */
final class Context extends AbstractText
{
    /**
     * {@inheritdoc}
     */
    public const INFO = [
        'desc' => 'Context',
        'type' => 'Text',
    ];

    /**
     * @var int the union of OPs that indicate there is a change
     */
    public const OP_BLOCK_CHANGED =
        SequenceMatcher::OP_DEL |
        SequenceMatcher::OP_INS |
        SequenceMatcher::OP_REP;

    /**
     * {@inheritdoc}
     */
    protected function renderWorker(Differ $differ): string
    {
        $ret = '';

        foreach ($differ->getGroupedOpcodesGnu() as $hunk) {
            $lastBlockIdx = \count($hunk) - 1;

            // note that these line number variables are 0-based
            $i1 = $hunk[0][1];
            $i2 = $hunk[$lastBlockIdx][2];
            $j1 = $hunk[0][3];
            $j2 = $hunk[$lastBlockIdx][4];

            $ret .=
                $this->cliColoredString("***************\n", '@') .
                $this->renderHunkHeader('*', $i1, $i2) .
                $this->renderHunkOld($differ, $hunk) .
                $this->renderHunkHeader('-', $j1, $j2) .
                $this->renderHunkNew($differ, $hunk);
        }

        return $ret;
    }

    /**
     * Render the hunk header.
     *
     * @param string $symbol the symbol
     * @param int    $a1     the begin index
     * @param int    $a2     the end index
     */
    protected function renderHunkHeader(string $symbol, int $a1, int $a2): string
    {
        $a1x = $a1 + 1; // 1-based begin line number

        return $this->cliColoredString(
            "{$symbol}{$symbol}{$symbol} " .
            ($a1x < $a2 ? "{$a1x},{$a2}" : $a2) .
            " {$symbol}{$symbol}{$symbol}{$symbol}\n",
            '@', // symbol
        );
    }

    /**
     * Render the old hunk.
     *
     * @param Differ  $differ the differ object
     * @param int[][] $hunk   the hunk
     */
    protected function renderHunkOld(Differ $differ, array $hunk): string
    {
        $ret = '';
        $hunkOps = 0;
        $noEolAtEofIdx = $differ->getOldNoEolAtEofIdx();

        foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
            // OP_INS does not belongs to an old hunk
            if ($op === SequenceMatcher::OP_INS) {
                continue;
            }

            $hunkOps |= $op;

            $ret .= $this->renderContext(
                self::SYMBOL_MAP[$op],
                $differ->getOld($i1, $i2),
                $i2 === $noEolAtEofIdx,
            );
        }

        // if there is no content changed, the hunk context should be omitted
        return $hunkOps & self::OP_BLOCK_CHANGED ? $ret : '';
    }

    /**
     * Render the new hunk.
     *
     * @param Differ  $differ the differ object
     * @param int[][] $hunk   the hunk
     */
    protected function renderHunkNew(Differ $differ, array $hunk): string
    {
        $ret = '';
        $hunkOps = 0;
        $noEolAtEofIdx = $differ->getNewNoEolAtEofIdx();

        foreach ($hunk as [$op, $i1, $i2, $j1, $j2]) {
            // OP_DEL does not belongs to a new hunk
            if ($op === SequenceMatcher::OP_DEL) {
                continue;
            }

            $hunkOps |= $op;

            $ret .= $this->renderContext(
                self::SYMBOL_MAP[$op],
                $differ->getNew($j1, $j2),
                $j2 === $noEolAtEofIdx,
            );
        }

        // if there is no content changed, the hunk context should be omitted
        return $hunkOps & self::OP_BLOCK_CHANGED ? $ret : '';
    }

    /**
     * Render the context array with the symbol.
     *
     * @param string   $symbol     the symbol
     * @param string[] $context    the context
     * @param bool     $noEolAtEof there is no EOL at EOF in this block
     */
    protected function renderContext(string $symbol, array $context, bool $noEolAtEof = false): string
    {
        if (empty($context)) {
            return '';
        }

        $ret = "{$symbol} " . implode("\n{$symbol} ", $context) . "\n";
        $ret = $this->cliColoredString($ret, $symbol);

        if ($noEolAtEof) {
            $ret .= self::GNU_OUTPUT_NO_EOL_AT_EOF . "\n";
        }

        return $ret;
    }
}
