<?php
// Phalanx
// Copyright (c) 2009 Blue Static
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program.  If not, see <http://www.gnu.org/licenses/>.

namespace phalanx\base;

// This manages a ref to a "descendable" type. A descendable type is either an
// object or an array, which are key-value structures that can hold,
// recursively, other descendable types. A KeyDescender allows a caller to
// access keys and values in a type-agnostic manner.
class KeyDescender
{
    // The root ref we descend.
    protected $root;

    // Whether or not we throw exceptions for undefined keys. This enables
    // strict keyed access. Otherwise, NULL will be returned on failure.
    protected $throw_undefined_errors = TRUE;

    public function __construct(/* Array|Object */ & $root)
    {
        if (!self::IsDescendable($root))
            throw new KeyDescenderException('Cannot create a KeyDescender on a non-descendable reference');
        $this->root = &$root;
    }

    // Checks whether a given type is descendable. This is used to create the
    // base case for recursive descention.
    static public function IsDescendable($value)
    {
        return (is_array($value) || is_object($value));
    }

    // Returns a ref to the keyed value. Example: "foo.bar.baz"
    public function & Get($key)
    {
        $stack = explode('.', $key);
        $current = &$this->root;
        for ($i = 0; $i < count($stack); $i++)
        {
            try
            {
                $current = &$this->_Get($current, $stack[$i]);
            }
            // Catch subkey exceptions and re-throw them as main key ones.
            catch (UndefinedKeyException $e)
            {
                if ($this->throw_undefined_errors)
                    throw new UndefinedKeyException("Undefined key $key");
                $null = NULL;  // Avoid PHP reference error.
                return $null;
            }
        }

        return $current;
    }

    // Returns a key, ignoring the |$this->throw_undefined_errors| setting and
    // returning NULL if not found.
    public function GetSilent($key)
    {
        $value = NULL;
        try
        {
            $value = $this->Get($key);
        }
        catch (UndefinedKeyException $e)
        {}
        return $value;
    }

    // Sets a value for a given key.
    public function Set($key, $value)
    {
        // Get the parent of the key we're inserting.
        $stack = explode('.', $key);

        // Remove the subkey that we will be creating (the last one).
        $single_key = array_pop($stack);

        $parent = NULL;
        // Attach to root.
        if (count($stack) < 1)
        {
            $parent = &$this->root;
        }
        // Find proper super-key.
        else
        {
            $parent_key = implode('.', $stack);
            $parent = &$this->Get($parent_key);
        }

        if ($parent === NULL)
            throw new UndefinedKeyException("Cannot insert '$key' because it has a non-existent super-key");

        if (is_object($parent))
            $parent->$single_key = $value;
        else if (is_array($parent))
            $parent[$single_key] = $value;
        else
            throw new KeyDescenderException("Cannot insert '$key' because it has a non-descendable super-key");
    }

    // Returns a value from a given key in a descendable.
    protected function & _Get(& $descendable, $single_key)
    {
        if (is_array($descendable))
        {
            if (isset($descendable[$single_key]))
                return $descendable[$single_key];
            else
                throw new UndefinedKeyException("Undefined key '$single_key' on $descendable");
        }
        else if (is_object($descendable))
        {
            if ($descendable instanceof KeyDescender)
                return $descendable->Get($single_key);
            else if (isset($descendable->$single_key))
                return $descendable->$single_key;
            else if (method_exists($descendable, '__get'))
                return $descendable->__get($single_key);
            else
                throw new UndefinedKeyException("Undefined '$single_key' on " . spl_object_hash($descendable));
        }
        else
        {
            throw new KeyDescenderException("'$descendable' is not descendable");
        }
    }

    // Wrappers for get() and set() so we can do magical property access, which
    // will even apply to arrays.
    public function __get($key) { return $this->Get($key); }
    public function __set($key, $value) { $this->Set($key, $value); }

    // Getters and setters.
    // -------------------------------------------------------------------------
    public function & root() { return $this->root; }

    public function throw_undefined_errors() { return $this->throw_undefined_errors; }
    public function set_throw_undefined_errors($throw)
    {
        $this->throw_undefined_errors = $throw;
    }
}

class KeyDescenderException extends \Exception
{
}

class UndefinedKeyException extends KeyDescenderException
{
}