<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace MailSo\Mime;

/**
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2019, Afterlogic Corp.
 *
 * @category MailSo
 * @package Mime
 */
class Part
{
    public const POS_HEADERS = 1;
    public const POS_BODY = 2;
    public const POS_SUBPARTS = 3;
    public const POS_CLOSE_BOUNDARY = 4;

    public const DEFAUL_BUFFER = 8192;

    /**
     * @var string
     */
    public static $DefaultCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1;

    /**
     * @var string
     */
    public static $ForceCharset = '';

    /**
     * @var \MailSo\Mime\HeaderCollection
     */
    public $Headers;

    /**
     * @var resource
     */
    public $Body;

    /**
     * @var \MailSo\Mime\PartCollection
     */
    public $SubParts;

    /**
     * @var array
     */
    public $LineParts;

    /**
     * @var string
     */
    private $sBoundary;

    /**
     * @var string
     */
    private $sParentCharset;

    /**
     * @var string
     */
    private $sCharset;

    /**
     * @var int
     */
    private $iParseBuffer;

    /**
     * @access private
     */
    private function __construct()
    {
        $this->iParseBuffer = \MailSo\Mime\Part::DEFAUL_BUFFER;
        $this->reset();
    }

    /**
     * @return \MailSo\Mime\Part
     */
    public static function NewInstance()
    {
        return new self();
    }

    /**
     * @return \MailSo\Mime\Part
     */
    public function reset()
    {
        \MailSo\Base\ResourceRegistry::CloseMemoryResource($this->Body);
        $this->Body = null;

        $this->Headers = HeaderCollection::NewInstance();
        $this->SubParts = PartCollection::NewInstance();
        $this->LineParts = array();
        $this->sBoundary = '';
        $this->sParentCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1;

        return $this;
    }

    /**
     * @return string
     */
    public function Boundary()
    {
        return $this->sBoundary;
    }

    /**
     * @return string
     */
    public function ParentCharset()
    {
        return (0 < \strlen($this->sCharset)) ? $this->sParentCharset : self::$DefaultCharset;
    }

    /**
     * @param string $sParentCharset
     * @return \MailSo\Mime\Part
     */
    public function SetParentCharset($sParentCharset)
    {
        $this->sParentCharset = $sParentCharset;

        return $this;
    }

    /**
     * @param string $sBoundary
     * @return \MailSo\Mime\Part
     */
    public function SetBoundary($sBoundary)
    {
        $this->sBoundary = $sBoundary;

        return $this;
    }

    /**
     * @param int $iParseBuffer
     * @return \MailSo\Mime\Part
     */
    public function SetParseBuffer($iParseBuffer)
    {
        $this->iParseBuffer = $iParseBuffer;

        return $this;
    }

    /**
     * @return string
     */
    public function HeaderCharset()
    {
        return ($this->Headers) ? trim(strtolower($this->Headers->ParameterValue(
            \MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
            \MailSo\Mime\Enumerations\Parameter::CHARSET
        ))) : '';
    }

    /**
     * @return string
     */
    public function HeaderBoundary()
    {
        return ($this->Headers) ? trim($this->Headers->ParameterValue(
            \MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
            \MailSo\Mime\Enumerations\Parameter::BOUNDARY
        )) : '';
    }

    /**
     * @return string
     */
    public function ContentType()
    {
        return ($this->Headers) ?
            trim(strtolower($this->Headers->ValueByName(
                \MailSo\Mime\Enumerations\Header::CONTENT_TYPE
            ))) : '';
    }

    /**
     * @return string
     */
    public function ContentTransferEncoding()
    {
        return ($this->Headers) ?
            trim(strtolower($this->Headers->ValueByName(
                \MailSo\Mime\Enumerations\Header::CONTENT_TRANSFER_ENCODING
            ))) : '';
    }

    /**
     * @return string
     */
    public function ContentID()
    {
        return ($this->Headers) ? trim($this->Headers->ValueByName(
            \MailSo\Mime\Enumerations\Header::CONTENT_ID
        )) : '';
    }

    /**
     * @return string
     */
    public function ContentLocation()
    {
        return ($this->Headers) ? trim($this->Headers->ValueByName(
            \MailSo\Mime\Enumerations\Header::CONTENT_LOCATION
        )) : '';
    }

    /**
     * @return string
     */
    public function FileName()
    {
        $sResult = '';
        if ($this->Headers) {
            $sResult = trim($this->Headers->ParameterValue(
                \MailSo\Mime\Enumerations\Header::CONTENT_DISPOSITION,
                \MailSo\Mime\Enumerations\Parameter::FILENAME
            ));

            if (0 === strlen($sResult)) {
                $sResult = trim($this->Headers->ParameterValue(
                    \MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
                    \MailSo\Mime\Enumerations\Parameter::NAME
                ));
            }
        }

        return $sResult;
    }

    /**
     * @param string $sFileName
     * @return \MailSo\Mime\Part
     */
    public function ParseFromFile($sFileName)
    {
        $rStreamHandle = (@file_exists($sFileName)) ? @fopen($sFileName, 'rb') : false;
        if (is_resource($rStreamHandle)) {
            $this->ParseFromStream($rStreamHandle);

            if (is_resource($rStreamHandle)) {
                fclose($rStreamHandle);
            }
        }

        return $this;
    }

    /**
     * @param string $sRawMessage
     * @return \MailSo\Mime\Part
     */
    public function ParseFromString($sRawMessage)
    {
        $rStreamHandle = (0 < strlen($sRawMessage)) ?
            \MailSo\Base\ResourceRegistry::CreateMemoryResource() : false;

        if (is_resource($rStreamHandle)) {
            fwrite($rStreamHandle, $sRawMessage);
            unset($sRawMessage);
            fseek($rStreamHandle, 0);

            $this->ParseFromStream($rStreamHandle);

            \MailSo\Base\ResourceRegistry::CloseMemoryResource($rStreamHandle);
        }

        return $this;
    }

    /**
     * @param resource $rStreamHandle
     * @return \MailSo\Mime\Part
     */
    public function ParseFromStream($rStreamHandle)
    {
        $this->reset();

        $oParserClass = new \MailSo\Mime\Parser\ParserMemory();

        $oMimePart = null;
        $bIsOef = false;
        $iOffset = 0;
        $sBuffer = '';
        $sPrevBuffer = '';
        $aBoundaryStack = array();


        $oParserClass->StartParse($this);

        $this->LineParts[] =& $this;
        $this->ParseFromStreamRecursion(
            $rStreamHandle,
            $oParserClass,
            $iOffset,
            $sPrevBuffer,
            $sBuffer,
            $aBoundaryStack,
            $bIsOef
        );

        $sFirstNotNullCharset = null;
        foreach ($this->LineParts as /* @var $oMimePart \MailSo\Mime\Part */ &$oMimePart) {
            $sCharset = $oMimePart->HeaderCharset();
            if (0 < strlen($sCharset)) {
                $sFirstNotNullCharset = $sCharset;
                break;
            }
        }

        $sForceCharset = self::$ForceCharset;
        if (0 < strlen($sForceCharset)) {
            foreach ($this->LineParts as /* @var $oMimePart \MailSo\Mime\Part */ &$oMimePart) {
                $oMimePart->SetParentCharset($sForceCharset);
                $oMimePart->Headers->SetParentCharset($sForceCharset);
            }
        } else {
            $sFirstNotNullCharset = (null !== $sFirstNotNullCharset)
                ? $sFirstNotNullCharset : self::$DefaultCharset;

            foreach ($this->LineParts as /* @var $oMimePart \MailSo\Mime\Part */ &$oMimePart) {
                $sHeaderCharset = $oMimePart->HeaderCharset();
                $oMimePart->SetParentCharset((0 < strlen($sHeaderCharset)) ? $sHeaderCharset : $sFirstNotNullCharset);
                $oMimePart->Headers->SetParentCharset($sHeaderCharset);
            }
        }

        $oParserClass->EndParse($this);

        return $this;
    }

    /**
     * @param resource $rStreamHandle
     * @return \MailSo\Mime\Part
     */
    public function ParseFromStreamRecursion(
        $rStreamHandle,
        &$oCallbackClass,
        &$iOffset,
        &$sPrevBuffer,
        &$sBuffer,
        &$aBoundaryStack,
        &$bIsOef,
        $bNotFirstRead = false
    ) {
        $oCallbackClass->StartParseMimePart($this);

        $iPos = 0;
        $iParsePosition = self::POS_HEADERS;
        $sCurrentBoundary = '';
        $bIsBoundaryCheck = false;
        $aHeadersLines = array();
        while (true) {
            if (!$bNotFirstRead) {
                $sPrevBuffer = $sBuffer;
                $sBuffer = '';
            }

            if (!$bIsOef && !feof($rStreamHandle)) {
                if (!$bNotFirstRead) {
                    $sBuffer = @fread($rStreamHandle, $this->iParseBuffer);
                    if (false === $sBuffer) {
                        break;
                    }

                    $oCallbackClass->ReadBuffer($sBuffer);
                } else {
                    $bNotFirstRead = false;
                }
            } elseif ($bIsOef && 0 === strlen($sBuffer)) {
                break;
            } else {
                $bIsOef = true;
            }

            while (true) {
                $sCurrentLine = $sPrevBuffer.$sBuffer;
                if (self::POS_HEADERS === $iParsePosition) {
                    $iEndLen = 4;
                    $iPos = strpos($sCurrentLine, "\r\n\r\n", $iOffset);
                    if (false === $iPos) {
                        $iEndLen = 2;
                        $iPos = strpos($sCurrentLine, "\n\n", $iOffset);
                    }

                    if (false !== $iPos) {
                        $aHeadersLines[] = substr($sCurrentLine, $iOffset, $iPos + $iEndLen - $iOffset);

                        $this->Headers->Parse(implode($aHeadersLines))->SetParentCharset($this->HeaderCharset());
                        $aHeadersLines = array();

                        $oCallbackClass->InitMimePartHeader();

                        $sBoundary = $this->HeaderBoundary();
                        if (0 < strlen($sBoundary)) {
                            $sBoundary = '--'.$sBoundary;
                            $sCurrentBoundary = $sBoundary;
                            array_unshift($aBoundaryStack, $sBoundary);
                        }

                        $iOffset = $iPos + $iEndLen;
                        $iParsePosition = self::POS_BODY;
                        continue;
                    } else {
                        $iBufferLen = strlen($sPrevBuffer);
                        if ($iBufferLen > $iOffset) {
                            $aHeadersLines[] = substr($sPrevBuffer, $iOffset);
                            $iOffset = 0;
                        } else {
                            $iOffset -= $iBufferLen;
                        }
                        break;
                    }
                } elseif (self::POS_BODY === $iParsePosition) {
                    $iPos = false;
                    $sBoundaryLen = 0;
                    $bIsBoundaryEnd = false;
                    $bCurrentPartBody = false;
                    $bIsBoundaryCheck = 0 < count($aBoundaryStack);

                    foreach ($aBoundaryStack as $sKey => $sBoundary) {
                        if (false !== ($iPos = strpos($sCurrentLine, $sBoundary, $iOffset))) {
                            if ($sCurrentBoundary === $sBoundary) {
                                $bCurrentPartBody = true;
                            }

                            $sBoundaryLen = strlen($sBoundary);
                            if ('--' === substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) {
                                $sBoundaryLen += 2;
                                $bIsBoundaryEnd = true;
                                unset($aBoundaryStack[$sKey]);
                                $sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1]))
                                    ? $aBoundaryStack[$sKey + 1] : '';
                            }

                            break;
                        }
                    }

                    if (false !== $iPos) {
                        $oCallbackClass->WriteBody(substr($sCurrentLine, $iOffset, $iPos - $iOffset));
                        $iOffset = $iPos;

                        if ($bCurrentPartBody) {
                            $iParsePosition = self::POS_SUBPARTS;
                            continue;
                        }

                        $oCallbackClass->EndParseMimePart($this);
                        return true;
                    } else {
                        $iBufferLen = strlen($sPrevBuffer);
                        if ($iBufferLen > $iOffset) {
                            $oCallbackClass->WriteBody(substr($sPrevBuffer, $iOffset));
                            $iOffset = 0;
                        } else {
                            $iOffset -= $iBufferLen;
                        }
                        break;
                    }
                } elseif (self::POS_SUBPARTS === $iParsePosition) {
                    $iPos = false;
                    $sBoundaryLen = 0;
                    $bIsBoundaryEnd = false;
                    $bCurrentPartBody = false;
                    $bIsBoundaryCheck = 0 < count($aBoundaryStack);

                    foreach ($aBoundaryStack as $sKey => $sBoundary) {
                        if (false !== ($iPos = strpos($sCurrentLine, $sBoundary, $iOffset))) {
                            if ($sCurrentBoundary === $sBoundary) {
                                $bCurrentPartBody = true;
                            }

                            $sBoundaryLen = strlen($sBoundary);
                            if ('--' === substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) {
                                $sBoundaryLen += 2;
                                $bIsBoundaryEnd = true;
                                unset($aBoundaryStack[$sKey]);
                                $sCurrentBoundary = (isset($aBoundaryStack[$sKey + 1]))
                                    ? $aBoundaryStack[$sKey + 1] : '';
                            }
                            break;
                        }
                    }

                    if (false !== $iPos && $bCurrentPartBody) {
                        $iOffset = $iPos + $sBoundaryLen;

                        $oSubPart = self::NewInstance();

                        $oSubPart
                            ->SetParseBuffer($this->iParseBuffer)
                            ->ParseFromStreamRecursion(
                                $rStreamHandle,
                                $oCallbackClass,
                                $iOffset,
                                $sPrevBuffer,
                                $sBuffer,
                                $aBoundaryStack,
                                $bIsOef,
                                true
                            );

                        $this->SubParts->Add($oSubPart);
                        $this->LineParts[] =& $oSubPart;
                        //$iParsePosition = self::POS_HEADERS;
                        unset($oSubPart);
                    } else {
                        $oCallbackClass->EndParseMimePart($this);
                        return true;
                    }
                }
            }
        }

        if (0 < strlen($sPrevBuffer)) {
            if (self::POS_HEADERS === $iParsePosition) {
                $aHeadersLines[] = ($iOffset < strlen($sPrevBuffer))
                    ? substr($sPrevBuffer, $iOffset)
                    : $sPrevBuffer;

                $this->Headers->Parse(implode($aHeadersLines))->SetParentCharset($this->HeaderCharset());
                $aHeadersLines = array();

                $oCallbackClass->InitMimePartHeader();
            } elseif (self::POS_BODY === $iParsePosition) {
                if (!$bIsBoundaryCheck) {
                    $oCallbackClass->WriteBody(($iOffset < strlen($sPrevBuffer))
                        ? substr($sPrevBuffer, $iOffset) : $sPrevBuffer);
                }
            }
        } else {
            if (self::POS_HEADERS === $iParsePosition && 0 < count($aHeadersLines)) {
                $this->Headers->Parse(implode($aHeadersLines))->SetParentCharset($this->HeaderCharset());
                $aHeadersLines = array();

                $oCallbackClass->InitMimePartHeader();
            }
        }

        $oCallbackClass->EndParseMimePart($this);

        return $this;
    }

    /**
     * @return void
     */
    public function Rewind()
    {
        if ($this->Body && \is_resource($this->Body)) {
            $aMeta = \stream_get_meta_data($this->Body);
            if (isset($aMeta['seekable']) && $aMeta['seekable']) {
                \rewind($this->Body);
            }
        }
    }

    /**
     * @return resource
     */
    public function ToStream()
    {
        $this->Rewind();

        $aSubStreams = array(

            $this->Headers->ToEncodedString().
                \MailSo\Mime\Enumerations\Constants::CRLF.
                \MailSo\Mime\Enumerations\Constants::CRLF,

            null === $this->Body ? '' : $this->Body,

            \MailSo\Mime\Enumerations\Constants::CRLF
        );

        if (0 < $this->SubParts->Count()) {
            $sBoundary = $this->HeaderBoundary();
            if (0 < strlen($sBoundary)) {
                $aSubStreams[] = '--'.$sBoundary.\MailSo\Mime\Enumerations\Constants::CRLF;

                $rSubPartsStream = $this->SubParts->ToStream($sBoundary);
                if (is_resource($rSubPartsStream)) {
                    $aSubStreams[] = $rSubPartsStream;
                }

                $aSubStreams[] = \MailSo\Mime\Enumerations\Constants::CRLF.
                    '--'.$sBoundary.'--'.\MailSo\Mime\Enumerations\Constants::CRLF;
            }
        }

        return \MailSo\Base\StreamWrappers\SubStreams::CreateStream($aSubStreams);
    }
}
