<?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
*/

/**
 * Abstract class that represents an artifact within a transportable package.
 *
 * @package xpdo
 * @subpackage transport
 */

/**
 * Represents an individual artifact within an {@link xPDOTransport} package.
 *
 * Extend this abstract class to provide custom xPDOVehicle behavior for various kinds of artifacts
 * (e.g. objects, xPDOObjects, files, database schemas, etc.).
 *
 * @package xpdo
 * @subpackage transport
 *
 * @abstract
 */
abstract class xPDOVehicle {
    /**
     * Represents the artifact and related attributes stored in the vehicle.
     * @var array
     */
    public $payload = array ();

    public $class = 'xPDOVehicle';

    /**
     * Build a manifest entry to be registered in a transport for this vehicle.
     *
     * @param xPDOTransport &$transport The xPDOTransport instance to register
     * the vehicle into.
     * @return array An array of vehicle attributes that will be registered into
     * an xPDOTransport manifest.
     */
    public function register(& $transport) {
        $vPackage = isset($this->payload['vehicle_package']) ? $this->payload['vehicle_package'] : 'transport';
        $vClass = isset($this->payload['vehicle_class']) ? $this->payload['vehicle_class'] : $this->class;
        $class = isset($this->payload['class']) ? $this->payload['class'] : $vClass;
        $entry = array(
            'vehicle_package' => $vPackage,
            'vehicle_class' => $vClass,
            'class' => $class,
            'guid' => $this->payload['guid'],
            'native_key' => array_key_exists('native_key', $this->payload) ? $this->payload['native_key'] : null,
            'filename' => $class . '/' . $this->payload['filename'],
        );
        if (isset($this->payload['namespace'])) {
            $entry['namespace'] = $this->payload['namespace'];
        }
        return $entry;
    }

    /**
     * Retrieve an artifact represented in this vehicle.
     *
     * By default, this method simply returns the raw payload merged with the
     * provided options, but you can optionally provide a payload element
     * specifically on which to operate as well as override the method in
     * derivatives to further transform the returned artifact.
     *
     * @param xPDOTransport $transport The transport package containing this
     * vehicle.
     * @param array $options Options that apply to the artifact or retrieval
     * process.
     * @param array $element An optional payload element representing a specific
     * part of the artifact to operate on.  If not specified, the root element
     * of the payload is used.
     */
    public function get(& $transport, $options = array (), $element = null) {
        $artifact = null;
        if ($element === null) $element = $this->payload;
        $artifact = array_merge($options, $element);
        return $artifact;
    }

    /**
     * Install the vehicle artifact into a transport host.
     *
     * @abstract Implement this in a derivative to make an installable vehicle.
     * @param xPDOTransport &$transport A reference to the transport.
     * @param array $options An array of options for altering the installation
     * of the artifact.
     * @return boolean True if the installation of the vehicle artifact was
     * successful.
     */
    abstract public function install(& $transport, $options);

    /**
     * Uninstalls the vehicle artifact from a transport host.
     *
     * @abstract Implement this in a derivative to make an uninstallable
     * vehicle.
     * @param xPDOTransport &$transport A reference to the transport.
     * @param array $options An array of options for altering the uninstallation
     * of the artifact.
     */
    abstract public function uninstall(& $transport, $options);

    /**
     * Resolve any dependencies of the artifact represented in this vehicle.
     *
     * @param xPDOTransport &$transport A reference to the xPDOTransport in
     * which this vehicle is stored.
     * @param mixed &$object An object reference to resolve dependencies for.
     * Use this to make the artifact or other important data available to the
     * resolver scripts.
     * @param array $options Additional options for the resolution process.
     * @return boolean Indicates if the resolution was successful.
     */
    public function resolve(& $transport, & $object, $options = array ()) {
        $resolved = false;
        if (isset ($this->payload['resolve'])) {
            foreach ($this->payload['resolve'] as $rKey => $r) {
                $type = $r['type'];
                $body = $r['body'];
                $preExistingMode = xPDOTransport::PRESERVE_PREEXISTING;
                if (!empty ($options[xPDOTransport::PREEXISTING_MODE])) {
                    $preExistingMode = intval($options[xPDOTransport::PREEXISTING_MODE]);
                }
                switch ($type) {
                    case 'file' :
                        if (isset ($options[xPDOTransport::RESOLVE_FILES]) && !$options[xPDOTransport::RESOLVE_FILES]) {
                            $resolved = true;
                            break;
                        }
                        if ($transport->xpdo->getDebug() === true) {
                            $transport->xpdo->log(xPDO::LOG_LEVEL_DEBUG, "Resolving transport files: " . print_r($this, true));
                        }
                        $fileMeta = $transport->xpdo->fromJSON($body, true);
                        $fileName = $fileMeta['name'];
                        $fileSource = $transport->path . $fileMeta['source'];
                        $fileTarget = eval ($fileMeta['target']);
                        $fileTargetPath = $fileTarget . $fileName;
                        $preservedArchive = $transport->path . $transport->signature . '/' . $this->payload['class'] . '/' . $this->payload['signature'] . '.' . $rKey . '.preserved.zip';
                        $cacheManager = $transport->xpdo->getCacheManager();
                        switch ($options[xPDOTransport::PACKAGE_ACTION]) {
                            case xPDOTransport::ACTION_UPGRADE:
                            case xPDOTransport::ACTION_INSTALL: // if package is installing
                                if ($cacheManager && file_exists($fileSource) && !empty ($fileTarget)) {
                                    $copied = array();
                                    if ($preExistingMode === xPDOTransport::PRESERVE_PREEXISTING && file_exists($fileTargetPath)) {
                                        $transport->xpdo->log(xPDO::LOG_LEVEL_INFO, "Attempting to preserve files at {$fileTargetPath} into archive {$preservedArchive}");
                                        $preserved = xPDOTransport::_pack($transport->xpdo, $preservedArchive, $fileTarget, $fileName);
                                    }
                                    if (is_dir($fileSource)) {
                                        $copied = $cacheManager->copyTree($fileSource, $fileTarget, array_merge($options, array('copy_return_file_stat' => true)));
                                    } elseif (is_file($fileSource)) {
                                        $copied = $cacheManager->copyFile($fileSource, $fileTarget, array_merge($options, array('copy_return_file_stat' => true)));
                                    }
                                    if (empty($copied)) {
                                        $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy {$fileSource} to {$fileTargetPath}");
                                    } else {
                                        if ($preExistingMode === xPDOTransport::PRESERVE_PREEXISTING && is_array($copied)) {
                                            foreach ($copied as $copiedFile => $stat) {
                                                if (isset($stat['overwritten'])) $transport->_preserved[$options['guid']]['files'][substr($copiedFile, strlen($fileTarget))]= $stat;
                                            }
                                        }
                                        $resolved = true;
                                    }
                                } else {
                                    $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy {$fileSource} to {$fileTargetPath}");
                                }
                                break;
                            case xPDOTransport::ACTION_UNINSTALL: /* if package is uninstalling
                                   user can override whether or not files from resolver are removed
                                   however default action is to remove */
                                if (!isset($options[xPDOTransport::RESOLVE_FILES_REMOVE]) || $options[xPDOTransport::RESOLVE_FILES_REMOVE] !== false) {
                                    $path = $fileTarget.$fileName;
                                    $transport->xpdo->log(xPDO::LOG_LEVEL_INFO,'Removing files in file resolver: '.$path);
                                    if ($cacheManager && file_exists($path)) {
                                        if (is_dir($path) && $cacheManager->deleteTree($path, array_merge(array('deleteTop' => true, 'skipDirs' => false, 'extensions' => array()), $options))) {
                                            $resolved = true;
                                        } elseif (is_file($path) && unlink($path)) {
                                            $resolved = true;
                                        } else {
                                            $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR,'Could not remove files from path: '.$path);
                                        }
                                    } else {
                                        $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR,'Could not find files to remove.');
                                    }
                                } else {
                                    /* action was chosen not to remove, send log message and continue */
                                    $transport->xpdo->log(xPDO::LOG_LEVEL_INFO,'Skipping removing of files according to vehicle attributes.');
                                    $resolved = true;
                                }
                                if ($preExistingMode === xPDOTransport::RESTORE_PREEXISTING && file_exists($preservedArchive)) {
                                    $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Attempting to restore files to {$fileTarget} from archive {$preservedArchive}");
                                    $unpackedResult = xPDOTransport::_unpack($transport->xpdo, $preservedArchive, $fileTarget);
                                    if ($unpackedResult > 0) {
                                        $resolved = true;
                                    } else {
                                        $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Error unpacking preserved files from archive {$preservedArchive}");
                                    }
                                }
                                break;
                        }
                        break;

                    case 'php' :
                        if (isset ($options[xPDOTransport::RESOLVE_PHP]) && !$options[xPDOTransport::RESOLVE_PHP]) {
                            break;
                        }
                        $fileMeta = $transport->xpdo->fromJSON($body, true);
                        $fileName = $fileMeta['name'];
                        $fileSource = $transport->path . $fileMeta['source'];
                        if (!$resolved = include ($fileSource)) {
                            $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOVehicle resolver failed: type php ({$fileSource})");
                        }
                        break;

                    default :
                        $transport->xpdo->log(xPDO::LOG_LEVEL_WARN, "xPDOVehicle does not support resolvers of type {$type}.");
                        break;
                }
            }
        } else {
            $resolved = true;
        }
        return $resolved;
    }

    /**
     * Validate any dependencies for the object represented in this vehicle.
     *
     * @param xPDOTransport &$transport A reference to the xPDOTransport in
     * which this vehicle is stored.
     * @param xPDOObject &$object An object reference to access during
     * validation.
     * @param array $options Additional options for the validation process.
     * @return boolean Indicating if the validation was successful.
     */
    public function validate(& $transport, & $object, $options = array ()) {
        $validated = true;
        if (isset ($this->payload['validate'])) {
            foreach ($this->payload['validate'] as $rKey => $r) {
                $type = $r['type'];
                $body = $r['body'];
                switch ($type) {
                    case 'php' :
//                        if (isset ($options[xPDOTransport::VALIDATE_PHP]) && !$options[xPDOTransport::VALIDATE_PHP]) {
//                            continue;
//                        }
                        $fileMeta = $transport->xpdo->fromJSON($body, true);
                        $fileName = $fileMeta['name'];
                        $fileSource = $transport->path . $fileMeta['source'];
                        if (!$validated = include ($fileSource)) {
                            if (!isset($fileMeta['silent_fail']) || !$fileMeta['silent_fail']) {
                                $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "xPDOVehicle validator failed: type php ({$fileSource})");
                            }
                        }
                        break;

                    default :
                        $transport->xpdo->log(xPDO::LOG_LEVEL_WARN, "xPDOVehicle does not support validators of type {$type}.");
                        break;
                }
            }
        } else {
            $validated = true;
        }
        return $validated;
    }

    /**
     * Put an artifact representation into this vehicle.
     *
     * @param xPDOTransport $transport The transport package hosting the
     * vehicle.
     * @param mixed &$object A reference to the artifact this vehicle will
     * represent.
     * @param array $attributes Additional attributes represented in the
     * vehicle.
     */
    public function put(& $transport, & $object, $attributes = array ()) {
        $this->payload = array_merge($this->payload, $attributes);
        if (!isset($this->payload['guid'])) {
            $this->payload['guid'] = md5(uniqid(rand(), true));
        }
        if (!isset ($this->payload['package'])) {
            if ($object instanceof xPDOObject) {
                $packageName = $object->_package;
            } else {
                $packageName = '';
            }
            $this->payload['package'] = $packageName;
        }
        if (!isset($this->payload['class'])) {
            $className = 'xPDOVehicle';
            if (is_object($object)) {
                if ($object instanceof xPDOObject) {
                    $className = $object->_class;
                } else {
                    $className = get_class($object);
                }
            }
            $this->payload['class'] = $className;
        }
        if (!isset($this->payload['signature'])) {
            $this->payload['signature'] = md5($this->payload['class'] . '_' . $this->payload['guid']);
        }
        if (!isset($this->payload['native_key'])) {
            $nativeKey = null;
            $nativeKeyAttr = isset($this->payload['native_key_attribute']) ? $this->payload['native_key_attribute'] : null;
            if (is_object($object)) {
                if ($object instanceof xPDOObject) {
                    $nativeKey = $object->getPrimaryKey();
                } elseif (!empty($nativeKeyAttr) && isset($object->$nativeKeyAttr)) {
                    $nativeKey = $object->$nativeKeyAttr;
                }
            } elseif (!empty($nativeKeyAttr) && isset($this->payload[$nativeKeyAttr])) {
                $nativeKey = $this->payload[$nativeKeyAttr];
            } else {
                $nativeKey = $this->payload['guid'];
            }
            $this->payload['native_key'] = $nativeKey;
        }
        if (isset ($attributes['validate'])) {
            $this->payload['validate'] = (array) $attributes['validate'];
        }
        if (isset ($attributes['resolve'])) {
            $this->payload['resolve'] = (array) $attributes['resolve'];
        }
        if (isset ($attributes['namespace'])) {
            if ($attributes['namespace'] instanceof xPDOObject) {
                $this->payload['namespace'] = $attributes['namespace']->get('name');
            } else {
                $this->payload['namespace'] = $attributes['namespace'];
            }
        }
    }

    /**
     * Store this xPDOVehicle instance into an xPDOTransport.
     *
     * @param xPDOTransport &$transport The transport to store the vehicle in.
     * @return boolean Indicates if the vehicle was stored in the transport.
     */
    public function store(& $transport) {
        $stored = false;
        $cacheManager = $transport->xpdo->getCacheManager();
        if ($cacheManager && !empty ($this->payload)) {
            $this->_compilePayload($transport);

            $content = '<?php return ';
            $content .= var_export($this->payload, true);
            $content .= ';';
            $this->payload['filename'] = $this->payload['signature'] . '.vehicle';
            $vFileName = $transport->path . $transport->signature . '/' . $this->payload['class'] . '/' . $this->payload['filename'];
            if (!($stored = $cacheManager->writeFile($vFileName, $content))) {
                $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Could not store vehicle to file ' . $vFileName);
            }
        }
        return $stored;
    }

    /**
     * Compile necessary resources in preparation for storing the vehicle.
     *
     * @access protected
     * @param xPDOTransport &$transport A reference to the transport the vehicle is being stored in.
     */
    protected function _compilePayload(& $transport) {
        $cacheManager = $transport->xpdo->getCacheManager();
        if ($cacheManager) {
            if (isset ($this->payload['resolve']) && is_array($this->payload['resolve'])) {
                foreach ($this->payload['resolve'] as $rKey => $r) {
                    $type = $r['type'];
                    $body = array ();
                    switch ($type) {
                        case 'file' :
                            $fileSource = $r['source'];
                            $body['source'] = $transport->signature . '/' . $this->payload['class'] . '/' . $this->payload['signature'] . '/' . $rKey . '/';
                            $fileTarget = $transport->path . $body['source'];
                            $body['target'] = $r['target'];
                            $fileName = isset ($r['name']) ? $r['name'] : basename($fileSource);
                            $body['name'] = $fileName;
                            if (!is_writable($fileTarget)) {
                                $cacheManager->writeTree($fileTarget);
                            }
                            if (file_exists($fileSource) && is_writable($fileTarget)) {
                                $copied = false;
                                if (is_dir($fileSource)) {
                                    $copied = $cacheManager->copyTree($fileSource, $fileTarget . $fileName);
                                }
                                elseif (is_file($fileSource)) {
                                    $copied = $cacheManager->copyFile($fileSource, $fileTarget . $fileName);
                                }
                                if (!$copied) {
                                    $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Could not copy file from {$fileSource} to {$fileTarget}{$fileName}");
                                    $body = null;
                                }
                            } else {
                                $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Source file {$fileSource} is missing or {$fileTarget} is not writable");
                                $body = null;
                            }
                            break;

                        case 'php' :
                            $fileSource = $r['source'];
                            $scriptName = basename($fileSource, '.php');
                            $body['source'] = $transport->signature . '/' . $this->payload['class'] . '/' . $this->payload['signature'] . '.' . $scriptName . '.resolver';
                            $fileTarget = $transport->path . $body['source'];
                            $body['name'] = $scriptName;
                            $body = array_merge($r, $body);
                            if (!$cacheManager->copyFile($fileSource, $fileTarget)) {
                                $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Source file {$fileSource} is missing or {$fileTarget} could not be written");
                            }
                            break;

                        default :
                            $transport->xpdo->log(xPDO::LOG_LEVEL_WARN, "xPDOVehicle does not support resolvers of type {$type}.");
                            break;
                    }
                    if ($body) {
                        $this->payload['resolve'][$rKey] = array (
                            'type' => $type,
                            'body' => $transport->xpdo->toJSON($body)
                        );
                    } else {
                        $this->payload['resolve'][$rKey] = null;
                    }
                }
            }

            if (isset($this->payload['validate']) && is_array($this->payload['validate'])) {
                foreach ($this->payload['validate'] as $vKey => $v) {
                    $type = $v['type'];
                    $body = array ();
                    switch ($type) {
                        case 'php' :
                            $fileSource = $v['source'];
                            $scriptName = basename($fileSource, '.php');
                            $body['source'] = $transport->signature . '/' . $this->payload['class'] . '/' . $this->payload['signature'] . '.' . $scriptName . '.validator';
                            $fileTarget = $transport->path . $body['source'];
                            $body['name'] = $scriptName;
                            $body = array_merge($v, $body);
                            if (!$cacheManager->copyFile($fileSource, $fileTarget)) {
                                $transport->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Source file {$fileSource} is missing or {$fileTarget} could not be written");
                            }
                            break;

                        default :
                            $transport->xpdo->log(xPDO::LOG_LEVEL_WARN, "xPDOVehicle does not support validators of type {$type}.");
                            break;
                    }
                    if ($body) {
                        $this->payload['validate'][$vKey] = array (
                            'type' => $type,
                            'body' => $transport->xpdo->toJSON($body)
                        );
                    } else {
                        $this->payload['validate'][$vKey] = null;
                    }
                }
            }
        }
    }
}
