<?php

namespace monoformat {

include_once("monoformat_bithelpers.php");
include_once("monoformat_schema.php");

function serializePixels(array $pixels): string {
    $n = count($pixels);
    $nBytes = intdiv($n + 7, 8);
    $result = "";
    for ($i = 0; $i < $nBytes; ++$i) {
        $byte = 0;
        for ($j = 0; $j < 8; ++$j) {
            $k = $i * 8 + $j;
            if ($k >= $n) {
                break;
            }
            maybeSetBit($byte, $j, $pixels[$k]);
        }
        $result .= chr($byte);
    }
    return $result;
}

function unserializePixels(int $n, string $serialized): array {
    $nBytes = intdiv($n + 7, 8);
    if (strlen($serialized) != $nBytes) {
        $actual = strlen($serialized);
        throw new \ValueError("Could not unserialize a pixel bitmap: the size $actual does not equal the expected $nBytes");
    }
    $result = array_fill(0, $n, false);
    for ($i = 0; $i < $nBytes; ++$i) {
        $byte = ord($serialized[$i]);
        for ($j = 0; $j < 8; ++$j) {
            $k = $i * 8 + $j;
            if ($k >= $n) {
                break;
            }
            $result[$k] = isBitSet($byte, $j);
        }
    }
    return $result;
}

function packU24LE(int $value) {
    return substr(pack("V", $value), 0, 3);
}

function unpackU24LE(string $parts) {
    return array_values(unpack("V", $parts . "\x00"))[0];
}

function alignSerialized(string $data, int $alignment) {
    $n = strlen($data);
    if ($n % $alignment) {
        $target = intdiv($n + $alignment - 1, $alignment) * $alignment;
        $remaining = $target - $n;
        $data .= str_repeat("\x00", $remaining);
    }
    return $data;
}

abstract class Section {
    abstract public function sectionType(): SectionType;
    abstract public function minimumFormatVersion(): int;
    abstract public function serializeBinary(int $formatVersion): string;
    abstract public function serializeJSON(int $formatVersion): array;
}

abstract class Element {
    abstract public function elementType(): ElementType;
    abstract public function minimumFormatVersion(): int;
    abstract public function serializeBinary(int $formatVersion): string;
    abstract public function serializeJSON(int $formatVersion): array;
}

class ImageElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private array $m_image = [];

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function image(): array {
        return $this->m_image;
    }

    public function setImage(array $image) {
        $n = $this->m_width * $this->m_height;
        if (count($image) != $n) {
            $actual = count($image);
            throw new \ValueError("Cannot update the image of an ImageElement: the number of pixels supplied, $actual, is not the expected number, $n");
        }
        $this->m_image = $image;
    }

    public function __construct(int $x, int $y, int $width, int $height) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $n = $width * $height;
        $this->m_image = array_fill(0, $n, false);
    }

    public function elementType(): ElementType {
        return ElementType::Image;
    }

    public function minimumFormatVersion(): int {
        return 1;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvv",
                       ElementType::Image->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height,
                       0);
        $result .= serializePixels($this->m_image);
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::Image->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        $result["image"] = $this->m_image;
    }

    public static function parseBinary(string &$data, int $formatVersion): ImageElement {
        [$type, $x, $y, $width, $height, $reserved] = array_values(unpack("v6", substr($data, 0, 12)));
        $type = ElementType::from($type);
        if ($type != ElementType::Image) {
            throw new \ValueError("Could not unserialize an image element: type mismatch");
        }
        $n = $width * $height;
        $nBytes = intdiv($n + 7, 8);
        $nAligned = intdiv($nBytes + 3, 4) * 4;
        $pixels = unserializePixels($width * $height, substr($data, 12, $nBytes));
        $result = new ImageElement($x, $y, $width, $height);
        $result->setImage($pixels);
        $data = substr($data, 12 + $nAligned);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): ImageElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::Image) {
            throw new \ValueError("Could not unserialize an image element: type mismatch");
        }
        $result = new ImageElement($element["x"], $element["y"], $element["width"], $element["height"]);
        $result->setImage($element["image"]);
        return $result;
    }
}

class AnimationElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private int $m_updateInterval = 0;
    private array $m_frames = [];

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function numberOfFrames(): int {
        return count($this->m_frames);
    }

    public function updateInterval(): int {
        return $this->m_updateInterval;
    }

    public function image(int $n): array {
        if ($n >= count($this->m_frames)) {
            $max = count($this->m_frames) - 1;
            throw new \ValueError("Cannot retrieve a frame of an AnimationElement: the frame index $n is not in the range [0..$max]");
        }
        return $this->m_frames[$n];
    }

    public function setImage(int $n, array $image) {
        if ($n >= count($this->m_frames)) {
            $max = count($this->m_frames) - 1;
            throw new \ValueError("Cannot retrieve a frame of an AnimationElement: the frame index $n is not in the range [0..$max]");
        }
        $nPixels = $this->m_width * $this->m_height;
        if (count($image) != $nPixels) {
            $actual = count($image);
            throw new \ValueError("Cannot update a frame of an AnimationElement: the number of pixels supplied, $actual, is not the expected number, $nPixels");
        }
        $this->m_frames[$n] = $image;
    }

    public function __construct(int $x, int $y, int $width, int $height, int $numberOfFrames, int $updateInterval) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_updateInterval = $updateInterval;
        $n = $width * $height;
        for ($i = 0; $i < $numberOfFrames; ++$i) {
            array_push($this->m_frames, array_fill(0, $n, false));
        }
    }

    public function elementType(): ElementType {
        return ElementType::Animation;
    }

    public function minimumFormatVersion(): int {
        return 1;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvvvv",
                       ElementType::Animation->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height,
                       count($this->m_frames),
                       $this->m_updateInterval,
                       0);
        foreach ($this->m_frames as $frame) {
            $result .= serializePixels($frame);
        }
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::Animation->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        $result["updateInterval"] = $this->m_updateInterval;
        $result["frames"] = $this->m_frames;
    }

    public static function parseBinary(string &$data, int $formatVersion): AnimationElement {
        [$type, $x, $y, $width, $height, $nFrames, $updateInterval, $reserved] = array_values(unpack("v8", substr($data, 0, 16)));
        $type = ElementType::from($type);
        if ($type != ElementType::Animation) {
            throw new \ValueError("Could not unserialize an animation element: type mismatch");
        }
        $n = $width * $height;
        $nBytes = intdiv($n + 7, 8);
        $nAligned = intdiv($nBytes * $nFrames + 3, 4) * 4;
        $frames = [];
        $result = new AnimationElement($x, $y, $width, $height, $nFrames, $updateInterval);
        for ($i = 0; $i < $nFrames; ++$i) {
            $pixels = unserializePixels($width * $height, substr($data, 16 + $i * $nBytes, $nBytes));
            $result->setImage($i, $pixels);
        }
        $data = substr($data, 16 + $nAligned);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): AnimationElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::Animation) {
            throw new \ValueError("Could not unserialize an animation element: type mismatch");
        }
        $result = new AnimationElement($element["x"], $element["y"], $element["width"], $element["height"], count($element["frames"]), $element["updateInterval"]);
        for ($i = 0; $i < count($element["frames"]); ++$i) {
            $result->setImage($i, $element["frames"][$i]);
        }
        return $result;
    }
}

class HScrollImageElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private int $m_contentWidth = 0;
    private ScrollFlags $m_flags;
    private int $m_scrollSpeed = 0;
    private array $m_image = [];

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function contentWidth(): int {
        return $this->m_contentWidth;
    }

    public function flags(): ScrollFlags {
        return $this->m_flags;
    }

    public function scrollSpeed(): int {
        return $this->m_scrollSpeed;
    }

    public function image(): array {
        return $this->m_image;
    }

    public function setImage(array $image) {
        $n = $this->m_contentWidth * $this->m_height;
        if (count($image) != $n) {
            $actual = count($image);
            throw new \ValueError("Cannot update the image of an HScrollImageElement: the number of pixels supplied, $actual, is not the expected number, $n");
        }
        $this->m_image = $image;
    }

    public function __construct(int $x, int $y, int $width, int $height, int $contentWidth, ScrollFlags $flags, int $scrollSpeed) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_contentWidth = $contentWidth;
        $this->m_flags = $flags;
        $this->m_scrollSpeed = $scrollSpeed;
        $n = $contentWidth * $height;
        $this->m_image = array_fill(0, $n, false);
    }

    public function elementType(): ElementType {
        return ElementType::HScrollImage;
    }

    public function minimumFormatVersion(): int {
        return 1;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvvCCv",
                       ElementType::HScrollImage->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height,
                       $this->m_contentWidth,
                       $this->m_flags->toBitField(),
                       $this->m_scrollSpeed,
                       0);
        $result .= serializePixels($this->m_image);
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::HScrollImage->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        $result["contentWidth"] = $this->m_contentWidth;
        $result["flags"] = $this->m_flags->toJSON();
        $result["scrollSpeed"] = $this->m_scrollSpeed;
        $result["image"] = $this->m_image;
    }

    public static function parseBinary(string &$data, int $formatVersion): HScrollImageElement {
        [$type, $x, $y, $width, $height, $contentWidth] = array_values(unpack("v6", substr($data, 0, 12)));
        $type = ElementType::from($type);
        if ($type != ElementType::HScrollImage) {
            throw new \ValueError("Could not unserialize a horizontally scrolling image element: type mismatch");
        }
        [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 12, 2)));
        $flags = TextFlags::fromBitField($flags);
        $n = $contentWidth * $height;
        $nBytes = intdiv($n + 7, 8);
        $nAligned = intdiv($nBytes + 3, 4) * 4;
        $pixels = unserializePixels($n, substr($data, 16, $nBytes));
        $result = new HScrollImageElement($x, $y, $width, $height, $contentWidth, $flags, $scrollSpeed);
        $result->setImage($pixels);
        $data = substr($data, 16 + $nAligned);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): HScrollImageElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::HScrollImage) {
            throw new \ValueError("Could not unserialize a horizontally scrolling image element: type mismatch");
        }
        $result = new HScrollImageElement($element["x"], $element["y"], $element["width"], $element["height"], $element["contentWidth"],
                                          ScrollFlags::fromJSON($element["flags"]), $element["scrollSpeed"]);
        $result->setImage($element["image"]);
        return $result;
    }
}

class VScrollImageElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private int $m_contentHeight = 0;
    private ScrollFlags $m_flags;
    private int $m_scrollSpeed = 0;
    private array $m_image = [];

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function contentHeight(): int {
        return $this->m_contentHeight;
    }

    public function flags(): ScrollFlags {
        return $this->m_flags;
    }

    public function scrollSpeed(): int {
        return $this->m_scrollSpeed;
    }

    public function image(): array {
        return $this->m_image;
    }

    public function setImage(array $image) {
        $n = $this->m_width * $this->m_contentHeight;
        if (count($image) != $n) {
            $actual = count($image);
            throw new \ValueError("Cannot update the image of a VScrollImageElement: the number of pixels supplied, $actual, is not the expected number, $n");
        }
        $this->m_image = $image;
    }

    public function __construct(int $x, int $y, int $width, int $height, int $contentHeight, ScrollFlags $flags, int $scrollSpeed) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_contentHeight = $contentHeight;
        $this->m_flags = $flags;
        $this->m_scrollSpeed = $scrollSpeed;
        $n = $width * $contentHeight;
        $this->m_image = array_fill(0, $n, false);
    }

    public function elementType(): ElementType {
        return ElementType::VScrollImage;
    }

    public function minimumFormatVersion(): int {
        return 1;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvvCCv",
                       ElementType::VScrollImage->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height,
                       $this->m_contentHeight,
                       $this->m_flags->toBitField(),
                       $this->m_scrollSpeed,
                       0);
        $result .= serializePixels($this->m_image);
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::HScrollImage->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        $result["contentHeight"] = $this->m_contentHeight;
        $result["flags"] = $this->m_flags->toJSON();
        $result["scrollSpeed"] = $this->m_scrollSpeed;
        $result["image"] = $this->m_image;
    }

    public static function parseBinary(string &$data, int $formatVersion): VScrollImageElement {
        [$type, $x, $y, $width, $height, $contentHeight] = array_values(unpack("v6", substr($data, 0, 12)));
        $type = ElementType::from($type);
        if ($type != ElementType::VScrollImage) {
            throw new \ValueError("Could not unserialize a vertically scrolling image element: type mismatch");
        }
        [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 12, 2)));
        $flags = TextFlags::fromBitField($flags);
        $n = $width * $contentHeight;
        $nBytes = intdiv($n + 7, 8);
        $nAligned = intdiv($nBytes + 3, 4) * 4;
        $pixels = unserializePixels($n, substr($data, 16, $nBytes));
        $result = new VScrollImageElement($x, $y, $width, $height, $contentHeight, $flags, $scrollSpeed);
        $result->setImage($pixels);
        $data = substr($data, 16 + $nAligned);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): VScrollImageElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::VScrollImage) {
            throw new \ValueError("Could not unserialize a vertically scrolling image element: type mismatch");
        }
        $result = new VScrollImageElement($element["x"], $element["y"], $element["width"], $element["height"], $element["contentHeight"],
                                          ScrollFlags::fromJSON($element["flags"]), $element["scrollSpeed"]);
        $result->setImage($element["image"]);
        return $result;
    }
}

class LineElement extends Element {
    private int $m_originX = 0;
    private int $m_orignY = 0;
    private int $m_targetX = 0;
    private int $m_targetY = 0;
    private LineStyle $m_lineStyle = LineStyle::Solid;
    private LineFlags $m_flags;

    public function originX(): int {
        return $this->m_originX;
    }

    public function originY(): int {
        return $this->m_originY;
    }

    public function targetX(): int {
        return $this->m_targetX;
    }

    public function targetY(): int {
        return $this->m_targetY;
    }

    public function lineStyle(): LineStyle {
        return $this->m_lineStyle;
    }

    public function flags(): LineFlags {
        return $this->m_flags;
    }

    public function __construct(int $originX, int $originY, int $targetX, int $targetY, LineStyle $lineStyle, LineFlags $flags) {
        $this->m_originX = $originX;
        $this->m_originY = $originY;
        $this->m_targetX = $targetX;
        $this->m_targetY = $targetY;
        $this->m_lineStyle = $lineStyle;
        $this->m_flags = $flags;
    }

    public function elementType(): ElementType {
        return ElementType::Line;
    }

    public function minimumFormatVersion(): int {
        if ($this->m_flags->toBitField() != 0) {
            return 2;
        } else {
            return 1;
        }
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvCC",
                       ElementType::Line->value,
                       $this->m_originX,
                       $this->m_originY,
                       $this->m_targetX,
                       $this->m_targetY,
                       $this->m_lineStyle->value,
                       $this->m_flags->toBitField());
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::Line->name;
        $result["originX"] = $this->m_originX;
        $result["originY"] = $this->m_originY;
        $result["targetX"] = $this->m_targetX;
        $result["targetY"] = $this->m_targetY;
        $result["lineStyle"] = $this->m_lineStyle->toJSON();
        $result["flags"] = $this->m_flags->toJSON();
        return $result;
    }

    public static function parseBinary(string &$data, int $formatVersion): LineElement {
        [$type, $originX, $originY, $targetX, $targetY] = array_values(unpack("v5", substr($data, 0, 10)));
        $data = substr($data, 10);
        $pos = 10;
        $type = ElementType::from($type);
        if ($type != ElementType::Line) {
            throw new \ValueError("Could not unserialize a line element: type mismatch");
        }
        [$lineStyle, $flags] = array_values(unpack("C2", substr($data, 0, 2)));
        $data = substr($data, 2);
        $pos += 2;
        $lineStyle = LineStyle::from($lineStyle);
        $flags = LineFlags::fromBitField($flags);
        $alignedPos = intdiv($pos + 3, 4) * 4;
        $data = substr($data, $alignedPos - $pos);

        $result = new LineElement($originX, $originY, $targetX, $targetY, $lineStyle, $flags);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): LineElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::Line) {
            throw new \ValueError("Could not unserialize a line element: type mismatch");
        }
        $flags = new LineFlags();
        $lineStyle = LineStyle::fromName($element["lineStyle"]);
        if ($formatVersion >= 2) {
            $flags = LineFlags::fromJSON($element["flags"]);
        }
        $result = new LineElement($element["originX"], $element["originY"], $element["targetX"], $element["targetY"], $lineStyle, $flags);
        return $result;
    }
}

class BoxElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private FillPattern $m_fillPattern = FillPattern::Solid;
    private FillFlags $m_flags;

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function fillPattern(): FillPattern {
        return $this->m_fillPattern;
    }

    public function flags(): FillFlags {
        return $this->m_flags;
    }

    public function __construct(int $x, int $y, int $width, int $height, FillPattern $fillPattern, FillFlags $flags) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_fillPattern = $fillPattern;
        $this->m_flags = $flags;
    }

    public function elementType(): ElementType {
        return ElementType::Box;
    }

    public function minimumFormatVersion(): int {
        return 2;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvvCC",
                       ElementType::Box->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height,
                       $this->m_fillPattern->value,
                       $this->m_flags->toBitField());
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::Box->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        $result["fillPattern"] = $this->m_fillPattern->toJSON();
        $result["flags"] = $this->m_flags->toJSON();
        return $result;
    }

    public static function parseBinary(string &$data, int $formatVersion): BoxElement {
        [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10)));
        $data = substr($data, 10);
        $pos = 10;
        $type = ElementType::from($type);
        if ($type != ElementType::Box) {
            throw new \ValueError("Could not unserialize a box element: type mismatch");
        }
        [$fillPattern, $flags] = array_values(unpack("C2", substr($data, 0, 2)));
        $data = substr($data, 2);
        $pos += 2;
        $fillPattern = FillPattern::from($fillPattern);
        $flags = FillFlags::fromBitField($flags);

        $alignedPos = intdiv($pos + $len + 3, 4) * 4;
        $data = substr($data, $alignedPos - $pos);

        $result = new BoxElement($x, $y, $width, $height, $fillPattern, $flags);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): BoxElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::Box) {
            throw new \ValueError("Could not unserialize a box element: type mismatch");
        }
        $fillPattern = FillPattern::fromName($element["fillPattern"]);
        $flags = FillFlags::fromJSON($element["flags"]);
        $result = new BoxElement($element["x"], $element["y"], $element["width"], $element["height"], $fillPattern, $flags);
        return $result;
    }
}

class ClippedTextElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private TextFlags $m_textFlags;
    private int $m_fontIndex = 0;
    private string $m_text = "";

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function textFlags(): TextFlags {
        return $this->m_textFlags;
    }

    public function fontIndex(): int {
        return $this->m_fontIndex;
    }

    public function text(): string {
        return $this->m_text;
    }

    public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, int $fontIndex, string $text) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_textFlags = $textFlags;
        $this->m_fontIndex = $fontIndex;
        $this->m_text = $text;
    }

    public function elementType(): ElementType {
        return ElementType::ClippedText;
    }

    public function minimumFormatVersion(): int {
        if ($this->m_textFlags->toBitField() != 0) {
            return 2;
        } else {
            return 1;
        }
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvv",
                       ElementType::ClippedText->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height);
        if ($formatVersion >= 2) {
            $result .= pack("CC",
                            $this->m_textFlags->toBitField(),
                            0);
        }
        $result .= pack("vv",
                        $this->m_fontIndex,
                        strlen($this->m_text));
        $result .= $this->m_text;
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::ClippedText->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        if ($formatVersion >= 2) {
            $result["textFlags"] = $this->m_textFlags->toJSON();
        }
        $result["fontIndex"] = $this->m_fontIndex;
        $result["text"] = $this->m_text;
        return $result;
    }

    public static function parseBinary(string &$data, int $formatVersion): ClippedTextElement {
        [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10)));
        $data = substr($data, 10);
        $pos = 10;
        $type = ElementType::from($type);
        if ($type != ElementType::ClippedText) {
            throw new \ValueError("Could not unserialize a clipped text element: type mismatch");
        }
        $textFlags = 0;
        if ($formatVersion >= 2) {
            [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2)));
            $data = substr($data, 2);
            $pos += 2;
        }
        $textFlags = TextFlags::fromBitField($textFlags);
        [$fontIndex, $len] = array_values(unpack("v2", substr($data, 0, 4)));
        $data = substr($data, 4);
        $text = substr($data, 0, $len);
        $pos += 4;
        $alignedPos = intdiv($pos + $len + 3, 4) * 4;
        $data = substr($data, $alignedPos - $pos);

        $result = new ClippedTextElement($x, $y, $width, $height, $textFlags, $fontIndex, $text);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): ClippedTextElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::ClippedText) {
            throw new \ValueError("Could not unserialize a clipped text element: type mismatch");
        }
        $textFlags = new TextFlags();
        if ($formatVersion >= 2) {
            $textFlags = TextFlags::fromJSON($element["textFlags"]);
        }
        $result = new ClippedTextElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $element["fontIndex"], $element["text"]);
        return $result;
    }
}

class HScrollTextElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private TextFlags $m_textFlags;
    private ScrollFlags $m_scrollFlags;
    private int $m_scrollSpeed = 0;
    private int $m_fontIndex = 0;
    private string $m_text = "";

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function textFlags(): TextFlags {
        return $this->m_textFlags;
    }

    public function flags(): ScrollFlags {
        return $this->m_flags;
    }

    public function scrollSpeed(): int {
        return $this->m_scrollSpeed;
    }

    public function fontIndex(): int {
        return $this->m_fontIndex;
    }

    public function text(): string {
        return $this->m_text;
    }

    public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, ScrollFlags $flags, int $scrollSpeed, int $fontIndex, string $text) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_textFlags = $textFlags;
        $this->m_flags = $flags;
        $this->m_scrollSpeed = $scrollSpeed;
        $this->m_fontIndex = $fontIndex;
        $this->m_text = $text;
    }

    public function elementType(): ElementType {
        return ElementType::HScrollText;
    }

    public function minimumFormatVersion(): int {
        if ($this->m_textFlags->toBitField() != 0) {
            return 2;
        } else {
            return 1;
        }
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvv",
                       ElementType::HScrollText->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height);
        if ($formatVersion >= 2) {
            $result .= pack("CC",
                            $this->m_textFlags->toBitField(),
                            0);
        }
        $result .= pack("CC",
                        $this->m_flags->toBitField(),
                        $this->m_scrollSpeed);
        if ($formatVersion >= 2) {
            $result .= pack("v", 0);
        }
        $result .= pack("vv",
                        $this->m_fontIndex,
                        strlen($this->m_text));
        $result .= $this->m_text;
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::HScrollText->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        if ($formatVersion >= 2) {
            $result["textFlags"] = $this->m_textFlags->toJSON();
        }
        $result["flags"] = $this->m_flags;
        $result["scrollSpeed"] = $this->m_scrollSpeed;
        $result["fontIndex"] = $this->m_fontIndex;
        $result["text"] = $this->m_text;
        return $result;
    }

    public static function parseBinary(string &$data, int $formatVersion): HScrollTextElement {
        [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10)));
        $data = substr($data, 10);
        $pos = 10;
        $type = ElementType::from($type);
        if ($type != ElementType::HScrollText) {
            throw new \ValueError("Could not unserialize a clipped text element: type mismatch");
        }
        $textFlags = 0;
        if ($formatVersion >= 2) {
            [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2)));
            $data = substr($data, 2);
            $pos += 2;
        }
        $textFlags = TextFlags::fromBitField($textFlags);
        [$flags, $scrollSpeed] = array_values(unpack("C2", substr($data, 0, 2)));
        $flags = ScrollFlags::fromBitField($flags);
        $data = substr($data, 2);
        $pos += 2;
        if ($formatVersion >= 2) {
            $data = substr($data, 2);
            $pos += 2;
        }
        [$fontIndex, $len] = array_values(unpack("v2", substr($data, 0, 4)));
        $data = substr($data, 4);
        $text = substr($data, 0, $len);
        $pos += 4;
        $alignedPos = intdiv($pos + $len + 3, 4) * 4;
        $data = substr($data, $alignedPos - $pos);

        $result = new HScrollTextElement($x, $y, $width, $height, $textFlags, $flags, $scrollSpeed, $fontIndex, $text);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): HScrollTextElement {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::HScrollText) {
            throw new \ValueError("Could not unserialize a horizontally scrolling text element: type mismatch");
        }
        $textFlags = new TextFlags();
        if ($formatVersion >= 2) {
            $textFlags = TextFlags::fromJSON($element["textFlags"]);
        }
        $flags = ScrollFlags::fromJSON($element["flags"]);
        $result = new HScrollTextElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $flags, $element["scrollSpeed"], $element["fontIndex"], $element["text"]);
        return $result;
    }
}

class CurrentTimeElement extends Element {
    private int $m_x = 0;
    private int $m_y = 0;
    private int $m_width = 0;
    private int $m_height = 0;
    private TextFlags $m_textFlags;
    private int $m_fontIndex = 0;
    private int $m_utcOffset = 0;
    private TimeDisplayFlags $m_flags;

    public function x(): int {
        return $this->m_x;
    }

    public function y(): int {
        return $this->m_y;
    }

    public function width(): int {
        return $this->m_width;
    }

    public function height(): int {
        return $this->m_height;
    }

    public function textFlags(): TextFlags {
        return $this->m_textFlags;
    }

    public function fontIndex(): int {
        return $this->m_fontIndex;
    }

    public function utcOffset(): int {
        return $this->m_utcOffset;
    }

    public function flags(): TimeDisplayFlags {
        return $this->m_flags;
    }

    public function __construct(int $x, int $y, int $width, int $height, TextFlags $textFlags, int $fontIndex, int $utcOffset, TimeDisplayFlags $flags) {
        $this->m_x = $x;
        $this->m_y = $y;
        $this->m_width = $width;
        $this->m_height = $height;
        $this->m_textFlags = $textFlags;
        $this->m_fontIndex = $fontIndex;
        $this->m_utcOffset = $utcOffset;
        $this->m_flags = $flags;
    }

    public function elementType(): ElementType {
        return ElementType::CurrentTime;
    }

    public function minimumFormatVersion(): int {
        if ($this->m_textFlags->toBitField() != 0) {
            return 2;
        } else {
            return 1;
        }
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("vvvvv",
                       ElementType::CurrentTime->value,
                       $this->m_x,
                       $this->m_y,
                       $this->m_width,
                       $this->m_height);
        if ($formatVersion >= 2) {
            $result .= pack("CC",
                            $this->m_textFlags->toBitField(),
                            0);
        }
        $result .= pack("vvv",
                        $this->m_fontIndex,
                        $this->m_utcOffset,
                        $this->m_flags->toBitField());
        if ($formatVersion >= 2) {
            $result .= pack("v", 0);
        }
        return alignSerialized($result, 4);
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = ElementType::CurrentTime->name;
        $result["x"] = $this->m_x;
        $result["y"] = $this->m_y;
        $result["width"] = $this->m_width;
        $result["height"] = $this->m_height;
        if ($formatVersion >= 2) {
            $result["textFlags"] = $this->m_textFlags->toJSON();
        }
        $result["fontIndex"] = $this->m_fontIndex;
        $result["utcOffset"] = $this->m_utcOffset;
        $result["flags"] = $this->m_flags->toJSON();
        return $result;
    }

    public static function parseBinary(string &$data, int $formatVersion): CurrentTimeElement {
        [$type, $x, $y, $width, $height] = array_values(unpack("v5", substr($data, 0, 10)));
        $data = substr($data, 10);
        $pos = 10;
        $type = ElementType::from($type);
        if ($type != ElementType::CurrentTime) {
            throw new \ValueError("Could not unserialize a current time element: type mismatch");
        }
        $textFlags = 0;
        if ($formatVersion >= 2) {
            [$textFlags, $reserved] = array_values(unpack("C2", substr($data, 0, 2)));
            $data = substr($data, 2);
            $pos += 2;
        }
        $textFlags = TextFlags::fromBitField($textFlags);
        [$fontIndex, $utcOffset, $flags] = array_values(unpack("v3", substr($data, 0, 6)));
        $flags = TimeDisplayFlags::fromBitField($flags);
        $data = substr($data, 6);
        $pos += 6;
        if ($formatVersion >= 2) {
            $data = substr($data, 2);
            $pos += 2;
        }
        $alignedPos = intdiv($pos + 3, 4) * 4;
        $data = substr($data, $alignedPos - $pos);

        $result = new CurrentTimeElement($x, $y, $width, $height, $textFlags, $fontIndex, $utcOffset, $flags);
        return $result;
    }

    public static function parseJSON(array $element, int $formatVersion): CurrentTime {
        $type = ElementType::fromString($element["type"]);
        if ($type != ElementType::CurrentTime) {
            throw new \ValueError("Could not unserialize a current time element: type mismatch");
        }
        $textFlags = new TextFlags();
        if ($formatVersion >= 2) {
            $textFlags = TextFlags::fromJSON($element["textFlags"]);
        }
        $flags = TimeDisplayFlags::fromJSON($element["flags"]);
        $result = new CurrentTimeElement($element["x"], $element["y"], $element["width"], $element["height"], $textFlags, $element["fontIndex"], $element["utcOffset"], $flags);
        return $result;
    }
}

abstract class ElementContainingSection extends Section {
    protected array $m_elements = [];

    public function elementCount(): int {
        return count($this->m_elements);
    }

    public function elementAt(int $i) {
        if ($i >= count($this->m_elements)) {
            $max = count($this->m_elements) - 1;
            throw new \IndexError("Index $i out of range [0 ..$max]");
        }
        return $this->m_elements[$i];
    }

    public function appendElement(Element $element) {
        array_push($this->m_elements, $element);
    }

    public function insertElement(int $index, Element $element) {
        if ($index < count($this->m_elements)) {
            array_splice($this->m_elements, $index, 0, $element);
        } else {
            array_push($this->m_elements, $element);
        }
    }

    public function replaceElement(int $index, Element $element): Element {
        $result = null;
        if ($index < count($this->m_elements)) {
            $result = $this->m_elements[$index];
            $this->m_elements[$index] = $element;
        }
        return $result;
    }

    public function eraseElement(int $index): Element {
        $result = null;
        if ($index < count($this->m_elements)) {
            $result = $this->m_elements[$index];
            array_splice($this->m_elements, $index, 1);
        }
        return $result;
    }

    protected static function parseElementsBinary(string &$elementData, int $nElements, int $formatVersion): array {
        $elements = [];
        for ($i = 0; $i < $nElements; ++$i) {
            $type = ElementType::from(unpack("v", substr($elementData, 0, 2))[1]);
            $element = null;
            switch ($type) {
            case ElementType::Image:          $element = ImageElement::parseBinary($elementData, $formatVersion);          break;
            case ElementType::Animation:      $element = AnimationElement::parseBinary($elementData, $formatVersion);      break;
            case ElementType::HScrollImage:   $element = HScrollImageElement::parseBinary($elementData, $formatVersion);   break;
            case ElementType::VScrollImage:   $element = VScrollImageElement::parseBinary($elementData, $formatVersion);   break;
            case ElementType::Line:           $element = LineElement::parseBinary($elementData, $formatVersion);           break;
            case ElementType::Box:            $element = BoxElement::parseBinary($elementData, $formatVersion);            break;
            case ElementType::ClippedText:    $element = ClippedTextElement::parseBinary($elementData, $formatVersion);    break;
            case ElementType::HScrollText:    $element = HScrollTextElement::parseBinary($elementData, $formatVersion);    break;
            case ElementType::CurrentTime:    $element = CurrenttimeElement::parseBinary($elementData, $formatVersion);    break;
            default: throw new \ValueError("Unsupported element type " . $type->name . " encountered in file");
            }
            array_push($elements, $element);
        }
        return $elements;
    }

    protected static function parseElementsJSON(array $elementData, int $formatVersion): array {
        $elements = [];
        foreach ($elementData as $serializedElement) {
            $type = ElementType::fromName($serializedElement["type"]);
            $element = null;
            switch ($type) {
            case ElementType::Image:          $element = ImageElement::parseJSON($serializedElement, $formatVersion);          break;
            case ElementType::Animation:      $element = AnimationElement::parseJSON($serializedElement, $formatVersion);      break;
            case ElementType::HScrollImage:   $element = HScrollImageElement::parseJSON($serializedElement, $formatVersion);   break;
            case ElementType::VScrollImage:   $element = VScrollImageElement::parseJSON($serializedElement, $formatVersion);   break;
            case ElementType::Line:           $element = LineElement::parseJSON($serializedElement, $formatVersion);           break;
            case ElementType::Box:            $element = BoxElement::parseJSON($serializedElement, $formatVersion);            break;
            case ElementType::ClippedText:    $element = ClippedTextElement::parseJSON($serializedElement, $formatVersion);    break;
            case ElementType::HScrollText:    $element = HScrollTextElement::parseJSON($serializedElement, $formatVersion);    break;
            case ElementType::CurrentTime:    $element = CurrenttimeElement::parseJSON($serializedElement, $formatVersion);    break;
            default: throw new \ValueError("Unsupported element type " . $type->name . " encountered in file");
            }
            array_push($elements, $element);
        }
        return $elements;
    }
}

class AlwaysDrawnSection extends ElementContainingSection {
    private bool $m_drawOnFront = true;
    private bool $m_drawOnBack = true;
    private bool $m_clearBeforeDrawing = true;

    public function doesDrawOnFront(): bool {
        return $this->m_drawOnFront;
    }

    public function doesDrawOnBack(): bool {
        return $this->m_drawOnBack;
    }

    public function doesClearBeforeDrawing(): bool {
        return $this->m_clearBeforeDrawing;
    }

    public function setDrawOnFront(bool $value) {
        $this->m_drawOnFront = $value;
    }

    public function setDrawOnBack(bool $value) {
        $this->m_drawOnBack = $value;
    }

    public function setClearBeforeDrawing(bool $value) {
        $this->m_clearBeforeDrawing = $value;
    }

    public function sectionType(): SectionType {
        return SectionType::AlwaysDrawn;
    }

    public function minimumFormatVersion(): int {
        $result = 1;
        foreach ($this->m_elements as $element) {
            $result = max($result, $element->minimumFormatVersion());
        }
        return $result;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("C", SectionType::AlwaysDrawn->value);
        // Will be replaced later
        $result .= packU24LE(0);
        $flags = 0;
        maybeSetBit($flags, 0, $this->m_drawOnFront);
        maybeSetBit($flags, 1, $this->m_drawOnBack);
        maybeSetBit($flags, 2, $this->m_clearBeforeDrawing);
        $result .= pack("vv", $flags, count($this->m_elements));
        foreach ($this->m_elements as $element) {
            $result .= $element->serializeBinary($formatVersion);
        }
        $result = alignSerialized($result, 4);
        $len = strlen($result);
        $len = packU24LE($len);
        $result[1] = $len[0];
        $result[2] = $len[1];
        $result[3] = $len[2];
        return $result;
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = SectionType::AlwaysDrawn->name;
        $result["flags"] = [];
        $result["flags"]["drawOnFront"] = $this->m_drawOnFront;
        $result["flags"]["drawOnBack"] = $this->m_drawOnBack;
        $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing;
        $result["elements"] = [];
        foreach ($this->m_elements as $element) {
            array_push($result["elements"], $element->serializeJSON($formatVersion));
        }
        return $result;
    }

    public static function parseBinary(string $section, int $formatVersion): AlwaysDrawnSection {
        $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]);
        if ($type != SectionType::AlwaysDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a AlwaysDrawnSection");
        }
        $len = unpackU24LE(substr($section, 1, 3));
        [$flags, $nElements] = array_values(unpack("v2", substr($section, 4, 4)));
        $elementData = substr($section, 8);
        $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion);
        $result = new AlwaysDrawnSection();
        $result->m_drawOnFront = isBitSet($flags, 0);
        $result->m_drawOnBack = isBitSet($flags, 1);
        $result->m_clearBeforeDrawing = isBitSet($flags, 2);
        $result->m_elements = $elements;
        return $result;
    }

    public static function parseJSON(array $section, int $formatVersion): AlwaysDrawnSection {
        $type = SectionType::fromString($section["type"]);
        if ($type != SectionType::AlwaysDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a AlwaysDrawnSection");
        }
        $drawOnFront = $section["flags"]["drawOnFront"];
        $drawOnBack = $section["flags"]["drawOnBack"];
        $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"];
        $elements = static::parseElementsJSON($section["elements"], $formatVersion);
        $result = new AlwaysDrawnSection();
        $result->m_drawOnFront = $drawOnFront;
        $result->m_drawOnBack = $drawOnBack;
        $result->m_clearBeforeDrawing = $clearBeforeDrawing;
        $result->m_elements = $elements;
        return $result;
    }
}

class TimeBasedDrawnSection extends ElementContainingSection {
    private int $m_startTimestamp = 0;
    private int $m_endTimestamp = 0;
    private bool $m_drawOnFront = true;
    private bool $m_drawOnBack = true;
    private bool $m_clearBeforeDrawing = true;

    public function doesDrawOnFront(): bool {
        return $this->m_drawOnFront;
    }

    public function doesDrawOnBack(): bool {
        return $this->m_drawOnBack;
    }

    public function doesClearBeforeDrawing(): bool {
        return $this->m_clearBeforeDrawing;
    }

    public function setDrawOnFront(bool $value) {
        $this->m_drawOnFront = $value;
    }

    public function setDrawOnBack(bool $value) {
        $this->m_drawOnBack = $value;
    }

    public function setClearBeforeDrawing(bool $value) {
        $this->m_clearBeforeDrawing = $value;
    }

    public function startTimestamp(): int {
        return $this->m_startTimestamp;
    }

    public function endTimestamp(): int {
        return $this->m_endTimestamp;
    }

    public function setStartTimestamp(int $value) {
        $this->m_startTimestamp = $value;
    }

    public function setEndTimestamp(int $value) {
        $this->m_endTimestamp = $value;
    }

    public function sectionType(): SectionType {
        return SectionType::TimeBasedDrawn;
    }

    public function minimumFormatVersion(): int {
        $result = 1;
        foreach ($this->m_elements as $element) {
            $result = max($result, $element->minimumFormatVersion());
        }
        return $result;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("C", SectionType::TimeBasedDrawn->value);
        // Will be replaced later
        $result .= packU24LE(0);
        $flags = 0;
        maybeSetBit($flags, 0, $this->m_drawOnFront);
        maybeSetBit($flags, 1, $this->m_drawOnBack);
        maybeSetBit($flags, 2, $this->m_clearBeforeDrawing);
        $result .= pack("vv", $flags, count($this->m_elements));
        $result .= pack("PP", $this->m_startTimestamp, $this->m_endTimestamp);
        foreach ($this->m_elements as $element) {
            $result .= $element->serializeBinary($formatVersion);
        }
        $result = alignSerialized($result, 4);
        $len = strlen($result);
        $len = packU24LE($len);
        $result[1] = $len[0];
        $result[2] = $len[1];
        $result[3] = $len[2];
        return $result;
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = SectionType::TimeBasedDrawn->name;
        $result["flags"] = [];
        $result["flags"]["drawOnFront"] = $this->m_drawOnFront;
        $result["flags"]["drawOnBack"] = $this->m_drawOnBack;
        $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing;
        $result["startTimestamp"] = $this->m_startTimestamp;
        $result["endTimestamp"] = $this->m_endTimestamp;
        $result["elements"] = [];
        foreach ($this->m_elements as $element) {
            array_push($result["elements"], $element->serializeJSON($formatVersion));
        }
        return $result;
    }

    public static function parseBinary(string $section, int $formatVersion): TimeBasedDrawnSection {
        $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]);
        if ($type != SectionType::TimeBasedDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a TimeBasedDrawnSection");
        }
        $len = unpackU24LE(substr($section, 1, 3));
        [$flags, $nElements] = array_values(unpack("v2", substr($section, 4, 4)));
        [$startTimestamp, $endTimestamp] = array_values(unpack("P2", substr($section, 8, 16)));
        $elementData = substr($section, 24);
        $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion);
        $result = new TimeBasedDrawnSection();
        $result->m_drawOnFront = isBitSet($flags, 0);
        $result->m_drawOnBack = isBitSet($flags, 1);
        $result->m_clearBeforeDrawing = isBitSet($flags, 2);
        $result->m_startTimestamp = $startTimestamp;
        $result->m_endTimestamp = $endTimestamp;
        $result->m_elements = $elements;
        return $result;
    }

    public static function parseJSON(array $section, int $formatVersion): TimeBasedDrawnSection {
        $type = SectionType::fromString($section["type"]);
        if ($type != SectionType::TimeBasedDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a TimeBasedDrawnSection");
        }
        $drawOnFront = $section["flags"]["drawOnFront"];
        $drawOnBack = $section["flags"]["drawOnBack"];
        $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"];
        $startTimestamp = $section["startTimestamp"];
        $endTimestamp = $section["endTimestamp"];
        $elements = static::parseElementsJSON($section["elements"], $formatVersion);
        $result = new TimeBasedDrawnSection();
        $result->m_drawOnFront = $drawOnFront;
        $result->m_drawOnBack = $drawOnBack;
        $result->m_clearBeforeDrawing = $clearBeforeDrawing;
        $result->m_startTimestamp = $startTimestamp;
        $result->m_endTimestamp = $endTimestamp;
        $result->m_elements = $elements;
        return $result;
    }
}

class MultiTimeBasedDrawnSection extends ElementContainingSection {
    private array $m_timeRanges = [];
    private bool $m_drawOnFront = true;
    private bool $m_drawOnBack = true;
    private bool $m_clearBeforeDrawing = true;

    public function doesDrawOnFront(): bool {
        return $this->m_drawOnFront;
    }

    public function doesDrawOnBack(): bool {
        return $this->m_drawOnBack;
    }

    public function doesClearBeforeDrawing(): bool {
        return $this->m_clearBeforeDrawing;
    }

    public function setDrawOnFront(bool $value) {
        $this->m_drawOnFront = $value;
    }

    public function setDrawOnBack(bool $value) {
        $this->m_drawOnBack = $value;
    }

    public function setClearBeforeDrawing(bool $value) {
        $this->m_clearBeforeDrawing = $value;
    }

    public function timeRanges(): array {
        return $this->m_timeRanges;
    }

    public function setTimeRanges(array $timeRanges) {
        foreach ($timeRanges as $timeRange) {
            if (!is_array($timeRange) || count($timeRange) != 2 || !isset($timeRange[0]) || !isset($timeRange[1])) {
                throw new \ValueError("Invalid time range list supplied");
            }
        }
        $this->m_timeRanges = $timeRanges;
    }

    public function sectionType(): SectionType {
        return SectionType::MultiTimeBasedDrawn;
    }

    public function minimumFormatVersion(): int {
        $result = 2;
        foreach ($this->m_elements as $element) {
            $result = max($result, $element->minimumFormatVersion());
        }
        return $result;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("C", SectionType::MultiTimeBasedDrawn->value);
        // Will be replaced later
        $result .= packU24LE(0);
        $flags = 0;
        maybeSetBit($flags, 0, $this->m_drawOnFront);
        maybeSetBit($flags, 1, $this->m_drawOnBack);
        maybeSetBit($flags, 2, $this->m_clearBeforeDrawing);
        $result .= pack("vvv", $flags, count($this->m_elements), count($this->m_timeRanges), 0);
        foreach ($this->m_timeRanges as $timeRange) {
            $result .= pack("PP", $timeRange[0], $timeRange[1]);
        }
        foreach ($this->m_elements as $element) {
            $result .= $element->serializeBinary($formatVersion);
        }
        $result = alignSerialized($result, 4);
        $len = strlen($result);
        $len = packU24LE($len);
        $result[1] = $len[0];
        $result[2] = $len[1];
        $result[3] = $len[2];
        return $result;
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = SectionType::TimeBasedDrawn->name;
        $result["flags"] = [];
        $result["flags"]["drawOnFront"] = $this->m_drawOnFront;
        $result["flags"]["drawOnBack"] = $this->m_drawOnBack;
        $result["flags"]["clearBeforeDrawing"] = $this->m_clearBeforeDrawing;
        $result["timeRanges"] = $this->m_timeRanges;
        $result["elements"] = [];
        foreach ($this->m_elements as $element) {
            array_push($result["elements"], $element->serializeJSON($formatVersion));
        }
        return $result;
    }

    public static function parseBinary(string $section, int $formatVersion): MultiTimeBasedDrawnSection {
        $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]);
        if ($type != SectionType::MultiTimeBasedDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a MultiTimeBasedDrawnSection");
        }
        $len = unpackU24LE(substr($section, 1, 3));
        [$flags, $nElements, $nRanges, $reserved] = array_values(unpack("v4", substr($section, 4, 8)));
        $section = substr($section, 12);
        $timeRanges = [];
        for ($i = 0; $i < $nRanges; ++$i) {
            [$startTimestamp, $endTimestamp] = array_values(unpack("P2", substr($section, 0, 16)));
            $section = substr($section, 16);
            array_push($timeRanges, [$startTimestamp, $endTimestamp]);
        }

        $elementData = $section;
        $elements = static::parseElementsBinary($elementData, $nElements, $formatVersion);
        $result = new MultiTimeBasedDrawnSection();
        $result->m_drawOnFront = isBitSet($flags, 0);
        $result->m_drawOnBack = isBitSet($flags, 1);
        $result->m_clearBeforeDrawing = isBitSet($flags, 2);
        $result->m_timeRanges = $timeRanges;
        return $result;
    }

    public static function parseJSON(array $section, int $formatVersion): MultiTimeBasedDrawnSection {
        $type = SectionType::fromString($section["type"]);
        if ($type != SectionType::MultiTimeBasedDrawn) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a MultiTimeBasedDrawnSection");
        }
        $drawOnFront = $section["flags"]["drawOnFront"];
        $drawOnBack = $section["flags"]["drawOnBack"];
        $clearBeforeDrawing = $section["flags"]["clearBeforeDrawing"];
        $timeRanges = $section["timeRanges"];
        $elements = static::parseElementsJSON($section["elements"], $formatVersion);
        $result = new MultiTimeBasedDrawnSection();
        $result->m_drawOnFront = $drawOnFront;
        $result->m_drawOnBack = $drawOnBack;
        $result->m_clearBeforeDrawing = $clearBeforeDrawing;
        // Use setter here to validate the structure
        $result->setTimeRanges($timeRanges);
        $result->m_elements = $elements;
        return $result;
    }
}

class ExpiryDateSection extends Section {
    private int $m_expiryDate = 0;

    public function expiryDate(): int {
        return $this->m_expiryDate;
    }

    public function setExpiryDate(int $value) {
        $this->m_expiryDate = $value;
    }

    public function sectionType(): SectionType {
        return SectionType::ExpiryDate;
    }

    public function minimumFormatVersion(): int {
        return 2;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("C", SectionType::ExpiryDate->value);
        // Will be replaced later
        $result .= packU24LE(0);
        $result .= pack("P", $this->m_expiryDate);
        $result = alignSerialized($result, 4);
        $len = strlen($result);
        $len = packU24LE($len);
        $result[1] = $len[0];
        $result[2] = $len[1];
        $result[3] = $len[2];
        return $result;
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = SectionType::ExpiryDate->name;
        $result["expiryDate"] = $this->m_expiryDate;
        return $result;
    }

    public static function parseBinary(string $section, int $formatVersion): ExpiryDateSection {
        $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]);
        if ($type != SectionType::ExpiryDate) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a ExpiryDateSection");
        }
        $len = unpackU24LE(substr($section, 1, 3));
        [$expiryDate] = array_values(unpack("P", substr($section, 4, 8)));
        $result = new ExpiryDateSection();
        $result->m_expiryDate = $expiryDate;
        return $result;
    }

    public static function parseJSON(array $section, int $formatVersion): ExpiryDateSection {
        $type = SectionType::fromString($section["type"]);
        if ($type != SectionType::ExpiryDate) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a ExpiryDateSection");
        }
        $expiryDate = $section["expiryDate"];
        $result = new ExpiryDateSection();
        $result->m_expiryDate = $expiryDate;
        return $result;
    }
}

class CustomFontSection extends Section {
    private string $m_fontData = "";

    public function fontData(): string {
        return $this->m_fontData;
    }

    public function setFontData(string $value) {
        $this->m_fontData = $value;
    }

    public function sectionType(): SectionType {
        return SectionType::CustomFont;
    }

    public function minimumFormatVersion(): int {
        return 1;
    }

    public function serializeBinary(int $formatVersion): string {
        $result = pack("C", SectionType::CustomFont->value);
        // Will be replaced later
        $result .= packU24LE(0);
        $result .= packU24LE(strlen($this->m_fontData));
        $result .= pack("C", 0);
        $result .= $this->m_fontData;
        $result = alignSerialized($result, 4);
        $len = strlen($result);
        $len = packU24LE($len);
        $result[1] = $len[0];
        $result[2] = $len[1];
        $result[3] = $len[2];
        return $result;
    }

    public function serializeJSON(int $formatVersion): array {
        $result = [];
        $result["type"] = SectionType::CustomFont->name;
        $result["data"] = base64_encode($this->m_fontData);
        return $result;
    }

    public static function parseBinary(string $section, int $formatVersion): CustomFontSection {
        $type = SectionType::from(unpack("C", substr($section, 0, 1))[1]);
        if ($type != SectionType::CustomFont) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a CustomFontSection");
        }
        $len = unpackU24LE(substr($section, 1, 3));
        $actualLen = unpackU24LE(substr($section, 4, 3));
        $fontData = substr($section, 8, $actualLen);
        $result = new CustomFontSection();
        $result->m_fontData = $fontData;
        return $result;
    }

    public static function parseJSON(array $section, int $formatVersion): CustomFontSection {
        $type = SectionType::fromString($section["type"]);
        if ($type != SectionType::CustomFont) {
            throw new \ValueError("Invalid section type ". $type->name . " encountered while parsing a CustomFontSection");
        }
        $fontData = base64_decode($section["data"]);
        $result = new CustomFontSection();
        $result->m_fontData = $fontData;
        return $result;
    }
}

class File {
    public array $sections = [];
}

function serializeFile(File $file): string {
    $result = "\xAF\x7E\x2B\x63";
    $formatVersion = 1;
    foreach ($file->sections as $section) {
        $formatVersion = max($formatVersion, $section->minimumFormatVersion());
    }
    $result .= pack("Vvv", $formatVersion, count($file->sections), 0);
    $result = alignSerialized($result, 4);
    foreach ($file->sections as $section) {
        $result .= $section->serializeBinary($formatVersion);
    }
    return $result;
}

function serializeFileJSON(File $file): array {
    $result = [];
    $formatVersion = 1;
    foreach ($file->sections as $section) {
        $formatVersion = max($formatVersion, $section->minimumFormatVersion());
    }
    $result["version"] = $formatVersion;
    $result["sections"] = [];
    foreach ($file->sections as $section) {
        array_push($result["sections"], $section->serializeJSON($formatVersion));
    }
    return $result;
}

function parseFile(string $data): File {
    if (substr($data, 0, 4) != "\xAF\x7E\x2B\x63") {
        throw new \ValueError("The file contains an invalid magic sequence");
    }
    $version = unpack("V", substr($data, 4, 4))[1];
    [$nSections, $reserved] = array_values(unpack("v2", substr($data, 8, 4)));
    $data = substr($data, 12);
    $result = new File();
    for ($i = 0; $i < $nSections; ++$i) {
        $type = SectionType::from(unpack("C", substr($data, 0, 1))[1]);
        $size = unpackU24LE(substr($data, 1, 3));
        $sectionData = substr($data, 0, $size);
        $data = substr($data, $size);
        $section = null;
        switch ($type) {
        case SectionType::AlwaysDrawn:         $section = AlwaysDrawnSection::parseBinary($sectionData, $version);         break;
        case SectionType::TimeBasedDrawn:      $section = TimeBasedDrawnSection::parseBinary($sectionData, $version);      break;
        case SectionType::MultiTimeBasedDrawn: $section = MultiTimeBasedDrawnSection::parseBinary($sectionData, $version); break;
        case SectionType::ExpiryDate:          $section = ExpiryDateSection::parseBinary($sectionData, $version);          break;
        case SectionType::CustomFont:          $section = CustomFontSection::parseBinary($sectionData, $version);          break;
        default: throw new \ValueError("Unsupported section type " . $type->name . " encountered in file");
        }
        array_push($result->sections, $section);
    }
    return $result;
}

function parseFileJSON(array $data): File {
    $version = $data["version"];
    if ($version < 1 || $version > 2) {
        throw new \ValueError("Unsupported file format version $version encountered while parsing a file");
    }
    $result = new File();
    foreach ($data["sections"] as $serializedSection) {
        $type = SectionType::fromString($serializedSection["type"]);
        $section = null;
        switch ($type) {
        case SectionType::AlwaysDrawn:         $section = AlwaysDrawnSection::parseJSON($serializedSection, $version);         break;
        case SectionType::TimeBasedDrawn:      $section = TimeBasedDrawnSection::parseJSON($serializedSection, $version);      break;
        case SectionType::MultiTimeBasedDrawn: $section = MultiTimeBasedDrawnSection::parseJSON($serializedSection, $version); break;
        case SectionType::ExpiryDate:          $section = ExpiryDateSection::parseJSON($serializedSection, $version);          break;
        case SectionType::CustomFont:          $section = CustomFontSection::parseJSON($serializedSection, $version);          break;
        default: throw new \ValueError("Unsupported section type " . $type->name . " encountered in file");
        }
        array_push($result->sections, $section);
    }
    return $result;
}

} // namespace monoformat

?>
