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

namespace ZBateson\MailMimeParser\Message;

use Psr\Log\LoggerInterface;
use Traversable;
use ZBateson\MailMimeParser\Header\HeaderConsts;
use ZBateson\MailMimeParser\Header\IHeader;
use ZBateson\MailMimeParser\Header\ParameterHeader;
use ZBateson\MailMimeParser\IMessage;
use ZBateson\MailMimeParser\MailMimeParser;

/**
 * A mime email message part.
 *
 * A MIME part may contain any combination of headers, content and children.
 *
 * @author Zaahid Bateson
 */
class MimePart extends MultiPart implements IMimePart
{
    /**
     * @var PartHeaderContainer Container for this part's headers.
     */
    protected PartHeaderContainer $headerContainer;

    /**
     * @var string Fallback charset for text parts without a declared charset.
     */
    protected string $fallbackCharset;

    public function __construct(
        ?IMimePart $parent = null,
        ?LoggerInterface $logger = null,
        ?PartStreamContainer $streamContainer = null,
        ?PartHeaderContainer $headerContainer = null,
        ?PartChildrenContainer $partChildrenContainer = null,
        string $defaultFallbackCharset = 'ISO-8859-1'
    ) {
        $di = MailMimeParser::getGlobalContainer();
        parent::__construct(
            $logger ?? $di->get(LoggerInterface::class),
            $streamContainer ?? $di->get(PartStreamContainer::class),
            $partChildrenContainer ?? $di->get(PartChildrenContainer::class),
            $parent
        );
        $this->headerContainer = $headerContainer ?? $di->get(PartHeaderContainer::class);
        $this->fallbackCharset = $defaultFallbackCharset;
    }

    /**
     * Returns a filename for the part if one is defined, or null otherwise.
     *
     * Uses the 'filename' parameter of the Content-Disposition header if it
     * exists, or the 'name' parameter of the 'Content-Type' header if it
     * doesn't.
     *
     * @return string|null the file name of the part or null.
     */
    public function getFilename() : ?string
    {
        return $this->getHeaderParameter(
            HeaderConsts::CONTENT_DISPOSITION,
            'filename',
            $this->getHeaderParameter(
                HeaderConsts::CONTENT_TYPE,
                'name'
            )
        );
    }

    /**
     * Returns true.
     *
     */
    public function isMime() : bool
    {
        return true;
    }

    public function isMultiPart() : bool
    {
        // casting to bool, preg_match returns 1 for true
        return (bool) (\preg_match(
            '~multipart/.*~i',
            $this->getContentType()
        ));
    }

    /**
     * Returns true if this part has a defined 'charset' on its Content-Type
     * header.
     *
     * This may result in some false positives if charset is set on a part that
     * is not plain text which has been seen.  If a part is known to be binary,
     * it's better to use {@see IMessagePart::getBinaryContentStream()} to
     * avoid issues, or to call {@see IMessagePart::saveContent()} directly if
     * saving a part's content.
     *
     */
    public function isTextPart() : bool
    {
        return ($this->getCharset() !== null);
    }

    /**
     * Returns the mime type of the content, or $default if one is not set.
     *
     * Looks at the part's Content-Type header and returns its value if set, or
     * defaults to 'text/plain'.
     *
     * Note that the returned value is converted to lower case, and may not be
     * identical to calling {@see MimePart::getHeaderValue('Content-Type')} in
     * some cases.
     *
     * @param string $default Optional default value to specify a default other
     *        than text/plain if needed.
     * @return string the mime type
     */
    public function getContentType(string $default = 'text/plain') : string
    {
        return \strtolower($this->getHeaderValue(HeaderConsts::CONTENT_TYPE, $default));
    }

    /**
     * Returns the charset of the content, or null if not applicable/defined.
     *
     * Looks for a 'charset' parameter under the 'Content-Type' header of this
     * part and returns it if set, defaulting to 'ISO-8859-1' if the
     * Content-Type header exists and is of type text/plain or text/html.
     *
     * Note that the returned value is also converted to upper case.
     *
     * @return string|null the charset
     */
    public function getCharset() : ?string
    {
        $charset = $this->getHeaderParameter(HeaderConsts::CONTENT_TYPE, 'charset');
        if ($charset === null || \strcasecmp($charset, 'binary') === 0) {
            $contentType = $this->getContentType();
            if ($contentType === 'text/plain' || $contentType === 'text/html') {
                return $this->fallbackCharset;
            }
            return null;
        }
        return \strtoupper($charset);
    }

    /**
     * Returns the content's disposition, or returns the value of $default if
     * not defined.
     *
     * Looks at the 'Content-Disposition' header, which should only contain
     * either 'inline' or 'attachment'.  If the header is not one of those
     * values, $default is returned, which defaults to 'inline' unless passed
     * something else.
     *
     * @param string $default Optional default value if not set or does not
     *        match 'inline' or 'attachment'.
     * @return string the content disposition
     */
    public function getContentDisposition(?string $default = 'inline') : ?string
    {
        $value = $this->getHeaderValue(HeaderConsts::CONTENT_DISPOSITION);
        if ($value === null || !\in_array($value, ['inline', 'attachment'])) {
            return $default;
        }
        return \strtolower($value);
    }

    /**
     * Returns the content transfer encoding used to encode the content on this
     * part, or the value of $default if not defined.
     *
     * Looks up and returns the value of the 'Content-Transfer-Encoding' header
     * if set, defaulting to '7bit' if an alternate $default param is not
     * passed.
     *
     * The returned value is always lowercase, and header values of 'x-uue',
     * 'uue' and 'uuencode' will return 'x-uuencode' instead.
     *
     * @param string $default Optional default value to return if the header
     *        isn't set.
     * @return string the content transfer encoding.
     */
    public function getContentTransferEncoding(?string $default = '7bit') : ?string
    {
        static $translated = [
            'x-uue' => 'x-uuencode',
            'uue' => 'x-uuencode',
            'uuencode' => 'x-uuencode'
        ];
        $type = \strtolower($this->getHeaderValue(HeaderConsts::CONTENT_TRANSFER_ENCODING, $default));
        if (isset($translated[$type])) {
            return $translated[$type];
        }
        return $type;
    }

    /**
     * Returns the Content ID of the part, or null if not defined.
     *
     * Looks up and returns the value of the 'Content-ID' header.
     *
     * @return string|null the content ID or null if not defined.
     */
    public function getContentId() : ?string
    {
        return $this->getHeaderValue(HeaderConsts::CONTENT_ID);
    }

    /**
     * Returns true if this part's parent is an IMessage, and is the same part
     * returned by {@see IMessage::getSignaturePart()}.
     */
    public function isSignaturePart() : bool
    {
        if ($this->parent === null || !$this->parent instanceof IMessage) {
            return false;
        }
        return $this->parent->getSignaturePart() === $this;
    }

    public function getHeader(string $name, int $offset = 0) : ?IHeader
    {
        return $this->headerContainer->get($name, $offset);
    }

    public function getHeaderAs(string $name, string $iHeaderClass, int $offset = 0) : ?IHeader
    {
        return $this->headerContainer->getAs($name, $iHeaderClass, $offset);
    }

    public function getAllHeaders() : array
    {
        return $this->headerContainer->getHeaderObjects();
    }

    public function getAllHeadersByName(string $name) : array
    {
        return $this->headerContainer->getAll($name);
    }

    public function getRawHeaders() : array
    {
        return $this->headerContainer->getHeaders();
    }

    public function getRawHeaderIterator() : Traversable
    {
        return $this->headerContainer->getIterator();
    }

    public function getHeaderValue(string $name, ?string $defaultValue = null) : ?string
    {
        $header = $this->getHeader($name);
        if ($header !== null) {
            return $header->getValue() ?: $defaultValue;
        }
        return $defaultValue;
    }

    public function getHeaderParameter(string $header, string $param, ?string $defaultValue = null) : ?string
    {
        $obj = $this->getHeader($header);
        if ($obj && $obj instanceof ParameterHeader) {
            return $obj->getValueFor($param, $defaultValue);
        }
        return $defaultValue;
    }

    public function setRawHeader(string $name, ?string $value, int $offset = 0) : static
    {
        $this->headerContainer->set($name, $value, $offset);
        $this->notify();
        return $this;
    }

    public function addRawHeader(string $name, string $value) : static
    {
        $this->headerContainer->add($name, $value);
        $this->notify();
        return $this;
    }

    public function removeHeader(string $name) : static
    {
        $this->headerContainer->removeAll($name);
        $this->notify();
        return $this;
    }

    public function removeSingleHeader(string $name, int $offset = 0) : static
    {
        $this->headerContainer->remove($name, $offset);
        $this->notify();
        return $this;
    }

    public function getErrorBagChildren() : array
    {
        return \array_merge(parent::getErrorBagChildren(), [$this->headerContainer]);
    }
}
