<?php
/*
 * Copyright 2010-2015 by MODX, LLC.
 *
 * This file is part of xPDO.
 *
 * xPDO 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 2 of the License, or (at your option) any later
 * version.
 *
 * xPDO 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
 * xPDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
 * Suite 330, Boston, MA 02111-1307 USA
 */

/**
 * Class for reverse and forward engineering xPDO domain models.
 *
 * @package xpdo
 * @subpackage om
 */

/**
 * A service for reverse and forward engineering xPDO domain models.
 *
 * This service utilizes an xPDOManager instance to generate class stub and
 * meta-data map files from a provided vanilla XML schema of a database
 * structure.  It can also reverse-engineer XML schemas from an existing
 * database.
 *
 * @abstract
 * @package xpdo
 * @subpackage om
 */
abstract class xPDOGenerator {
    /**
     * @var xPDOManager $manager A reference to the xPDOManager using this
     * generator.
     */
    public $manager= null;
    /**
     * @var xPDOSchemaManager $schemaManager
     */
    public $schemaManager= null;
    /**
     * @var string $outputDir The absolute path to output the class and map
     * files to.
     */
    public $outputDir= '';
    /**
     * @var string $schemaFile An absolute path to the schema file.
     */
    public $schemaFile= '';
    /**
     * @var string $schemaContent The stored content of the newly-created schema
     * file.
     */
    public $schemaContent= '';
    /**
     * @var string $classTemplate The class template string to build the class
     * files from.
     */
    public $classTemplate= '';
    /**
     * @var string $platformTemplate The class platform template string to build
     * the class platform files from.
     */
    public $platformTemplate= '';
    /**
     * @var string $metaTemplate The class platform template string to build
     * the meta class map files from.
     */
    public $metaTemplate= '';
    /**
     * @var string $mapHeader The map header string to build the map files from.
     */
    public $mapHeader= '';
    /**
     * @var string $mapFooter The map footer string to build the map files from.
     */
    public $mapFooter= '';
    /**
     * @var array $model The stored model array.
     */
    public $model= array ();
    /**
     * @var array $classes The stored classes array.
     */
    public $classes= array ();
    /**
     * @var array $map The stored map array.
     */
    public $map= array ();
    /**
     * @var SimpleXMLElement
     */
    public $schema= null;

    /**
     * Constructor
     *
     * @access protected
     * @param xPDOManager &$manager A reference to a valid xPDOManager instance.
     * @return xPDOGenerator
     */
    public function __construct(& $manager) {
        $this->manager= & $manager;
    }

    /**
     * Formats a class name to a specific value, stripping the prefix if
     * specified.
     *
     * @access public
     * @param string $string The name to format.
     * @param string $prefix If specified, will strip the prefix out of the
     * first argument.
     * @param boolean $prefixRequired If true, will return a blank string if the
     * prefix specified is not found.
     * @return string The formatting string.
     */
    public function getTableName($string, $prefix= '', $prefixRequired= false) {
        if (!empty($prefix) && strpos($string, $prefix) === 0) {
            $string= substr($string, strlen($prefix));
        }
        elseif ($prefixRequired) {
            $string= '';
        }
        return $string;
    }

    /**
     * Gets a class name from a table name by splitting the string by _ and
     * capitalizing each token.
     *
     * @access public
     * @param string $string The table name to format.
     * @return string The formatted string.
     */
    public function getClassName($string) {
        if (is_string($string) && $strArray= explode('_', $string)) {
            $return= '';
            foreach ($strArray as $k => $v) {
                $return.= strtoupper(substr($v, 0, 1)) . substr($v, 1) . '';
            }
            $string= $return;
        }
        return trim($string);
    }

    /**
     * Format the passed default value as an XML attribute.
     *
     * Override this in different PDO driver implementations if necessary.
     *
     * @access public
     * @param string $value The value to encapsulate in the default tag.
     * @return string The parsed XML string
     */
    public function getDefault($value) {
        $return= '';
        if ($value !== null) {
            $return= ' default="'.$value.'"';
        }
        return $return;
    }

    /**
     * Format the passed database index value as an XML attribute.
     *
     * @abstract Implement this for specific PDO driver implementations.
     * @access public
     * @param string $index The DB representation string of the index
     * @return string The formatted XML attribute string
     */
    abstract public function getIndex($index);

    /**
     * Parses an XPDO XML schema and generates classes and map files from it.
     *
     * Requires SimpleXML for parsing an XML schema.
     *
     * @param string $schemaFile The name of the XML file representing the
     * schema.
     * @param string $outputDir The directory in which to generate the class and
     * map files into.
     * @param boolean $compile Create compiled copies of the classes and maps from the schema.
     * @return boolean True on success, false on failure.
     */
    public function parseSchema($schemaFile, $outputDir= '', $compile= false) {
        $this->schemaFile= $schemaFile;
        $this->classTemplate= $this->getClassTemplate();
        if (!is_file($schemaFile)) {
            $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not find specified XML schema file {$schemaFile}");
            return false;
        }

        $this->schema = new SimpleXMLElement($schemaFile, 0, true);
        if (isset($this->schema)) {
            foreach ($this->schema->attributes() as $attributeKey => $attribute) {
                /** @var SimpleXMLElement $attribute */
                $this->model[$attributeKey] = (string) $attribute;
            }
            if (isset($this->schema->object)) {
                foreach ($this->schema->object as $object) {
                    /** @var SimpleXMLElement $object */
                    $class = (string) $object['class'];
                    $extends = isset($object['extends']) ? (string) $object['extends'] : $this->model['baseClass'];
                    $this->classes[$class] = array('extends' => $extends);
                    $this->map[$class] = array(
                        'package' => $this->model['package'],
                        'version' => $this->model['version']
                    );
                    foreach ($object->attributes() as $objAttrKey => $objAttr) {
                        if ($objAttrKey == 'class') continue;
                        $this->map[$class][$objAttrKey]= (string) $objAttr;
                        if (!in_array($objAttrKey, array('package', 'version', 'extends', 'table'))) {
                            $this->classes[$class][$objAttrKey] = (string) $objAttr;
                        }
                    }

                    $engine = (string) $object['engine'];
                    if (!empty($engine)) {
                        $this->map[$class]['tableMeta'] = array('engine' => $engine);
                    } elseif (isset($this->model['defaultEngine'])) {
                        $this->map[$class]['tableMeta'] = array('engine' => $this->model['defaultEngine']);
                    }

                    $this->map[$class]['fields']= array();
                    $this->map[$class]['fieldMeta']= array();
                    if (isset($object->field)) {
                        foreach ($object->field as $field) {
                            $key = (string) $field['key'];
                            $dbtype = (string) $field['dbtype'];
                            $defaultType = $this->manager->xpdo->driver->getPhpType($dbtype);
                            $this->map[$class]['fields'][$key]= null;
                            $this->map[$class]['fieldMeta'][$key]= array();
                            foreach ($field->attributes() as $fldAttrKey => $fldAttr) {
                                $fldAttrValue = (string) $fldAttr;
                                switch ($fldAttrKey) {
                                    case 'key':
                                        continue 2;
                                    case 'default':
                                        if ($fldAttrValue === 'NULL') {
                                            $fldAttrValue = null;
                                        }
                                        switch ($defaultType) {
                                            case 'integer':
                                            case 'boolean':
                                            case 'bit':
                                                $fldAttrValue = (integer) $fldAttrValue;
                                                break;
                                            case 'float':
                                            case 'numeric':
                                                $fldAttrValue = (float) $fldAttrValue;
                                                break;
                                            default:
                                                break;
                                        }
                                        $this->map[$class]['fields'][$key]= $fldAttrValue;
                                        break;
                                    case 'null':
                                        $fldAttrValue = (!empty($fldAttrValue) && strtolower($fldAttrValue) !== 'false') ? true : false;
                                        break;
                                    default:
                                        break;
                                }
                                $this->map[$class]['fieldMeta'][$key][$fldAttrKey]= $fldAttrValue;
                            }
                        }
                    }
                    if (isset($object->alias)) {
                        $this->map[$class]['fieldAliases'] = array();
                        foreach ($object->alias as $alias) {
                            $aliasKey = (string) $alias['key'];
                            $aliasNode = array();
                            foreach ($alias->attributes() as $attrName => $attr) {
                                $attrValue = (string) $attr;
                                switch ($attrName) {
                                    case 'key':
                                        continue 2;
                                    case 'field':
                                        $aliasNode = $attrValue;
                                        break;
                                    default:
                                        break;
                                }
                            }
                            if (!empty($aliasKey) && !empty($aliasNode)) {
                                $this->map[$class]['fieldAliases'][$aliasKey] = $aliasNode;
                            }
                        }
                    }
                    if (isset($object->index)) {
                        $this->map[$class]['indexes'] = array();
                        foreach ($object->index as $index) {
                            $indexNode = array();
                            $indexName = (string) $index['name'];
                            foreach ($index->attributes() as $attrName => $attr) {
                                $attrValue = (string) $attr;
                                switch ($attrName) {
                                    case 'name':
                                        continue 2;
                                    case 'primary':
                                    case 'unique':
                                    case 'fulltext':
                                        $attrValue = (empty($attrValue) || $attrValue === 'false' ? false : true);
                                    default:
                                        $indexNode[$attrName] = $attrValue;
                                        break;
                                }
                            }
                            if (!empty($indexNode) && isset($index->column)) {
                                $indexNode['columns']= array();
                                foreach ($index->column as $column) {
                                    $columnKey = (string) $column['key'];
                                    $indexNode['columns'][$columnKey] = array();
                                    foreach ($column->attributes() as $attrName => $attr) {
                                        $attrValue = (string) $attr;
                                        switch ($attrName) {
                                            case 'key':
                                                continue 2;
                                            case 'null':
                                                $attrValue = (empty($attrValue) || $attrValue === 'false' ? false : true);
                                            default:
                                                $indexNode['columns'][$columnKey][$attrName]= $attrValue;
                                                break;
                                        }
                                    }
                                }
                                if (!empty($indexNode['columns'])) {
                                    $this->map[$class]['indexes'][$indexName]= $indexNode;
                                }
                            }
                        }
                    }
                    if (isset($object->composite)) {
                        $this->map[$class]['composites'] = array();
                        foreach ($object->composite as $composite) {
                            $compositeNode = array();
                            $compositeAlias = (string) $composite['alias'];
                            foreach ($composite->attributes() as $attrName => $attr) {
                                $attrValue = (string) $attr;
                                switch ($attrName) {
                                    case 'alias' :
                                        continue 2;
                                    case 'criteria' :
                                        $attrValue = $this->manager->xpdo->fromJSON(urldecode($attrValue));
                                    default :
                                        $compositeNode[$attrName]= $attrValue;
                                        break;
                                }
                            }
                            if (!empty($compositeNode)) {
                                if (isset($composite->criteria)) {
                                    /** @var SimpleXMLElement $criteria */
                                    foreach ($composite->criteria as $criteria) {
                                        $criteriaTarget = (string) $criteria['target'];
                                        $expression = (string) $criteria;
                                        if (!empty($expression)) {
                                            $expression = $this->manager->xpdo->fromJSON($expression);
                                            if (!empty($expression)) {
                                                if (!isset($compositeNode['criteria'])) $compositeNode['criteria'] = array();
                                                if (!isset($compositeNode['criteria'][$criteriaTarget])) $compositeNode['criteria'][$criteriaTarget] = array();
                                                $compositeNode['criteria'][$criteriaTarget] = array_merge($compositeNode['criteria'][$criteriaTarget], (array) $expression);
                                            }
                                        }
                                    }
                                }
                                $this->map[$class]['composites'][$compositeAlias] = $compositeNode;
                            }
                        }
                    }
                    if (isset($object->aggregate)) {
                        $this->map[$class]['aggregates'] = array();
                        foreach ($object->aggregate as $aggregate) {
                            $aggregateNode = array();
                            $aggregateAlias = (string) $aggregate['alias'];
                            foreach ($aggregate->attributes() as $attrName => $attr) {
                                $attrValue = (string) $attr;
                                switch ($attrName) {
                                    case 'alias' :
                                        continue 2;
                                    case 'criteria' :
                                        $attrValue = $this->manager->xpdo->fromJSON(urldecode($attrValue));
                                    default :
                                        $aggregateNode[$attrName]= $attrValue;
                                        break;
                                }
                            }
                            if (!empty($aggregateNode)) {
                                if (isset($aggregate->criteria)) {
                                    /** @var SimpleXMLElement $criteria */
                                    foreach ($aggregate->criteria as $criteria) {
                                        $criteriaTarget = (string) $criteria['target'];
                                        $expression = (string) $criteria;
                                        if (!empty($expression)) {
                                            $expression = $this->manager->xpdo->fromJSON($expression);
                                            if (!empty($expression)) {
                                                if (!isset($aggregateNode['criteria'])) $aggregateNode['criteria'] = array();
                                                if (!isset($aggregateNode['criteria'][$criteriaTarget])) $aggregateNode['criteria'][$criteriaTarget] = array();
                                                $aggregateNode['criteria'][$criteriaTarget] = array_merge($aggregateNode['criteria'][$criteriaTarget], (array) $expression);
                                            }
                                        }
                                    }
                                }
                                $this->map[$class]['aggregates'][$aggregateAlias] = $aggregateNode;
                            }
                        }
                    }
                    if (isset($object->validation)) {
                        $this->map[$class]['validation'] = array();
                        $validation = $object->validation[0];
                        $validationNode = array();
                        foreach ($validation->attributes() as $attrName => $attr) {
                            $validationNode[$attrName]= (string) $attr;
                        }
                        if (isset($validation->rule)) {
                            $validationNode['rules'] = array();
                            foreach ($validation->rule as $rule) {
                                $ruleNode = array();
                                $field= (string) $rule['field'];
                                $name= (string) $rule['name'];
                                foreach ($rule->attributes() as $attrName => $attr) {
                                    $attrValue = (string) $attr;
                                    switch ($attrName) {
                                        case 'field' :
                                        case 'name' :
                                            continue 2;
                                        default :
                                            $ruleNode[$attrName]= $attrValue;
                                            break;
                                    }
                                }
                                if (!empty($field) && !empty($name) && !empty($ruleNode)) {
                                    $validationNode['rules'][$field][$name]= $ruleNode;
                                }
                            }
                            if (!empty($validationNode['rules'])) {
                                $this->map[$class]['validation'] = $validationNode;
                            }
                        }
                    }
                }
            } else {
                $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Schema {$schemaFile} contains no valid object elements.");
            }
        } else {
            $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not read schema from {$schemaFile}.");
        }

        $om_path= XPDO_CORE_PATH . 'om/';
        $path= !empty ($outputDir) ? $outputDir : $om_path;
        if (isset ($this->model['package']) && strlen($this->model['package']) > 0) {
            $path .= strtr($this->model['package'], '.', '/');
            $path .= '/';
        }
        $this->outputMeta($path);
        $this->outputClasses($path);
        $this->outputMaps($path);
        if ($compile) $this->compile($path, $this->model, $this->classes, $this->maps);
        unset($this->model, $this->classes, $this->map);
        return true;
    }

    /**
     * Write the generated class files to the specified path.
     *
     * @access public
     * @param string $path An absolute path to write the generated class files
     * to.
     */
    public function outputClasses($path) {
        $newClassGeneration= false;
        $newPlatformGeneration= false;
        $platform= $this->model['platform'];
        if (!is_dir($path)) {
            $newClassGeneration= true;
            if ($this->manager->xpdo->getCacheManager()) {
                if (!$this->manager->xpdo->cacheManager->writeTree($path)) {
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not create model directory at {$path}");
                    return false;
                }
            }
        }
        $ppath= $path;
        $ppath .= $platform;
        if (!is_dir($ppath)) {
            $newPlatformGeneration= true;
            if ($this->manager->xpdo->getCacheManager()) {
                if (!$this->manager->xpdo->cacheManager->writeTree($ppath)) {
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not create platform subdirectory {$ppath}");
                    return false;
                }
            }
        }
        $model= $this->model;
        if (isset($this->model['phpdoc-package'])) {
            $model['phpdoc-package']= '@package ' . $this->model['phpdoc-package'];
            if (isset($this->model['phpdoc-subpackage']) && !empty($this->model['phpdoc-subpackage'])) {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['phpdoc-subpackage'] . '.' . $this->model['platform'];
            } else {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['platform'];
            }
        } else {
            $basePos= strpos($this->model['package'], '.');
            $package= $basePos
                ? substr($this->model['package'], 0, $basePos)
                : $this->model['package'];
            $subpackage= $basePos
                ? substr($this->model['package'], $basePos + 1)
                : '';
            $model['phpdoc-package']= '@package ' . $package;
            if ($subpackage) $model['phpdoc-subpackage']= '@subpackage ' . $subpackage;
        }
        foreach ($this->classes as $className => $classDef) {
            $newClass= false;
            $classDef['class']= $className;
            $classDef['class-lowercase']= strtolower($className);
            $classDef= array_merge($model, $classDef);
            $replaceVars= array ();
            foreach ($classDef as $varKey => $varValue) {
                if (is_scalar($varValue)) $replaceVars["[+{$varKey}+]"]= $varValue;
            }
            $fileContent= str_replace(array_keys($replaceVars), array_values($replaceVars), $this->classTemplate);
            if (is_dir($path)) {
                $fileName= $path . strtolower($className) . '.class.php';
                if (!file_exists($fileName)) {
                    if ($file= @ fopen($fileName, 'wb')) {
                        if (!fwrite($file, $fileContent)) {
                            $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not write to file: {$fileName}");
                        }
                        $newClass= true;
                        @fclose($file);
                    } else {
                        $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create file: {$fileName}");
                    }
                } else {
                    $newClass= false;
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_INFO, "Skipping {$fileName}; file already exists.\nMove existing class files to regenerate them.");
                }
            } else {
                $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create dir: {$path}");
            }
            $fileContent= str_replace(array_keys($replaceVars), array_values($replaceVars), $this->getClassPlatformTemplate($platform));
            if (is_dir($ppath)) {
                $fileName= $ppath . '/' . strtolower($className) . '.class.php';
                if (!file_exists($fileName)) {
                    if ($file= @ fopen($fileName, 'wb')) {
                        if (!fwrite($file, $fileContent)) {
                            $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not write to file: {$fileName}");
                        }
                        @fclose($file);
                    } else {
                        $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create file: {$fileName}");
                    }
                } else {
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_INFO, "Skipping {$fileName}; file already exists.\nMove existing class files to regenerate them.");
                    if ($newClassGeneration || $newClass) $this->manager->xpdo->log(xPDO::LOG_LEVEL_WARN, "IMPORTANT: {$fileName} already exists but you appear to have generated classes with an older xPDO version.  You need to edit your class definition in this file to extend {$className} rather than {$classDef['extends']}.");
                }
            } else {
                $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create dir: {$path}");
            }
        }
    }

    /**
     * Write the generated class maps to the specified path.
     *
     * @access public
     * @param string $path An absolute path to write the generated maps to.
     */
    public function outputMaps($path) {
        if (!is_dir($path)) {
            mkdir($path, 0777);
        }
        $path .= $this->model['platform'];
        if (!is_dir($path)) {
            mkdir($path, 0777);
        }
        $model= $this->model;
        if (isset($this->model['phpdoc-package'])) {
            $model['phpdoc-package']= '@package ' . $this->model['phpdoc-package'];
            if (isset($this->model['phpdoc-subpackage']) && !empty($this->model['phpdoc-subpackage'])) {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['phpdoc-subpackage'] . '.' . $this->model['platform'];
            } else {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['platform'];
            }
        } else {
            $basePos= strpos($this->model['package'], '.');
            $package= $basePos
                ? substr($this->model['package'], 0, $basePos)
                : $this->model['package'];
            $subpackage= $basePos
                ? substr($this->model['package'], $basePos + 1) . '.' . $this->model['platform']
                : $this->model['platform'];
            $model['phpdoc-package']= '@package ' . $package;
            $model['phpdoc-subpackage']= '@subpackage ' . $subpackage;
        }
        foreach ($this->map as $className => $map) {
            $lcClassName= strtolower($className);
            $fileName= $path . '/' . strtolower($className) . '.map.inc.php';
            $vars= array_merge($model, $map);
            $replaceVars= array ();
            foreach ($vars as $varKey => $varValue) {
                if (is_scalar($varValue)) $replaceVars["[+{$varKey}+]"]= $varValue;
            }
            $fileContent= str_replace(array_keys($replaceVars), array_values($replaceVars), $this->getMapHeader());
            $fileContent.= "\n\$xpdo_meta_map['$className']= " . var_export($map, true) . ";\n";
            $fileContent.= str_replace(array_keys($replaceVars), array_values($replaceVars), $this->getMapFooter());
            if (is_dir($path)) {
                if ($file= @ fopen($fileName, 'wb')) {
                    if (!fwrite($file, $fileContent)) {
                        $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not write to file: {$fileName}");
                    }
                    fclose($file);
                } else {
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create file: {$fileName}");
                }
            } else {
                $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not open or create dir: {$path}");
            }
        }
    }

    /**
     * Write the generated meta map to the specified path.
     *
     * @param string $path An absolute path to write the generated maps to.
     * @return bool
     */
    public function outputMeta($path) {
        if (!is_dir($path)) {
            if ($this->manager->xpdo->getCacheManager()) {
                if (!$this->manager->xpdo->cacheManager->writeTree($path)) {
                    $this->manager->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not create model directory at {$path}");
                    return false;
                }
            }
        }
        $placeholders = array();

        $model= $this->model;
        if (isset($this->model['phpdoc-package'])) {
            $model['phpdoc-package']= '@package ' . $this->model['phpdoc-package'];
            if (isset($this->model['phpdoc-subpackage']) && !empty($this->model['phpdoc-subpackage'])) {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['phpdoc-subpackage'] . '.' . $this->model['platform'];
            } else {
                $model['phpdoc-subpackage']= '@subpackage ' . $this->model['platform'];
            }
        } else {
            $basePos= strpos($this->model['package'], '.');
            $package= $basePos
                ? substr($this->model['package'], 0, $basePos)
                : $this->model['package'];
            $subpackage= $basePos
                ? substr($this->model['package'], $basePos + 1) . '.' . $this->model['platform']
                : $this->model['platform'];
            $model['phpdoc-package']= '@package ' . $package;
            $model['phpdoc-subpackage']= '@subpackage ' . $subpackage;
        }
        $placeholders = array_merge($placeholders,$model);

        $classMap = array();
//        $skipClasses = array('xPDOObject','xPDOSimpleObject');
        foreach ($this->classes as $className => $meta) {
            if (!isset($meta['extends'])) {
                $meta['extends'] = 'xPDOObject';
            }
            if (!isset($classMap[$meta['extends']])) {
                $classMap[$meta['extends']] = array();
            }
            $classMap[$meta['extends']][] = $className;
        }
        if ($this->manager->xpdo->getCacheManager()) {
            $placeholders['map'] = var_export($classMap,true);
            $replaceVars = array();
            foreach ($placeholders as $varKey => $varValue) {
                if (is_scalar($varValue)) $replaceVars["[+{$varKey}+]"]= $varValue;
            }
            $fileContent= str_replace(array_keys($replaceVars), array_values($replaceVars), $this->getMetaTemplate());
            $this->manager->xpdo->cacheManager->writeFile("{$path}/metadata.{$model['platform']}.php",$fileContent);
        }
        return true;
    }

    /**
     * Compile the packages into a single file for quicker loading.
     *
     * @abstract
     * @access public
     * @param string $path The absolute path to compile into.
     * @return boolean True if the compiling went successfully.
     */
    abstract public function compile($path= '');

    /**
     * Return the class template for the class files.
     *
     * @access public
     * @return string The class template.
     */
    public function getClassTemplate() {
        if ($this->classTemplate) return $this->classTemplate;
        $template= <<<EOD
<?php
class [+class+] extends [+extends+] {}
EOD;
        return $template;
    }

    /**
     * Return the class platform template for the class files.
     *
     * @access public
     * @return string The class platform template.
     */
    public function getClassPlatformTemplate($platform) {
        if ($this->platformTemplate) return $this->platformTemplate;
        $template= <<<EOD
<?php
require_once (dirname(__DIR__) . '/[+class-lowercase+].class.php');
class [+class+]_$platform extends [+class+] {}
EOD;
        return $template;
    }

    /**
     * Gets the map header template.
     *
     * @access public
     * @return string The map header template.
     */
    public function getMapHeader() {
        if ($this->mapHeader) return $this->mapHeader;
        $header= <<<EOD
<?php
EOD;
        return $header;
    }

    /**
     * Gets the map footer template.
     *
     * @access public
     * @return string The map footer template.
     */
    public function getMapFooter() {
        if ($this->mapFooter) return $this->mapFooter;
        return '';
    }


    /**
     * Gets the meta template.
     *
     * @access public
     * @return string The meta template.
     */
    public function getMetaTemplate() {
        if ($this->metaTemplate) return $this->metaTemplate;
        $tpl= <<<EOD
<?php
\n\$xpdo_meta_map = [+map+];
EOD;
        return $tpl;
    }
}
