<?php
/**
 * Copyright (C) 2016-2017  SSilence
 * For the full license, please see LICENSE.
 */

namespace SSilence\ImapClient;

/**
 * Class for all incoming messages.
 *
 * This class get message and generates a message structure of the form:
 * ```php
 * $incomingMessage = new IncomingMessage();
 * $incomingMessage->header;
 * $incomingMessage->message;
 * $incomingMessage->attachments;
 * $incomingMessage->section;
 * $incomingMessage->structure;
 * $incomingMessage->debug;
 * ```
 * And marks the message read.
 * TODO: Format class correctly.
 *
 * @copyright  Copyright (c) Tobias Zeising (http://www.aditu.de)
 * @author    Sergey144010
 */
class IncomingMessage
{
    /**
     * Used to handle sections of the e-mail easier.
     */
    const SECTION_ATTACHMENTS = 1;

    /**
     * Used to handle sections of the e-mail easier.
     */
    const SECTION_BODY = 2;

    /**
     * Do not use decode incoming message.
     */
    const NOT_DECODE = 'not_decode';

    /**
     * Use decode incoming message.
     */
    const DECODE = 'decode';

    /**
     * Header of the message.
     *
     * @var object
     */
    public $header;

    /**
     * The message.
     *
     * @var object
     */
    public $message;

    /**
     * Attachments.
     *
     * @var array
     */
    public $attachments;

    /**
     * Section of the message.
     *
     * @var string|array
     */
    public $section;

    /**
     * Structure of the message.
     *
     * @var object
     */
    public $structure;

    /**
     * Debug on or off.
     *
     * @var object
     */
    public $debug;

    /**
     * The imap string.
     *
     * @var resource
     */
    private $imapStream;

    /**
     * ID of the message.
     *
     * @var int
     */
    private $id;

    /**
     * UID of the message.
     *
     * @var int
     */
    private $uid;

    /**
     * Count the attachments.
     *
     * @var int
     */
    private $countAttachment;

    /**
     * Disable/enable decode current incoming message.
     *
     * @var string
     */
    private $_decode;

    /**
     * Called when the class has a new instance made of it.
     *
     * @param resource $imapStream
     * @param int      $id
     * @param string   $decode
     *
     * @return IncomingMessage
     */
    public function __construct($imapStream, $id, $decode = self::DECODE)
    {
        $this->imapStream = $imapStream;
        if (is_array($id)) {
            $identifier = $id;
            if (isset($identifier['id'])) {
                $this->id = $identifier['id'];
                $this->uid = null;
            }
            if (isset($identifier['uid'])) {
                $this->uid = $identifier['uid'];
                $this->id = null;
            }
            unset($identifier);
        }
        if (is_int($id)) {
            $this->id = $id;
        }

        if (isset($decode)) {
            $this->_decode = $decode;
        }

        $this->init();
    }

    /**
     * Main process.
     *
     * @return void
     */
    protected function init()
    {
        $structure = $this->imapFetchstructure();
        $this->structure = $structure;
        if (isset($structure->parts)) {
            $countSection = count($structure->parts);
            $this->countAttachment = $countSection - 1;
        }
        $this->getCountSection();
        $this->getHeader();
        $this->getAttachments();
        $this->getBody();
        if ($this->_decode === self::DECODE) {
            $this->decode();
        }
    }

    /**
     * Get headers in the current message.
     *
     * Set
     * ```php
     * $this->header
     * $this->header->details
     * ```
     *
     * @return void
     */
    protected function getHeader()
    {
        $header = $this->imapFetchOverview();
        $this->header = $header[0];
        $this->header->details = $this->imapHeaderInfo();
    }

    /**
     * Returns current object.
     *
     * Set $this->debug
     *
     * @return void
     */
    public function debug()
    {
        $this->debug = $this;
    }

    /**
     * Get count section.
     *
     * We take $this->section and make a simple array from an array of arrays.
     * If getRecursiveSections($this->structure) set $this->section to NULL,
     * then we think that there is only one section in the letter.
     * We install $this->section[0] = [0],
     * and then we will take this into account in subsequent processing.
     * Namely here getSection() and $this->getSectionStructure()
     * or getSectionStructureFromIncomingStructure().
     * Because if the message id is correct and the structure is returned,
     * then there is exactly one section in the message.
     *
     * @return array sections
     */
    protected function getCountSection()
    {
        $this->getRecursiveSections($this->structure);
        $sections = array();
        if (!isset($this->section)) {
            $this->section[0] = array(0);
        }
        foreach ($this->section as $array) {
            foreach ($array as $section) {
                $sections[] = $section;
            }
        }
        $sections = array_unique($sections);
        sort($sections);
        $this->section = $sections;

        return $this->section;
    }

    /**
     * Bypasses the recursive parts current message.
     *
     * Counts sections based on $obj->parts.
     * And sets $this->section as an array of arrays or null.
     * Null if $obj->parts is not.
     *
     * @param object $obj
     * @param string $before
     *
     * @return void
     */
    protected function getRecursiveSections($obj, $before = null)
    {
        if (!isset($obj->parts)) {
            return;
        }
        $countParts = count($obj->parts);
        $out = array();
        $beforeSave = $before;
        foreach ($obj->parts as $key => $subObj) {
            if (!isset($beforeSave)) {
                $before = ($key + 1);
            } else {
                $before = $beforeSave.'.'.($key + 1);
            }
            $this->getRecursiveSections($subObj, $before);
            $out[] = (string) $before;
        }
        $this->section[] = $out;
    }

    /**
     * Gets all sections, or if parameter is specified sections by type.
     *
     * @param string $type
     *
     * @throws ImapClientException
     *
     * @return array
     */
    protected function getSections($type = null)
    {
        if (!$type) {
            return $this->section;
        }
        $types = null;
        switch ($type) {
            case self::SECTION_ATTACHMENTS:
                $types = TypeAttachments::get();
                break;
            case self::SECTION_BODY:
                $types = TypeBody::get();
                break;
            default:
                throw new ImapClientException('Section type not recognised/supported');
                break;
        }
        $sections = array();
        foreach ($this->section as $section) {
            $obj = $this->getSectionStructure($section);
            if (!isset($obj->subtype)) {
                continue;
            }
            if (in_array($obj->subtype, $types, false)) {
                $sections[] = $section;
            }
        }

        return $sections;
    }

    /**
     * Get attachments in the current message.
     *
     * Set
     * $this->attachments->name
     * $this->attachments->body
     * $this->attachments->info
     *
     * @return array
     */
    protected function getAttachments()
    {
        $attachments = array();
        foreach ($this->getSections(self::SECTION_ATTACHMENTS) as $section) {
            $obj = $this->getSection($section);
            $attachment = new IncomingMessageAttachment($obj);
            $objNew = new \stdClass();
            $objNew->name = $attachment->name;
            $objNew->body = $attachment->body;
            $objNew->info = $obj;
            $attachments[] = $objNew;
        }
        $this->attachments = $attachments;
    }

    /**
     * Get body current message.
     *
     * Set
     * $this->message->$subtype
     * $this->message->$subtype->charset
     * $this->message->text
     * $this->message->info[]
     * $this->message->types[]
     *
     * @return object
     */
    protected function getBody()
    {
        $objNew = new \stdClass();
        $i = 1;
        $subType = new SubtypeBody();
        foreach ($this->getSections(self::SECTION_BODY) as $section) {
            $obj = $this->getSection($section, array('class' => $subType));
            if(!is_object($obj) || !$obj->__get('structure')) {
                continue;
            }
            $subtype = strtolower($obj->__get('structure')->subtype);
            if(!empty($subtype)) {
                if (!isset($objNew->$subtype)) {
                    $objNew->$subtype = $obj;
                } else {
                    $subtype = $subtype.'_'.$i;
                    $objNew->$subtype = $obj;
                    $i++;
                }
                $objNew->info[] = $obj;
                $objNew->types[] = $subtype;
                /*
                * Set charset
                */
                foreach ($objNew->$subtype->__get('structure')->parameters as $parameter) {
                    $attribute = strtolower($parameter->attribute);
                    if ($attribute === 'charset') {
                        $value = strtolower($parameter->value);
                        /*
                        * Here must be array, but
                        */
                        //$objNew->$subtype->charset[] = $value;
                        $objNew->$subtype->charset = $value;
                    }
                }
            }
        }

        if (isset($objNew->plain)) {
            switch ($objNew->plain->structure->encoding) {
                case 3:
                    $objNew->text = imap_base64(mb_convert_encoding( $objNew->plain, "utf-8", $objNew->plain->charset ));
                    break;
                default:
                    $objNew->text = quoted_printable_decode(mb_convert_encoding( $objNew->plain, "utf-8", 'ISO-8859-1'));
                    break;
            }
            $objNew->types[] = 'text';
        }
        else
            {
                $objNew->text = null;
            }
        $this->message = $objNew;

    }

    /**
     * Get a section message.
     *
     * Return object with 2 properties:
     * $obj->structure
     * $obj->body
     *
     * @param string     $section
     * @param array|null $options have one option $options['class']. It create object, which must be instance \SSilence\ImapClient\Section.
     *
     * @throws ImapClientException
     *
     * @return \SSilence\ImapClient\Section object
     */
    public function getSection($section, $options = null)
    {
        if (isset($options['class'])) {
            $sectionObj = new $options['class']();
            if ($sectionObj instanceof Section) {
            } else {
                throw new ImapClientException('Incoming class not instance \SSilence\ImapClient\Section');
            }
        } else {
            $sectionObj = new Section();
        }
        if ($section === 0) {
            /*
            If the message id is correct and the structure is returned,
            then there is exactly one section in the message.
            */
            $sectionObj->structure = $this->imapBodystruct(1);
            $sectionObj->body = $this->imapFetchbody(1);
        } else {
            $sectionObj->structure = $this->imapBodystruct($section);
            $sectionObj->body = $this->imapFetchbody($section);
        }

        return $sectionObj;
    }

    /**
     * Alias for getSectionStructureFromIncomingStructure();.
     *
     * @param string $section
     *
     * @return object|null
     */
    public function getSectionStructure($section)
    {
        return $this->getSectionStructureFromIncomingStructure($section);
    }

    /**
     * Get section structure from incoming structure.
     *
     * @param string $section
     *
     * @return object|null
     */
    protected function getSectionStructureFromIncomingStructure($section)
    {
        $pos = strpos($section, '.');
        if ($pos === false) {
            $section = (int) $section;
            if ($section === 0) {
                return $this->structure;
            }

            return $this->structure->parts[($section - 1)];
        }
        $sections = explode('.', $section);
        $count = count($sections);
        $outObject = null;
        foreach ($sections as $section) {
            $section = (int) $section;
            if (!isset($outObject)) {
                $outObject = $this->getObjectStructureFromParts($this->structure, ($section - 1));
            } else {
                $outObject = $this->getObjectStructureFromParts($outObject, ($section - 1));
            }
        }

        return $outObject;
    }

    /**
     * Get object structure from parts.
     *
     * @param object $inObject
     * @param int    $part
     *
     * @return object
     */
    protected function getObjectStructureFromParts($inObject, $part)
    {
        return $inObject->parts[$part];
    }

    /**
     * Get a specific section.
     *
     * @param string $section
     *
     * @return string
     */
    protected function imapFetchbody($section)
    {
        /*
     * Update note: We must add FT_PEEK to perserve the unread status of the email.
     * Documentation of this can see seen here: http://php.net/manual/en/function.imap-fetchbody.php under options
     */
        return imap_fetchbody($this->imapStream, $this->id, $section, FT_PEEK);
    }

    /**
     * Structure all messages.
     *
     * @return object
     */
    protected function imapFetchstructure()
    {
        return imap_fetchstructure($this->imapStream, $this->id);
    }

    /**
     * Structure specific section.
     *
     * @param string $section
     *
     * @return object
     */
    protected function imapBodystruct($section)
    {
        return imap_bodystruct($this->imapStream, $this->id, $section);
    }

    /**
     * Fetch a quick "Overview" on a message.
     *
     * @see http://php.net/manual/ru/function.imap-fetch-overview.php
     *
     * @throws ImapClientException
     *
     * @return object
     */
    protected function imapFetchOverview()
    {
        if (isset($this->id) && isset($this->uid)) {
            throw new ImapClientException('What to use id or uid?');
        }
        $sequence = null;
        $options = null;
        if (isset($this->id) && !isset($this->uid)) {
            $sequence = $this->id;
            $options = null;
        }
        if (!isset($this->id) && isset($this->uid)) {
            $sequence = $this->uid;
            $options = FT_UID;
        }

        return imap_fetch_overview($this->imapStream, $sequence, $options);
    }

    /**
     * Imap Header Info.
     *
     * Wrapper for http://php.net/manual/ru/function.imap-headerinfo.php
     *
     * @see http://php.net/manual/ru/function.imap-headerinfo.php
     * @see http://php.net/manual/en/function.imap-headerinfo.php#98809
     *
     * @return object
     */
    protected function imapHeaderInfo()
    {
	 return imap_rfc822_parse_headers(imap_fetchheader($this->imapStream, $this->id));
    }

    /**
     * Convert to utf8 if necessary.
     *
     * @param string $str utf8 encoded string
     *
     * @return string
     */
    public function convertToUtf8($str, $charset = 'ISO-8859-1')
    {
        if ($charset == 'default') {
            $charset = 'utf-8';
        }

        $encoding = mb_detect_encoding($str, mb_detect_order(), false);

        if($encoding == 'UTF-8'){
            $str = mb_convert_encoding($str, 'UTF-8', 'UTF-8');
        }

        $str = iconv(mb_detect_encoding($str, mb_detect_order(), false), "UTF-8//IGNORE", $str);
//        $str = iconv($charset, 'UTF-8//IGNORE', $str);

        return $str;
    }

    /**
     * Wrapper for imap_mime_header_decode()
     * http://php.net/manual/ru/function.imap-mime-header-decode.php.
     *
     * @see http://php.net/manual/ru/function.imap-mime-header-decode.php
     *
     * @param string $string
     *
     * @return array
     */
    protected function imapMimeHeaderDecode($string)
    {
        return imap_mime_header_decode($string);
    }

    /**
     * Decodes and glues the title bar
     * http://php.net/manual/ru/function.imap-mime-header-decode.php.
     *
     * @see http://php.net/manual/ru/function.imap-mime-header-decode.php
     *
     * @param string $string
     *
     * @return string
     */
    protected function mimeHeaderDecode($string)
    {
        $cache = null;
        $array = $this->imapMimeHeaderDecode($string);
        foreach ($array as $object) {
            $cache .= $this->convertToUtf8($object->text, $object->charset);
        }

        return $cache;
    }

    /**
     * Decode incoming message.
     *
     * @return void
     */
    protected function decode()
    {
        $this->decodeHeader();
        $this->decodeBody();
        $this->decodeAttachments();
    }

    /**
     * Decode header.
     *
     * @return void
     */
    protected function decodeHeader()
    {
        if (isset($this->header->subject)) {
            $this->header->subject = $this->mimeHeaderDecode($this->header->subject);
        }
        if (isset($this->header->details->subject)) {
            $this->header->details->subject = $this->mimeHeaderDecode($this->header->details->subject);
        }
        if (isset($this->header->details->Subject)) {
            $this->header->details->Subject = $this->mimeHeaderDecode($this->header->details->Subject);
        }
        if (isset($this->header->from)) {
            $this->header->from = $this->mimeHeaderDecode($this->header->from);
        }
        if (isset($this->header->to)) {
            $this->header->to = $this->mimeHeaderDecode($this->header->to);
        }
    }

    /**
     * Decode attachments.
     *
     * @return void
     */
    protected function decodeAttachments()
    {
        foreach ($this->attachments as $key => $attachment) {
            if(
                is_object($attachment) &&
                property_exists($attachment, 'info') &&
                is_object($attachment->info) &&
                property_exists($attachment->info, 'structure') &&
                is_object($attachment->info->structure) &&
                property_exists($attachment->info->structure, 'encoding')
            ) {
                /*
                 * Decode body
                 */
                switch ($attachment->info->structure->encoding) {
                    case 3:
                        $this->attachments[$key]->body = imap_base64($attachment->body);
                        break;
                    case 4:
                        $this->attachments[$key]->body = quoted_printable_decode($attachment->body);
                        break;
                }
                /*
                 * Decode name
                 */
                $this->attachments[$key]->name = $this->mimeHeaderDecode($attachment->name);
            }
        }
    }

    /**
     * Decode body.
     *
     * @return void
     */
    protected function decodeBody()
    {
        if(is_object($this->message)){
            if(property_exists($this->message, 'types')) {
                foreach ($this->message->types as $typeMessage) {
                    if(is_object($this->message->$typeMessage)){
                        switch ($this->message->$typeMessage->structure->encoding) {
                            case 3:
                                $this->message->$typeMessage->body = imap_base64($this->message->$typeMessage->body);
                                break;
                            case 4:
                                $this->message->$typeMessage->body = imap_qprint($this->message->$typeMessage->body);
                                break;
                        }
                    }

                    if(is_object($this->message->$typeMessage)){
                        if ($this->message->$typeMessage->charset) {
                            $this->message->$typeMessage->body = $this->convertToUtf8($this->message->$typeMessage->body, $this->message->$typeMessage->charset);
                        }
                    }
                }
            }
        }
    }

    /**
     * Info about this object.
     *
     * @return array
     */
    public function __debugInfo()
    {
        return array(
            'header' => $this->header,
            'message' => $this->message,
            'attachments' => $this->attachments,
            'section' => $this->section,
            'structure' => $this->structure,
            'debug' => $this->debug,
        );
    }
    
    /**
     * Get the id property of the message
     *
     * @return int
     */
    public function getID()
    {
        return $this->id;   
    }
}
