<?php
/**
 * YANA library
 *
 * Software:  Yana PHP-Framework
 * Version:   {VERSION} - {DATE}
 * License:   GNU GPL  http://www.gnu.org/licenses/
 *
 * This program: can be redistributed and/or modified under the
 * terms of the GNU General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or
 * (at your option) 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/.
 *
 * This notice MAY NOT be removed.
 *
 * @package  yana
 * @license  http://www.gnu.org/licenses/gpl.txt
 */

/**
 * database structure
 *
 * This wrapper class represents the structure of a database
 *
 * @access      public
 * @abstract
 * @package     yana
 * @subpackage  database
 */
abstract class DDL extends Object implements IsSerializable
{
    /**
     * File extensions
     *
     * @access  protected
     * @static
     * @var     string
     * @ignore
     */
    protected static $extension = ".db.xml";

    /**
     * tag name
     *
     * This setting is used for persistance mapping: object <-> XDDL
     *
     * @var  string
     */
    protected $xddlTag = null;

    /**
     * class name must match exactly
     *
     * @var  bool
     */
    protected $exactMatch = false;

    /**
     * list of XML-attributes
     *
     * There settings are used for persistance mapping: object <-> XDDL
     *
     * 2-dimensional, associative array, where each row presents a XML attribute.
     * They key to each row is the attribute name.
     *
     * Each row may have 2 values:
     * <ul>
     *   <li> Object property name </li>
     *   <li> Property type (string, bool, int, float, nmtoken, array) </li>
     * </ul>
     *
     * The type "nmtoken" refers to an identifier (like "name"), while "array" is a comma-seperated
     * list of identifiers.
     *
     * @access  protected
     * @var     array
     * @see     DDL::_unserializeFromXDDL()
     * @ignore
     */
    protected $xddlAttributes = array();

    /**
     * list of XML-tags
     *
     * There settings are used for persistance mapping: object <-> XDDL
     *
     * 2-dimensional, associative array, where each row presents a XML tag.
     * They key to each row is the tag name.
     *
     * Each row may have 6 values:
     * <ul>
     *   <li> Object property name </li>
     *   <li> Property type (string, bool, int, float, array, class name) </li>
     *   <li> (optional) class name (for each item of an array only) </li>
     *   <li> (optional) name of key column for array items </li>
     *   <li> (optional) name of value column for array items (only if array has scalar values) </li>
     *   <li> (optional) default key value </li>
     * </ul>
     *
     * The classes must all be sub-classes of DDL. These MUST implement DDL::unserializeFromXDDL()
     * and MAY overwrite DDL::serializeToXDDL().
     *
     * @access  protected
     * @var     array
     * @see     DDL::_unserializeFromXDDL()
     * @ignore
     */
    protected $xddlTags = array();

    /**
     * database base directory
     *
     * @access  protected
     * @var     string
     * @ignore
     */
    protected static $databaseDirectory = null;

    /**
     * get a string representation of this object
     *
     * This function is intended to be called when the object
     * is used in a string context.
     *
     * @access  public
     * @return  string
     */
    public function toString()
    {
        try {
            $xml = $this->serializeToXDDL();
            return $xml->asXML();
        } catch (Exception $e) {
            return $e->getMessage(); // toString() must not throw an exception
        }
    }

    /**
     * serialize this object to a string
     *
     * Returns the serialized object as a string.
     *
     * @access  public
     * @return  string
     */
    public function serialize()
    {
        return serialize($this);
    }

    /**
     * unserialize a string to a serializable object
     *
     * Returns the unserialized object.
     *
     * @access  public
     * @static
     * @param   string  $string  string to unserialize
     * @return  IsSerializable
     */
    public static function unserialize($string)
    {
        return unserialize($string);
    }

    /**
     * serialize this object to XDDL
     *
     * Returns the serialized object as a string in XML-DDL format.
     *
     * @access  public
     * @param   SimpleXMLElement $parentNode  parent node
     * @return  SimpleXMLElement
     */
    public function serializeToXDDL(SimpleXMLElement $parentNode = null)
    {
        // is not serializable - need tag name!
        if (empty($this->xddlTag)) {
            return "";

        // is root node
        } elseif (is_null($parentNode)) {
            $xddl = new SimpleXMLElement('<' . $this->xddlTag . '/>', LIBXML_NOXMLDECL | LIBXML_NOENT);

        // is inner node with pcdata-section
        } elseif (isset($this->xddlAttributes['#pcdata'])) {
            $property = $this->xddlAttributes['#pcdata'][0];
            $value = (string) $this->$property;
            $xddl = $parentNode->addChild($this->xddlTag, $value);
            unset($property, $value);

        // is inner node with children
        } else {
            $xddl = $parentNode->addChild($this->xddlTag);
        }

        // set attributes
        if (!empty($this->xddlAttributes)) {
            foreach ($this->xddlAttributes as $name => $attribute)
            {
                if ($name !== '#pcdata') {
                    $property = $attribute[0];
                    $type = $attribute[1];
                    if (isset($this->$property)) {
                        $value = null;
                        switch ($type)
                        {
                            case 'array':
                                if (!empty($this->$property)) {
                                    $value = implode(',', $this->$property);
                                }
                            break;
                            case 'bool':
                                if ($this->$property) {
                                    $value = 'yes';
                                } else {
                                    $value = 'no';
                                }
                            break;
                            default:
                                if (!is_null($this->$property) && $this->$property !== "") {
                                    $value = (string) $this->$property;
                                }
                            break;
                        }
                        if (!is_null($value)) {
                            $xddl->addAttribute($name, $value);
                        }
                        unset($value);
                    }
                    unset($name, $attribute);
                }
            }
        }

        // set children
        if (!empty($this->xddlTags)) {
            foreach ($this->xddlTags as $name => $tag)
            {
                $property = $tag[0];
                $type = $tag[1];
                // skip undefined tags
                if (is_null($this->$property)) {
                    continue;
                }
                // switch by data type
                switch ($type)
                {
                    case 'string':
                    case 'nmtoken':
                    case 'int':
                    case 'float':
                        $xddl->addChild($name, (string) $this->$property);
                    break;
                    case 'bool':
                        if ($this->$property) {
                            $xddl->addChild($name, 'yes');
                        } else {
                            $xddl->addChild($name, 'no');
                        }
                    break;
                    case 'array':
                        // is list of complex child-tags
                        if (isset($tag[2])) {
                            assert('!isset($className); // Cannot redeclare var $className');
                            $className = $tag[2];
                            foreach ($this->$property as $object)
                            {
                                if (! $object instanceof $className) {
                                    continue;
                                }
                                if ($object->exactMatch && strcasecmp(get_class($object), $className) !== 0) {
                                    continue;
                                }
                                if ($object instanceof IsIncludableDDL && $this !== $object->getParent()) {
                                    continue;
                                }
                                $object->serializeToXDDL($xddl);
                            }
                            unset($className);

                        // is list of string tags
                        } else {
                            // attribute name for keys
                            if (isset($tag[3])) {
                                $keyAttr = $tag[3];
                            } else {
                                $keyAttr = null;
                            }
                            // attribute name for values
                            if (isset($tag[4])) {
                                $valAttr = $tag[4];
                            } else {
                                $valAttr = null;
                            }
                            // iterate through
                            assert('!isset($key);');
                            assert('!isset($value);');
                            assert('!isset($childNode);');
                            foreach ($this->$property as $key => $value)
                            {
                                if (is_null($valAttr)) {
                                    $childNode = $xddl->addChild($name, (string) $value);
                                } else {
                                    $childNode = $xddl->addChild($name);
                                    $childNode->addAttribute($valAttr, (string) $value);
                                }
                                if (!is_null($keyAttr) && is_string($key) && $keyAttr != $valAttr) {
                                    $childNode->addAttribute($keyAttr, $key);
                                }
                            }
                            unset($key, $value, $childNode);
                        }
                    break;
                    default:
                        if (is_array($this->$property)) {
                            foreach ($this->$property as $object)
                            {
                                $object->serializeToXDDL($xddl);
                            }
                        } elseif (!is_null($this->$property)) {
                            $object = $this->$property;
                            $object->serializeToXDDL($xddl);
                        }
                    break;
                }

            }
            unset($name, $tag);
        }
        return $xddl;
    }

    /**
     * unserialize a XDDL-node to an object
     *
     * Returns the unserialized object.
     *
     * @access  public
     * @abstract
     * @static
     * @param   SimpleXMLElement  $node    node create via XMLArray::toArray()
     * @param   mixed             $parent  parent node (if any)
     * @return  DDL
     */
    public static function unserializeFromXDDL(SimpleXMLElement $node, $parent = null)
    {
        // Note: as of PHP 5.2 static functions may not be declared abstract
    }

    /**
     * unserialize a XDDL-node to an object
     *
     * Returns the unserialized object.
     *
     * @access  protected
     * @final
     * @param   SimpleXMLElement  $node  XML node
     * @ignore
     */
    final protected function _unserializeFromXDDL(SimpleXMLElement $node)
    {
        $this->_unserializeAttributes($node);
        foreach ($node->children() as $currentNode)
        {
            $this->_unserializeChild($currentNode);
        }
    }

    /**
     * unserialize child node
     *
     * @access  protected
     * @final
     * @param   SimpleXMLElement  $node  XML node
     * @ignore
     */
    final protected function _unserializeChild(SimpleXMLElement $node)
    {
        $xml = $node->getName();
        $attributes = $node->attributes();
        if (isset($this->xddlTags[$xml])) {
            $tag = $this->xddlTags[$xml];
            $property = $tag[0];
            $type = $tag[1];
            switch ($type)
            {
                case 'nmtoken':
                    $this->$property = mb_strtolower("$node");
                break;
                case 'string':
                    $this->$property = trim("$node");
                break;
                case 'array':
                    $array =& $this->$property;
                    if (isset($tag[3])) {
                        $index = $tag[3];
                    } else {
                        $index = 'name';
                    }
                    if (isset($attributes->$index)) {
                        $id = (string) $attributes->$index;
                    } else {
                        $id = null;
                    }
                    unset($index);
                    if (isset($tag[2])) {
                        if (!is_null($id)) {
                            $id = mb_strtolower($id);
                        }
                        $class = $tag[2];
                        $value = call_user_func(array($class, 'unserializeFromXDDL'), $node, $this);
                    } else {
                        $value = null;
                        if (isset($tag[4])) {
                            $index = $tag[4];
                            if (isset($attributes->$index)) {
                                $value = (string) $attributes->$index;
                            } else {
                                $value = "";
                            }
                        } else {
                            $value = trim("$node");
                        }
                        unset($index);
                    }
                    if (!is_null($id)) {
                        $array[$id] = $value;
                    } elseif (isset($tag[5])) {
                        $array[$tag[5]] = $value;
                    } else {
                        $array[] = $value;
                    }
                break;
                case 'bool':
                    $this->$property = ("$node" === 'yes');
                break;
                case 'int':
                    $this->$property = (int) "$node";
                break;
                case 'float':
                    $this->$property = (float) "$node";
                break;
                default:
                    $object = call_user_func(array($type, 'unserializeFromXDDL'), $node, $this);
                    $this->$property = $object;
                break;
            }
        }
    }

    /**
     * unserialize attributes
     *
     * @access  protected
     * @final
     * @param   SimpleXMLElement  $node  XML node
     * @ignore
     */
    final protected function _unserializeAttributes(SimpleXMLElement $node)
    {
        $attributes = $node->attributes();
        // set attributes
        foreach ($this->xddlAttributes as $xml => $attribute)
        {
            $value = null;
            $property = $attribute[0];
            $type = $attribute[1];
            if ($xml === '#pcdata') {
                $value = trim("$node");
            } elseif (isset($attributes->$xml)) {
                $value = (string) $attributes->$xml;
            }
            if (isset($value)) {
                switch ($type)
                {
                    case 'nmtoken':
                        $this->$property = mb_strtolower($value);
                    break;
                    case 'array':
                        $value = preg_split('/, ?/', $value);
                        $this->$property = $value;
                    break;
                    case 'bool':
                        $this->$property = ($value === 'yes');
                    break;
                    case 'int':
                        $this->$property = (int) $value;
                    break;
                    case 'float':
                        $this->$property = (float) $value;
                    break;
                    default:
                        $this->$property = (string) $value;
                    break;
                }
            } // end if
        } // end foreach
    }

    /**
     * get database directory
     *
     * Returns the path to the directory where XDDL files are to be stored.
     * The path is configured using the setting "DBDIR" in the system configuration.
     * You may overwrite this setting by calling DDL::setDirectory().
     *
     * @access  public
     * @static
     * @return  string
     */
    public static function getDirectory()
    {
        if (!isset(self::$databaseDirectory)) {
            $Yana = Yana::getInstance();
            self::$databaseDirectory = getcwd() . '/' . $Yana->getVar('DBDIR');
        }
        return self::$databaseDirectory;
    }

    /**
     * set database directory
     *
     * Set the path to the directory where XDDL files are to be stored.
     *
     * @access  public
     * @static
     * @param   string  $directory  path to XDDL base directory
     */
    public static function setDirectory($directory)
    {
        assert('is_string($directory); // Wrong argument type argument 1. String expected');
        assert('is_dir($directory); // Wrong argument type argument 1. Directory expected');

        self::$databaseDirectory = "$directory";
    }

    /**
     * get file path from database name
     *
     * Looks up the source path for the wanted database in the file system and returns the path.
     *
     * @access  public
     * @static
     * @param   string  $databaseName   database name
     * @return  string
     */
    public static function getPath($databaseName)
    {
        assert('is_string($databaseName); // Wrong type for argument 1. String expected');
        if (!preg_match('/^([\w\d_]+)$/', $databaseName)) {
            return "$databaseName";
        }
        $file = self::getDirectory() . "$databaseName" . DDL::$extension;
        assert('is_file($file); // File not found: ' . $file);
        return $file;
    }

    /**
     * get database name from file path
     *
     * Returns a unique identifier for the given file path as a string.
     * Actually this is the filename for the XDDL file.
     *
     * @access  public
     * @static
     * @param   string  $path   database path
     * @return  string
     */
    public static function getNameFromPath($path)
    {
        assert('is_string($path); // Wrong type for argument 1. String expected');
        return basename("$path", DDL::$extension);
    }

    /**
     * return list of known DDL files
     *
     * This function returns a numeric list of filenames of known DDL files.
     * Each item is a valid argument for creating a new database connection.
     *
     * If the argument $useFullFilename is set to bool(true) the items are complete filenames,
     * including the path, relative to the framework's root directory.
     * Otherwise only the file names are returned, which are easier to read for humans.
     *
     * In case of an unexpected error, this function returns an empty array.
     *
     * @static
     * @access  public
     * @param   bool  $useFullFilename  return items as full filenames (true = yes, false = no)
     * @return  array
     */
    public static function getListOfFiles($useFullFilename = false)
    {
        assert('is_bool($useFullFilename); // Wrong type for argument 1. Boolean expected');
        if (isset($GLOBALS['YANA'])) {
            $dbDir = $GLOBALS['YANA']->getVar('DBDIR');
        } else {
            $dbDir = "config/db/";
        }
        $list = array();
        $dirList = glob($dbDir . "/*" . DDL::$extension);
        if (is_array($dirList)) {
            foreach ($dirList as $filename)
            {
                /* prepend path */
                if ($useFullFilename) {
                    $list[] = $filename;

                /* remove suffix: '.db.xml' */
                } else {
                    $list[] = basename($filename, DDL::$extension);
                }
            }
        }
        return $list;
    }
}

?>