<?php
/**
 * Copyright 2012-2017 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (BSD). If you
 * did not receive this file, see http://www.horde.org/licenses/bsd.
 *
 * @category  Horde
 * @copyright 2012-2017 Horde LLC
 * @license   http://www.horde.org/licenses/bsd New BSD License
 * @package   Mail
 */

/**
 * Container object for a collection of RFC 822 elements.
 *
 * @author    Michael Slusarz <slusarz@horde.org>
 * @category  Horde
 * @copyright 2012-2017 Horde LLC
 * @license   http://www.horde.org/licenses/bsd New BSD License
 * @package   Mail
 *
 * @property-read array $addresses  The list of all addresses (address
 *                                  w/personal parts).
 * @property-read array $bare_addresses  The list of all addresses (mail@host).
 * @property-read array $bare_addresses_idn  The list of all addresses
 *                                           (mail@host; IDN encoded).
 *                                           (@since 2.1.0)
 * @property-read array $base_addresses  The list of ONLY base addresses
 *                                       (Address objects).
 * @property-read array $raw_addresses  The list of all addresses (Address
 *                                      objects).
 */
class Horde_Mail_Rfc822_List
    extends Horde_Mail_Rfc822_Object
    implements ArrayAccess, Countable, SeekableIterator, Serializable
{
    /** Filter masks. */
    const HIDE_GROUPS = 1;
    const BASE_ELEMENTS = 2;

    /**
     * List data.
     *
     * @var array
     */
    protected $_data = array();

    /**
     * Current Iterator filter.
     *
     * @var array
     */
    protected $_filter = array();

    /**
     * Current Iterator pointer.
     *
     * @var array
     */
    protected $_ptr;

    /**
     * Constructor.
     *
     * @param mixed $obs  Address data to store in this object.
     */
    public function __construct($obs = null)
    {
        if (!is_null($obs)) {
            $this->add($obs);
        }
    }

    /**
     */
    public function __get($name)
    {
        switch ($name) {
        case 'addresses':
        case 'bare_addresses':
        case 'bare_addresses_idn':
        case 'base_addresses':
        case 'raw_addresses':
            $old = $this->_filter;
            $mask = ($name == 'base_addresses')
                ? self::BASE_ELEMENTS
                : self::HIDE_GROUPS;
            $this->setIteratorFilter($mask, empty($old['filter']) ? null : $old['filter']);

            $out = array();
            foreach ($this as $val) {
                switch ($name) {
                case 'addresses':
                    $out[] = strval($val);
                    break;

                case 'bare_addresses':
                    $out[] = $val->bare_address;
                    break;

                case 'bare_addresses_idn':
                    $out[] = $val->bare_address_idn;
                    break;

                case 'base_addresses':
                case 'raw_addresses':
                    $out[] = clone $val;
                    break;
                }
            }

            $this->_filter = $old;
            return $out;
        }
    }

    /**
     * Add objects to the container.
     *
     * @param mixed $obs  Address data to store in this object.
     */
    public function add($obs)
    {
        foreach ($this->_normalize($obs) as $val) {
            $this->_data[] = $val;
        }
    }

    /**
     * Remove addresses from the container. This method ignores Group objects.
     *
     * @param mixed $obs  Addresses to remove.
     */
    public function remove($obs)
    {
        $old = $this->_filter;
        $this->setIteratorFilter(self::HIDE_GROUPS | self::BASE_ELEMENTS);

        foreach ($this->_normalize($obs) as $val) {
            $remove = array();

            foreach ($this as $key => $val2) {
                if ($val2->match($val)) {
                    $remove[] = $key;
                }
            }

            foreach (array_reverse($remove) as $key) {
                unset($this[$key]);
            }
        }

        $this->_filter = $old;
    }

    /**
     * Removes duplicate addresses from list. This method ignores Group
     * objects.
     */
    public function unique()
    {
        $exist = $remove = array();

        $old = $this->_filter;
        $this->setIteratorFilter(self::HIDE_GROUPS | self::BASE_ELEMENTS);

        // For duplicates, we use the first address that contains personal
        // information.
        foreach ($this as $key => $val) {
            $bare = $val->bare_address;
            if (isset($exist[$bare])) {
                if (($exist[$bare] == -1) || is_null($val->personal)) {
                    $remove[] = $key;
                } else {
                    $remove[] = $exist[$bare];
                    $exist[$bare] = -1;
                }
            } else {
                $exist[$bare] = is_null($val->personal)
                    ? $key
                    : -1;
            }
        }

        foreach (array_reverse($remove) as $key) {
            unset($this[$key]);
        }

        $this->_filter = $old;
    }

    /**
     * Group count.
     *
     * @return integer  The number of groups in the list.
     */
    public function groupCount()
    {
        $ret = 0;

        foreach ($this->_data as $val) {
            if ($val instanceof Horde_Mail_Rfc822_Group) {
                ++$ret;
            }
        }

        return $ret;
    }

    /**
     * Set the Iterator filter.
     *
     * @param integer $mask  Filter masks.
     * @param mixed $filter  An e-mail, or as list of e-mails, to filter by.
     */
    public function setIteratorFilter($mask = 0, $filter = null)
    {
        $this->_filter = array();

        if ($mask) {
            $this->_filter['mask'] = $mask;
        }

        if (!is_null($filter)) {
            $rfc822 = new Horde_Mail_Rfc822();
            $this->_filter['filter'] = $rfc822->parseAddressList($filter);
        }
    }

    /**
     */
    protected function _writeAddress($opts)
    {
        $out = array();

        foreach ($this->_data as $val) {
            $out[] = $val->writeAddress($opts);
        }

        return implode(', ', $out);
    }

    /**
     */
    public function match($ob)
    {
        if (!($ob instanceof Horde_Mail_Rfc822_List)) {
            $ob = new Horde_Mail_Rfc822_List($ob);
        }

        $a = $this->bare_addresses;
        sort($a);
        $b = $ob->bare_addresses;
        sort($b);

        return ($a == $b);
    }

    /**
     * Does this list contain the given e-mail address?
     *
     * @param mixed $address  An e-mail address.
     *
     * @return boolean  True if the e-mail address is contained in the list.
     */
    public function contains($address)
    {
        $ob = new Horde_Mail_Rfc822_Address($address);

        foreach ($this->raw_addresses as $val) {
            if ($val->match($ob)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Convenience method to return the first element in a list.
     *
     * Useful since it allows chaining; older PHP versions did not allow array
     * access dereferencing from the results of a function call.
     *
     * @since 2.5.0
     *
     * @return Horde_Mail_Rfc822_Object  Rfc822 object, or null if no object.
     */
    public function first()
    {
        return $this[0];
    }

    /**
     * Normalize objects to add to list.
     *
     * @param mixed $obs  Address data to store in this object.
     *
     * @return array  Entries to add.
     */
    protected function _normalize($obs)
    {
        $add = array();

        if (!($obs instanceof Horde_Mail_Rfc822_List) &&
            !is_array($obs)) {
            $obs = array($obs);
        }

        foreach ($obs as $val) {
            if (is_string($val)) {
                $rfc822 = new Horde_Mail_Rfc822();
                $val = $rfc822->parseAddressList($val);
            }

            if ($val instanceof Horde_Mail_Rfc822_List) {
                $val->setIteratorFilter(self::BASE_ELEMENTS);
                foreach ($val as $val2) {
                    $add[] = $val2;
                }
            } elseif ($val instanceof Horde_Mail_Rfc822_Object) {
                $add[] = $val;
            }
        }

        return $add;
    }

    /* ArrayAccess methods. */

    /**
     */
    public function offsetExists($offset)
    {
        return !is_null($this[$offset]);
    }

    /**
     */
    public function offsetGet($offset)
    {
        try {
            $this->seek($offset);
            return $this->current();
        } catch (OutOfBoundsException $e) {
            return null;
        }
    }

    /**
     */
    public function offsetSet($offset, $value)
    {
        if ($ob = $this[$offset]) {
            if (is_null($this->_ptr['subidx'])) {
                $tmp = $this->_normalize($value);
                if (isset($tmp[0])) {
                    $this->_data[$this->_ptr['idx']] = $tmp[0];
                }
            } else {
                $ob[$offset] = $value;
            }
            $this->_ptr = null;
        }
    }

    /**
     */
    public function offsetUnset($offset)
    {
        if ($ob = $this[$offset]) {
            if (is_null($this->_ptr['subidx'])) {
                unset($this->_data[$this->_ptr['idx']]);
                $this->_data = array_values($this->_data);
            } else {
                unset($ob->addresses[$this->_ptr['subidx']]);
            }
            $this->_ptr = null;
        }
    }

    /* Countable methods. */

    /**
     * Address count.
     *
     * @return integer  The number of addresses.
     */
    public function count()
    {
        return count($this->addresses);
    }

    /* Iterator methods. */

    public function current()
    {
        if (!$this->valid()) {
            return null;
        }

        $ob = $this->_data[$this->_ptr['idx']];

        return is_null($this->_ptr['subidx'])
            ? $ob
            : $ob->addresses[$this->_ptr['subidx']];
    }

    public function key()
    {
        return $this->_ptr['key'];
    }

    public function next()
    {
        if (is_null($this->_ptr['subidx'])) {
            $curr = $this->current();
            if (($curr instanceof Horde_Mail_Rfc822_Group) && count($curr)) {
                $this->_ptr['subidx'] = 0;
            } else {
                ++$this->_ptr['idx'];
            }
            $curr = $this->current();
        } elseif (!($curr = $this->_data[$this->_ptr['idx']]->addresses[++$this->_ptr['subidx']])) {
            $this->_ptr['subidx'] = null;
            ++$this->_ptr['idx'];
            $curr = $this->current();
        }

        if (!is_null($curr)) {
            if (!empty($this->_filter) && $this->_iteratorFilter($curr)) {
                $this->next();
            } else {
                ++$this->_ptr['key'];
            }
        }
    }

    public function rewind()
    {
        $this->_ptr = array(
            'idx' => 0,
            'key' => 0,
            'subidx' => null
        );

        if ($this->valid() &&
            !empty($this->_filter) &&
            $this->_iteratorFilter($this->current())) {
            $this->next();
            $this->_ptr['key'] = 0;
        }
    }

    public function valid()
    {
        return (!empty($this->_ptr) && isset($this->_data[$this->_ptr['idx']]));
    }

    public function seek($position)
    {
        if (!$this->valid() ||
            ($position < $this->_ptr['key'])) {
            $this->rewind();
        }

        for ($i = $this->_ptr['key']; ; ++$i) {
            if ($i == $position) {
                return;
            }

            $this->next();
            if (!$this->valid()) {
                throw new OutOfBoundsException('Position not found.');
            }
        }
    }

    protected function _iteratorFilter($ob)
    {
        if (!empty($this->_filter['mask'])) {
            if (($this->_filter['mask'] & self::HIDE_GROUPS) &&
                ($ob instanceof Horde_Mail_Rfc822_Group)) {
                return true;
            }

            if (($this->_filter['mask'] & self::BASE_ELEMENTS) &&
                !is_null($this->_ptr['subidx'])) {
                return true;
            }
        }

        if (!empty($this->_filter['filter']) &&
            ($ob instanceof Horde_Mail_Rfc822_Address)) {
            foreach ($this->_filter['filter'] as $val) {
                if ($ob->match($val)) {
                    return true;
                }
            }
        }

        return false;
    }

    /* Serializable methods. */

    public function serialize()
    {
        return serialize($this->_data);
    }

    public function unserialize($data)
    {
        $this->_data = unserialize($data);
    }

}
