<?php

namespace RocketTheme\Toolbox\File;

use BadMethodCallException;
use RuntimeException;
use function strlen;

/**
 * Implements Gettext Mo File reader (readonly).
 *
 * @package RocketTheme\Toolbox\File
 * @author RocketTheme
 * @license MIT
 */
class MoFile extends File
{
    /** @var string */
    protected $extension = '.mo';
    /** @var int */
    protected $pos = 0;
    /** @var string */
    protected $str;
    /** @var int */
    protected $len;
    /** @var string */
    protected $endian;

    /** @var static[] */
    static protected $instances = [];

    /**
     * @param array|null $var
     * @return array
     */
    public function content($var = null)
    {
        /** @var array $content */
        $content = parent::content($var);

        return $content;
    }

    /**
     * File can never be written.
     *
     * @return bool
     */
    public function writable()
    {
        return false;
    }

    /**
     * Prevent saving file.
     *
     * @param mixed $data
     * @return void
     * @throws BadMethodCallException
     */
    public function save($data = null)
    {
        throw new BadMethodCallException('save() not supported for .mo files.');
    }

    /**
     * Prevent deleting file from filesystem.
     *
     * @return bool
     */
    public function delete()
    {
        return false;
    }

    /**
     * @param string $var
     * @return array
     * @throws RuntimeException
     */
    public function decode($var)
    {
        $this->endian = 'V';
        $this->str = $var;
        $this->len = strlen($var);

        $magic = $this->readInt() & 0xffffffff;

        if ($magic === 0x950412de) {
            // Low endian.
            $this->endian = 'V';
        } elseif ($magic === 0xde120495) {
            // Big endian.
            $this->endian = 'N';
        } else {
            throw new RuntimeException('Not a Gettext file (.mo).');
        }

        // Skip revision number.
        $rev = $this->readInt();
        // Total count.
        $total = $this->readInt();
        // Offset of original table.
        $originals = $this->readInt();
        // Offset of translation table.
        $translations = $this->readInt();

        if ($originals === false || $translations === false) {
            throw new RuntimeException('Bad Gettext file.');
        }

        // Each table consists of string length and offset of the string.
        $this->seek($originals);
        $table_originals = $this->readIntArray($total * 2);
        $this->seek($translations);
        $table_translations = $this->readIntArray($total * 2);

        if ($table_originals === false || $table_translations === false) {
            throw new RuntimeException('Bad Gettext file.');
        }

        $items = [];
        for ($i = 0; $i < $total; $i++) {
            $this->seek($table_originals[$i * 2 + 2]);

            // TODO: Original string can have context concatenated on it. We do not yet support that.
            $original = $this->read($table_originals[$i * 2 + 1]);

            if ($original) {
                $this->seek($table_translations[$i * 2 + 2]);

                // TODO: Plural forms are stored by letting the plural of the original string follow the singular of the original string, separated through a NUL byte.
                $translated = $this->read($table_translations[$i * 2 + 1]);
                $items[$original] = $translated;
            }
        }

        return $items;
    }

    /**
     * @return int|false
     */
    protected function readInt()
    {
        $read = $this->read(4);
        if ($read === false) {
            return false;
        }

        $read = unpack($this->endian, $read);
        if ($read === false) {
            return false;
        }

        return array_shift($read);
    }

    /**
     * @param int $count
     * @return array|false
     */
    protected function readIntArray($count)
    {
        $read = $this->read(4 * $count);

        return is_string($read) ? unpack($this->endian . (string)$count, $read) : false;
    }

    /**
     * @param int $bytes
     * @return string|false
     */
    private function read($bytes)
    {
        $data = substr($this->str, $this->pos, $bytes);
        $this->seek($this->pos + $bytes);

        if (strlen($data) < $bytes) {
            return false;
        }

        return $data;
    }

    /**
     * @param int $pos
     * @return int
     */
    private function seek($pos)
    {
        $this->pos = $pos < $this->len ? $pos : $this->len;

        return $this->pos;
    }
}
