<?php
/**
 * This file is part of the ZBateson\MailMimeParser project.
 *
 * @license http://opensource.org/licenses/bsd-license.php BSD
 */

namespace ZBateson\MailMimeParser\Parser;

use GuzzleHttp\Psr7\StreamWrapper;
use Psr\Http\Message\StreamInterface;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
use ZBateson\MailMimeParser\Parser\Proxy\ParserMimePartProxy;
use ZBateson\MailMimeParser\Parser\Proxy\ParserPartProxy;

/**
 * Holds generic/all purpose information about a part while it's being parsed.
 *
 * The class holds:
 *  - a HeaderContainer to hold headers
 *  - stream positions (part start/end positions, content start/end)
 *  - the message's psr7 stream and a resource handle created from it (held
 *    only for a top-level PartBuilder representing the message, child
 *    PartBuilders do not duplicate/hold a separate stream).
 *
 * More specific information a parser needs to keep about a message as it's
 * parsing it should be stored in its ParserPartProxy.
 *
 * @author Zaahid Bateson
 */
class PartBuilder
{
    /**
     * @var int The offset read start position for this part (beginning of
     * headers) in the message's stream.
     */
    private int $streamPartStartPos;

    /**
     * @var int The offset read end position for this part.  If the part is a
     * multipart mime part, the end position is after all of this parts
     * children.
     */
    private int $streamPartEndPos;

    /**
     * @var ?int The offset read start position in the message's stream for the
     * beginning of this part's content (body).
     */
    private ?int $streamContentStartPos = null;

    /**
     * @var ?int The offset read end position in the message's stream for the
     * end of this part's content (body).
     */
    private ?int $streamContentEndPos = null;

    /**
     * @var PartHeaderContainer The parsed part's headers.
     */
    private PartHeaderContainer $headerContainer;

    /**
     * @var StreamInterface the raw message input stream for a message, or null
     *      for a child part.
     */
    private ?StreamInterface $messageStream = null;

    /**
     * @var resource|null the raw message input stream handle constructed from
     *      $messageStream or null for a child part
     */
    private mixed $messageHandle = null;

    /**
     * @var ParserPartProxy|null The parent proxy part if one is set, or null if
     *      the part being built doesn't have a parent.
     */
    private ?ParserPartProxy $parent = null;

    public function __construct(PartHeaderContainer $headerContainer, ?StreamInterface $messageStream = null, ?ParserPartProxy $parent = null)
    {
        $this->headerContainer = $headerContainer;
        $this->messageStream = $messageStream;
        $this->parent = $parent;
        if ($messageStream !== null) {
            $this->messageHandle = StreamWrapper::getResource($messageStream);
        }
        $this->setStreamPartStartPos($this->getMessageResourceHandlePos());
    }

    public function __destruct()
    {
        if ($this->messageHandle !== null) {
            \fclose($this->messageHandle);
        }
    }

    /**
     * The ParserPartProxy parent of this PartBuilder.
     */
    public function getParent() : ?ParserPartProxy
    {
        return $this->parent;
    }

    /**
     * Returns this part's PartHeaderContainer.
     */
    public function getHeaderContainer() : PartHeaderContainer
    {
        return $this->headerContainer;
    }

    /**
     * Returns the raw message StreamInterface for a message, getting it from
     * the parent part if this is a child part.
     */
    public function getStream() : StreamInterface
    {
        return ($this->messageStream === null && $this->parent !== null) ?
            $this->parent->getStream() :
            $this->messageStream;
    }

    /**
     * Returns the resource handle for a the message's stream, getting it from
     * the parent part if this is a child part.
     *
     * @return resource
     */
    public function getMessageResourceHandle() : mixed
    {
        return ($this->messageHandle === null && $this->parent !== null) ?
            $this->parent->getMessageResourceHandle() :
            $this->messageHandle;
    }

    /**
     * Shortcut for calling ftell($partBuilder->getMessageResourceHandle()).
     */
    public function getMessageResourceHandlePos() : int
    {
        return \ftell($this->getMessageResourceHandle());
    }

    /**
     * Returns the byte offset start position for this part within the message
     * stream.
     */
    public function getStreamPartStartPos() : int
    {
        return $this->streamPartStartPos;
    }

    /**
     * Returns the number of raw bytes this part has.
     *
     * This method does not perform checks on whether the start pos and end pos
     * of this part have been set, and so could cause errors if called before
     * being set and are still null.
     *
     */
    public function getStreamPartLength() : int
    {
        return $this->streamPartEndPos - $this->streamPartStartPos;
    }

    /**
     * Returns the byte offset start position of the content of this part within
     * the main raw message stream, or null if not set.
     *
     */
    public function getStreamContentStartPos() : ?int
    {
        return $this->streamContentStartPos;
    }

    /**
     * Returns the length of this part's content stream.
     *
     * This method does not perform checks on whether the start pos and end pos
     * of this part's content have been set, and so could cause errors if called
     * before being set and are still null.
     *
     */
    public function getStreamContentLength() : int
    {
        return $this->streamContentEndPos - $this->streamContentStartPos;
    }

    /**
     * Sets the byte offset start position of the part in the raw message
     * stream.
     */
    public function setStreamPartStartPos(int $streamPartStartPos) : static
    {
        $this->streamPartStartPos = $streamPartStartPos;
        return $this;
    }

    /**
     * Sets the byte offset end position of the part in the raw message stream,
     * and also calls its parent's setParentStreamPartEndPos to expand to parent
     * PartBuilders.
     */
    public function setStreamPartEndPos(int $streamPartEndPos) : static
    {
        $this->streamPartEndPos = $streamPartEndPos;
        if ($this->parent !== null) {
            $this->parent->setStreamPartEndPos($streamPartEndPos);
        }
        return $this;
    }

    /**
     * Sets the byte offset start position of the content in the raw message
     * stream.
     */
    public function setStreamContentStartPos(int $streamContentStartPos) : static
    {
        $this->streamContentStartPos = $streamContentStartPos;
        return $this;
    }

    /**
     * Sets the byte offset end position of the content and part in the raw
     * message stream.
     */
    public function setStreamPartAndContentEndPos(int $streamContentEndPos) : static
    {
        $this->streamContentEndPos = $streamContentEndPos;
        $this->setStreamPartEndPos($streamContentEndPos);
        return $this;
    }

    /**
     * Returns true if the byte offset positions for this part's content have
     * been set.
     *
     * @return bool true if set.
     */
    public function isContentParsed() : bool
    {
        return ($this->streamContentEndPos !== null);
    }

    /**
     * Returns true if this part, or any parent, have a Content-Type or
     * MIME-Version header set.
     *
     * @return bool true if it's a mime message or child of a mime message.
     */
    public function isMime() : bool
    {
        if ($this->getParent() !== null) {
            return $this->getParent()->isMime();
        }
        return ($this->headerContainer->exists(HeaderConsts::CONTENT_TYPE) ||
            $this->headerContainer->exists(HeaderConsts::MIME_VERSION));
    }
}
