<?php declare(strict_types=1);
/*
 * This file is part of sebastian/diff.
 *
 * (c) Sebastian Bergmann <sebastian@phpunit.de>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace SebastianBergmann\Diff\Output;

/**
 * Builds a diff string representation in unified diff format in chunks.
 */
final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder
{
    /**
     * @var string
     */
    private $header;

    /**
     * @var bool
     */
    private $addLineNumbers;

    public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false)
    {
        $this->header         = $header;
        $this->addLineNumbers = $addLineNumbers;
    }

    public function getDiff(array $diff): string
    {
        $buffer = \fopen('php://memory', 'r+b');

        if ('' !== $this->header) {
            \fwrite($buffer, $this->header);
            if ("\n" !== \substr($this->header, -1, 1)) {
                \fwrite($buffer, "\n");
            }
        }

        $this->writeDiffChunked($buffer, $diff, $this->getCommonChunks($diff));

        $diff = \stream_get_contents($buffer, -1, 0);

        \fclose($buffer);

        return $diff;
    }

    // `old` is an array with key => value pairs . Each pair represents a start and end index of `diff`
    // of a list of elements all containing `same` (0) entries.
    private function writeDiffChunked($output, array $diff, array $old)
    {
        $upperLimit = \count($diff);
        $start      = 0;
        $fromStart  = 0;
        $toStart    = 0;

        if (\count($old)) { // no common parts, list all diff entries
            \reset($old);

            // iterate the diff, go from chunk to chunk skipping common chunk of lines between those
            do {
                $commonStart = \key($old);
                $commonEnd   = \current($old);

                if ($commonStart !== $start) {
                    list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $commonStart);
                    $this->writeChunk($output, $diff, $start, $commonStart, $fromStart, $fromRange, $toStart, $toRange);

                    $fromStart += $fromRange;
                    $toStart += $toRange;
                }

                $start        = $commonEnd + 1;
                $commonLength = $commonEnd - $commonStart + 1; // calculate number of non-change lines in the common part
                $fromStart += $commonLength;
                $toStart += $commonLength;
            } while (false !== \next($old));

            \end($old); // short cut for finding possible last `change entry`
            $tmp = \key($old);
            \reset($old);
            if ($old[$tmp] === $upperLimit - 1) {
                $upperLimit = $tmp;
            }
        }

        if ($start < $upperLimit - 1) { // check for trailing (non) diff entries
            do {
                --$upperLimit;
            } while (isset($diff[$upperLimit][1]) && $diff[$upperLimit][1] === 0);
            ++$upperLimit;

            list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $upperLimit);
            $this->writeChunk($output, $diff, $start, $upperLimit, $fromStart, $fromRange, $toStart, $toRange);
        }
    }

    private function writeChunk(
        $output,
        array $diff,
        int $diffStartIndex,
        int $diffEndIndex,
        int $fromStart,
        int $fromRange,
        int $toStart,
        int $toRange
    ) {
        if ($this->addLineNumbers) {
            \fwrite($output, '@@ -' . (1 + $fromStart));

            if ($fromRange > 1) {
                \fwrite($output, ',' . $fromRange);
            }

            \fwrite($output, ' +' . (1 + $toStart));
            if ($toRange > 1) {
                \fwrite($output, ',' . $toRange);
            }

            \fwrite($output, " @@\n");
        } else {
            \fwrite($output, "@@ @@\n");
        }

        for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
            if ($diff[$i][1] === 1 /* ADDED */) {
                \fwrite($output, '+' . $diff[$i][0]);
            } elseif ($diff[$i][1] === 2 /* REMOVED */) {
                \fwrite($output, '-' . $diff[$i][0]);
            } else { /* Not changed (old) 0 or Warning 3 */
                \fwrite($output, ' ' . $diff[$i][0]);
            }

            $lc = \substr($diff[$i][0], -1);
            if ($lc !== "\n" && $lc !== "\r") {
                \fwrite($output, "\n"); // \No newline at end of file
            }
        }
    }

    private function getChunkRange(array $diff, int $diffStartIndex, int $diffEndIndex): array
    {
        $toRange   = 0;
        $fromRange = 0;

        for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
            if ($diff[$i][1] === 1) { // added
                ++$toRange;
            } elseif ($diff[$i][1] === 2) { // removed
                ++$fromRange;
            } elseif ($diff[$i][1] === 0) { // same
                ++$fromRange;
                ++$toRange;
            }
        }

        return [$fromRange, $toRange];
    }
}
