<?php
/**
 * @copyright (c) Kufer Software GmbH, Loisachstrasse 4, D-84453 Mühldorf
 */

namespace Kufer\Web\Classes\Rvf;

use Kufer\Web\Classes\Converter;


class RvfToTextConverter implements Converter\TextConverter, Converter\RvfConverter
{
  protected $rvf;

  protected $idx = -1;
  protected $result = '';

  
  protected $version;

  public function __construct(string $rvf)
  {
    $this->rvf = $rvf;
  }

  public function toRvf(): string
  {
    return $this->rvf;
  }

  protected function init(): void
  {
    $this->idx = -1;
    $this->result = '';
    $this->version = RvfVersionRecord::getDefault();
  }

  public function toText(): string
  {
    if ($this->rvf === '') { 
      return '';
    }

    try {
      $records = $this->parse();
      $result = '';
      $isfirst = true;
      foreach ($records as $record) {
        if (!$record->header->isText()) {
          continue;
        }

        if (!$isfirst) {
          if ($record->header->paragraph !== -1) {
            $result .= "\n";
          }
        } else {
          $isfirst = false;
        }

        for ($i = 0; $i < \count($record->text); ++$i) {
          if ($i > 0 && $i < \count($record->text)) {
            $result .= "\n";
          }
          $result .= $record->text[$i];
        }
      }

      return $result;
    } catch (\Exception $ex) {
      if (DEBUG) {
        throw $ex;
      }

      return '';
    }
  }

  
  protected function parse(): \Generator
  {
    $this->init();
    $this->version = $this->readVersionRecord();

    while (!$this->isEof()) {
      $this->eatSpace();
      if ($this->isEof()) {
        break;
      }
      yield $this->readRecord();
    }
  }

  protected function undoReadNext(): void
  {
    $this->idx--;
    if ($this->idx < -1) { 
      $this->idx = -1;
    }
  }

  protected function undoReadNextNotEof(): void
  {
    if (!$this->isEof()) {
      $this->undoReadNext();
    }
  }

  protected function readCurrent(): string
  {
    if ($this->idx === -1) {
      return '';
    } elseif ($this->idx >= \strlen($this->rvf)) {
      return '';
    }

    return $this->rvf[$this->idx];
  }

  protected function readVersionRecord(): RvfVersionRecord
  {
    if (!\StringOperations::startsWith($this->rvf, '-8', true, 0, 'ASCII')) {
      return RvfVersionRecord::getDefault();
    }

    $this->readRecordInt(); 
    $version = $this->readRecordInt(); 
    $subVersion = $this->readRecordInt(); 
    $subSubVersion = $this->readOptionalRecordInt(0); 
    $this->eatSpace();

    return new RvfVersionRecord($version, $subVersion, $subSubVersion);
  }

  protected function readOptionalRecordInt(int $default): int
  {
    $c = $this->readNext();
    if ($c !== '-' && !$this->isDigit($c)) {
      $this->undoReadNextNotEof();

      return $default;
    }

    $this->undoReadNext();

    return $this->readRecordInt();
  }

  protected function readRecordIntEnd(): void
  {
    $c0 = $this->readNext();
    if ($c0 === "\n") {
      $c1 = $this->readNext();
      if ($c1 === "\r") {
      }
      $this->undoReadNextNotEof();
    } elseif ($c0 === "\r") {
      $c1 = $this->readNext();
      if ($c1 === "\n") {
      }
      $this->undoReadNextNotEof();
    } elseif ($c0 === ' ') {
    } else {
      throw new \Exception("Leerzeichen oder Newline erwartet bei {$this->idx}");
    }
  }

  protected function readWhileNotStartingWithRecord(): void
  {
    while (!$this->isEof() && ($c = $this->readNext()) !== '-' && !$this->isDigit($c)) {
      $this->undoReadNextNotEof();
      $this->readWhileNotCRLF();
      $this->readNewLine();
    }

    $this->undoReadNextNotEof();
  }

  protected function readRecordInt(): int
  {
    $c = $this->readNext();
    if ($c !== '-') {
      if ($this->isDigit($c)) {
        $this->undoReadNext();
        $result = $this->readInt();
        $this->readRecordIntEnd();

        return $result;
      }

      throw new \Exception("- oder Nummer erwartet, nicht '$c' bei {$this->idx}");
    }

    $result = -$this->readInt();
    $this->readRecordIntEnd();

    return $result;
  }

  protected function readRecord(): RvfRecord
  {
    $header = $this->readRecordHeader();
    if (!$header->isText()) {
      foreach ($this->readRecordBody($header) as $record) {
      }

      return new RvfUnknownRecord($header);
    } else {
      $text = \iterator_to_array($this->readRecordBody($header));

      return new RvfTextRecord($header, $text);
    }
  }

  
  protected function readTextItem($header): \Generator
  {
    if ($header->isItemOptionUnicode()) {
      if ($header->query === 3) {
        for ($i = 0; $i < $header->stringsCount; $i++) {
          yield $this->readHexUnicode();
          if (!$this->isEof()) {
            $this->readNewLine();
          }
        }
      } elseif ($header->query === 0) {
        for ($i = 0; $i < $header->stringsCount; $i++) {
          yield $this->readUnicodeUTF16();
        }
      }
    } else {
      for ($i = 0; $i < $header->stringsCount; $i++) {
        yield $this->convertString($this->readWhileNotNewline());
        if (!$this->isEof()) {
          $this->readNewLine();
        }
      }
    }
  }

  protected function skip(int $i): string
  {
    $start = $this->idx;
    $this->idx += $i;

    return \substr($this->rvf, $start, $i);
  }

  protected function readBinary(): string
  {
    $len = $this->readBinaryInt();

    return $this->skip($len);
  }

  
  protected function readTableItem($header): \Generator
  {
    $linestoread = \max(array($header->stringsCount - 1, 0));
    for ($i = 0; $i < $linestoread; ++$i) {
      yield $this->readWhileNotCRLF();
      $this->readNewLine();
    }

    yield $this->readBinary();
  }

  
  protected function readDocProperty($header): \Generator
  {
    $restofheader = $header->queryOptions[0] ?? 0;
    $islinethendata = $restofheader === 1
        || $restofheader === 2
        || $restofheader === 4
        || $restofheader === 10;
    if ($header->query === 2 || $header->query === 3) {
      if ($islinethendata) {
        yield $this->readWhileNotNewline(); 
        $this->readNewLine();
        yield from $this->readData($header);
      }
    } else {
      for ($i = 0; $i < $header->stringsCount; ++$i) {
        yield $this->convertString($this->readWhileNotNewline());
        $this->readNewLine();
      }
    }
  }

  protected function readBinaryInt(): int
  {
    $c0 = $this->readNext();
    $c1 = $this->readNext();
    $c2 = $this->readNext();
    $c3 = $this->readNext();
    $bin = $c3 . $c2 . $c1 . $c0;
    if (\strlen($bin) !== 4) {
      return 0;
    }

    return (int)unpack('N', $bin)[1];
  }

  
  protected function readPictureItem($header): \Generator
  {
    yield $this->readWhileNotNewline(); 
    $this->readNewLine();
    yield $this->readWhileNotNewline(); 
    $this->readNewLine();
    $restparameters = \max(array($header->stringsCount - 3, 0));
    for ($i = 0; $i < $restparameters; $i++) {
      yield $this->readWhileNotNewline();
      $this->readNewLine();
    }
    yield from $this->readData($header);
  }

  
  protected function readBackgroundItem($header): \Generator
  {
    yield $this->readWhileNotNewline(); 
    $this->readNewLine();
    $restparameters = \max(array($header->stringsCount - 2, 0));
    for ($i = 0; $i < $restparameters; $i++) {
      yield $this->readWhileNotNewline();
      $this->readNewLine();
    }
    yield from $this->readData($header);
  }

  
  protected function readData($header)
  {
    if ($header->query === 2) {
      yield $this->readBinary();
    } elseif ($header->query === 3) {
      yield $this->readHexUnicode();
      $this->readNewLine();
    }
  }

  
  protected function readRecordBody($header): \Generator
  {
    if ($header->type >= 0) { 
      yield from $this->readTextItem($header);
    } elseif ($header->type === -9) { 
      yield from $this->readDocProperty($header);
    } elseif ($header->type === -3) {
      yield from $this->readPictureItem($header);
    } elseif ($header->type === -7) {
      yield from $this->readBackgroundItem($header);
    } elseif ($header->type === -60) {
      yield from $this->readTableItem($header);
    } else {
      if ($header->query === 2) {
        
        for ($i = 0; $i < $header->stringsCount - 1; ++$i) {
          yield $this->convertString($this->readWhileNotCRLF());
          $this->readNewLine();
        }

        yield $this->readBinary();
      } elseif ($header->query === 3) {
        
        for ($i = 0; $i < $header->stringsCount - 1; ++$i) {
          yield $this->convertString($this->readWhileNotCRLF());
          $this->readNewLine();
        }

        yield $this->readHexUnicode();
        $this->readNewLine();
      } else {
        for ($i = 0; $i < $header->stringsCount; ++$i) {
          yield $this->convertString($this->readWhileNotCRLF());
          $this->readNewLine();
        }
      }
    }
  }

  protected function isHexDigit($c): bool
  {
    return $this->isDigit($c)
        || ($c >= 'A' && $c <= 'F')
        || ($c >= 'a' && $c <= 'f');
  }

  protected function readHexUnicode(): string
  {
    $result = '';
    while ($this->isHexDigit($c = $this->readNext())) {
      $result .= $c;
    }
    $this->undoReadNextNotEof();

    return \mb_convert_encoding(\hex2bin($result), \CodingOperations::getInternalEncoding(), 'UTF-8');
  }

  protected function readUnicodeUTF16(): string
  {
    $result = '';
    $paragraphEnd = mb_convert_encoding('&#x2029;', 'UTF-16LE', 'HTML-ENTITIES');
    while (!$this->isEof() && (($c = $this->readUtf16()) !== $paragraphEnd)) {
      $result .= $c;
    }

    return \mb_convert_encoding($result, \CodingOperations::getInternalEncoding(), 'UTF-16LE');
  }

  protected function readUtf16(): string
  {
    $c0 = $this->readNext();
    $c1 = $this->readNext();

    return $c0 . $c1;
  }

  protected function readRecordHeader(): RvfHeader
  {
    $type = $this->readRecordInt();
    $stringsCount = $this->readRecordInt();
    $paragraph = $this->readRecordInt();
    $itemOptions = $this->readRecordInt();
    $query = $this->readQuery();
    $tag = $this->readTag();

    $queryOptions = $this->readRestOfHeader($type);
    while ($this->peekNext() === ' ') {
      $this->readNext();
    }
    $this->readNewLine();

    return new RvfHeader($type, $stringsCount, $paragraph, $itemOptions, $query, $tag, $queryOptions);
  }

  protected function readQuery(): int
  {
    $query = $this->readRecordInt();
    if ($query >= 0 && $query <= 3) {
      return $query;
    }

    throw new \Exception("Query erwartet Zahl zwischen 0 und 3, nicht $query");
  }

  protected function readTag(): string
  {
    $c = $this->readNext();
    if ($c === '0') {
      return '';
    } else {
      $this->undoReadNextNotEof();

      return $this->readQuotedString();
    }
  }

  protected function readQuotedString(): string
  {
    $c0 = $this->readNext();
    if ($c0 !== '"') {
      throw new \Exception('" erwartet');
    }

    $result = '';
    while (!$this->isEof()) {
      $c1 = $this->readNext();
      if ($c1 == '"') {
        if ($this->readNext() === '"') {
          $result .= '"';
        } else {
          $this->undoReadNextNotEof();
          $this->undoReadNext();
          break;
        }
      } else {
        $c1 .= $result;
      }
    }

    $c2 = $this->readNext();
    if ($c2 !== '"') {
      throw new \Exception('" erwartet');
    }

    return $this->decodeSpecialString($result);
  }

  protected function decodeSpecialString($str): string
  {
    return $this->convertString($str);
  }

  protected function readRestOfHeader($type): array
  {
    $result = array();

    if ($type === -9) {
      $this->readNext(); 
      $result[] = $this->readInt();
    }

    $this->undoReadNextNotEof();
    $this->readWhileNotCRLF();

    return $result;
  }

  protected function readWhileNotCRLF(): string
  {
    $result = '';
    while (!$this->isEof()) {
      $c0 = $this->readNext();
      if ($c0 === "\r") {
        $c1 = $this->readNext();
        if ($c1 === "\n") {
          $this->undoReadNext();
          $this->undoReadNext();

          return $result;
        } elseif ($c0 !== '') {
          $this->undoReadNext();
          $result .= $c0;
        }
      } else {
        $result .= $c0;
      }
    }

    return $result;
  }

  protected function readWhileNotNewline(): string
  {
    $result = '';
    while (!$this->isEof() && !$this->isNewline($this->readNext())) {
      $result .= $this->readCurrent();
    }

    $this->undoReadNextNotEof();
    if ($this->isEof()) {
      return $result;
    }

    $this->readNewLine();

    return $result;
  }

  protected function isSpace($c): bool
  {
    return $c === ' ' || $this->isNewline($c);
  }

  protected function eatSpace(): bool
  {
    $hasspace = false;
    while ($this->isSpace($this->readNext())) {
      $hasspace = true;
    }
    $this->undoReadNextNotEof();

    return $hasspace;
  }

  
  protected function readNext(): string
  {
    ++$this->idx;
    if ($this->idx >= \strlen($this->rvf)) {
      return '';
    }

    return $this->rvf[$this->idx];
  }

  
  protected function peekNext(): string
  {
    if ($this->idx + 1 >= \strlen($this->rvf)) {
      return '';
    }

    return $this->rvf[$this->idx + 1];
  }

  protected function isDigit($c): bool
  {
    return $c >= '0' && $c <= '9';
  }

  protected function isEof(): bool
  {
    return $this->idx >= \strlen($this->rvf);
  }

  protected function isNewline($c): bool
  {
    return $c == "\n" || $c == "\r" || $c == "\r\n";
  }

  protected function readNewLine(): bool
  {
    $c0 = $this->readNext();
    if ($this->isNewline($c0)) {
      $c1 = $this->readNext();
      if ($this->isNewline($c1) && $c0 !== $c1) {
        return true;
      }
      $this->undoReadNextNotEof();

      return true;
    }

    $this->undoReadNextNotEof();

    return false;
  }

  protected function readInt(): int
  {
    $result = '';
    while (!$this->isEof() && $this->isDigit($this->readNext())) {
      $result .= $this->readCurrent();
    }
    $this->undoReadNextNotEof();

    return $result;
  }

  protected function convertString($str): string
  {
    if ($this->version->isUtf8String()) {
      return \StringOperations::utf8_decode($str);
    } else {
      return $str;
    }
  }
}