<?php
/**
 * Link         :   http://www.phpcorner.net
 * User         :   qingbing<780042175@qq.com>
 * Date         :   2018-12-10
 * Version      :   1.0
 */

namespace Helper;


use Abstracts\Base;
use Traversable;

class CMap extends Base implements \ArrayAccess, \Countable, \IteratorAggregate
{
    /* @var array map的对象集合 */
    private $_data = [];
    /* @var bool map是否只读，默认可读写 */
    private $_readOnly = false;

    /**
     * 构建一个 map
     * @param mixed $data
     * @param bool $readOnly
     * @throws Exception
     */
    public function __construct($data = null, $readOnly = false)
    {
        if ($data !== null) {
            $this->copyFrom($data);
        }
        $this->setReadOnly($readOnly);
    }

    /**
     * 返回 map 是否只读
     * @return boolean
     */
    public function getReadOnly()
    {
        return $this->_readOnly;
    }

    /**
     * 设置 map 是否只读
     * @param boolean $readOnly
     */
    public function setReadOnly($readOnly)
    {
        $this->_readOnly = $readOnly;
    }

    /**
     * 从可迭代数据中复制成 map
     * @param mixed $data
     * @throws Exception
     */
    public function copyFrom($data)
    {
        if (is_array($data) || $data instanceof \Traversable) {
            if ($this->getCount() > 0) {
                $this->clear();
            }
            if ($data instanceof CMap) {
                $data = $data->_data;
            }
            foreach ($data as $key => $value) {
                $this->add($key, $value);
            }
        } elseif ($data !== null) {
            throw new Exception('实例化列表参数必须是数组或可遍历的对象', 100100601);
        }
    }

    /**
     * map 合并
     * @param mixed $data
     * @param bool|true $recursive
     * @throws Exception
     */
    public function mergeWith($data, $recursive = true)
    {
        if (is_array($data) || $data instanceof \Traversable) {
            if ($data instanceof CMap) {
                $data = $data->_data;
            }
            if ($recursive) {
                if ($data instanceof \Traversable) {
                    $d = [];
                    foreach ($data as $key => $value) {
                        $d[$key] = $value;
                    }
                    $this->_data = self::mergeArray($this->_data, $d);
                } else {
                    $this->_data = self::mergeArray($this->_data, $data);
                }
            } else {
                foreach ($data as $key => $value) {
                    $this->add($key, $value);
                }
            }
        } elseif (null !== $data) {
            throw new Exception('合并map参数必须是数组或可遍历的对象', 100100602);
        }
    }

    /**
     * @param mixed $a
     * @param mixed $b
     * @return array|mixed
     */
    static public function mergeArray($a, $b)
    {
        $args = func_get_args();
        $res = array_shift($args);
        while (!empty($args)) {
            $next = array_shift($args);
            foreach ($next as $k => $v) {
                if (is_integer($k)) {
                    isset($res[$k]) ? $res[] = $v : $res[$k] = $v;
                } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) {
                    $res[$k] = self::mergeArray($res[$k], $v);
                } else {
                    $res[$k] = $v;
                }
            }
        }
        return $res;
    }

    /**
     * 获取 map 的长度
     * @return int
     */
    public function getCount()
    {
        return $this->count();
    }

    /**
     * 返回 map 中 key 对应的元素
     * @param mixed $key
     * @return null|mixed
     */
    protected function itemAt($key)
    {
        if (isset($this->_data[$key])) {
            return $this->_data[$key];
        }
        return null;
    }

    /**
     * 判断 map 中是否包含某个key
     * @param mixed $key
     * @return bool
     */
    public function contains($key)
    {
        return isset($this->_data[$key]) || array_key_exists($key, $this->_data);
    }

    /**
     * 返回 map 的键集合
     * @return array
     */
    public function getKeys()
    {
        return array_keys($this->_data);
    }

    /**
     * 为 map 添加一个元素
     * @param mixed $key
     * @param string $value
     * @throws Exception
     */
    public function add($key, $value)
    {
        if ($this->getReadOnly()) {
            throw new Exception('只读map，不允许执行添加操作', 100100603);
        }
        if (null === $key) {
            $this->_data[] = $value;
        } else {
            $this->_data[$key] = $value;
        }
    }

    /**
     * 返回 map 中 key 对应的元素
     * @param mixed $key
     * @return mixed|null
     */
    public function get($key)
    {
        return $this->itemAt($key);
    }

    /**
     * 删除并返回一个 map 元素
     * @param string $key
     * @return mixed
     * @throws Exception
     */
    public function remove($key)
    {
        if ($this->getReadOnly()) {
            throw new Exception('只读map，不允许执行删除操作', 100100604);
        }
        if (isset($this->_data[$key])) {
            $value = $this->_data[$key];
            unset($this->_data[$key]);
        } else {
            $value = null;
        }
        return $value;
    }

    /**
     * 清空 map 中的元素
     * @throws Exception
     */
    public function clear()
    {
        foreach (array_keys($this->_data) as $key) {
            $this->remove($key);
        }
    }

    /**
     * Whether a offset exists
     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
     * @param mixed $offset <p>
     * An offset to check for.
     * </p>
     * @return boolean true on success or false on failure.
     * </p>
     * <p>
     * The return value will be casted to boolean if non-boolean was returned.
     * @since 5.0.0
     */
    public function offsetExists($offset)
    {
        return $this->contains($offset);
    }

    /**
     * Offset to retrieve
     * @link http://php.net/manual/en/arrayaccess.offsetget.php
     * @param mixed $offset <p>
     * The offset to retrieve.
     * </p>
     * @return mixed Can return all value types.
     * @since 5.0.0
     */
    public function offsetGet($offset)
    {
        return $this->itemAt($offset);
    }

    /**
     * Offset to set
     * @link http://php.net/manual/en/arrayaccess.offsetset.php
     * @param mixed $offset <p>
     * The offset to assign the value to.
     * </p>
     * @param mixed $value <p>
     * The value to set.
     * </p>
     * @return void
     * @since 5.0.0
     * @throws Exception
     */
    public function offsetSet($offset, $value)
    {
        $this->add($offset, $value);
    }

    /**
     * Offset to unset
     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
     * @param mixed $offset <p>
     * The offset to unset.
     * </p>
     * @return void
     * @since 5.0.0
     * @throws Exception
     */
    public function offsetUnset($offset)
    {
        $this->remove($offset);
    }

    /**
     * Count elements of an object
     * @link http://php.net/manual/en/countable.count.php
     * @return int The custom count as an integer.
     * </p>
     * <p>
     * The return value is cast to an integer.
     * @since 5.1.0
     */
    public function count()
    {
        return count($this->_data);
    }

    /**
     * Retrieve an external iterator
     * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
     * @return Traversable An instance of an object implementing <b>Iterator</b> or
     * <b>Traversable</b>
     * @since 5.0.0
     */
    public function getIterator()
    {
        return new CMapIterator($this->_data);
    }
}