<?php

/*
 * This file is part of Zippy.
 *
 * (c) Alchemy <info@alchemy.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Alchemy\Zippy\Adapter;

use Alchemy\Zippy\Adapter\Resource\ResourceInterface;
use Alchemy\Zippy\Adapter\Resource\ZipArchiveResource;
use Alchemy\Zippy\Adapter\VersionProbe\ZipExtensionVersionProbe;
use Alchemy\Zippy\Archive\Archive;
use Alchemy\Zippy\Archive\Member;
use Alchemy\Zippy\Exception\NotSupportedException;
use Alchemy\Zippy\Exception\RuntimeException;
use Alchemy\Zippy\Exception\InvalidArgumentException;
use Alchemy\Zippy\Resource\Resource as ZippyResource;
use Alchemy\Zippy\Resource\ResourceManager;

/**
 * ZipExtensionAdapter allows you to create and extract files from archives
 * using PHP Zip extension
 *
 * @see http://www.php.net/manual/en/book.zip.php
 */
class ZipExtensionAdapter extends AbstractAdapter
{
    private $errorCodesMapping = array(
        \ZipArchive::ER_EXISTS => "File already exists",
        \ZipArchive::ER_INCONS => "Zip archive inconsistent",
        \ZipArchive::ER_INVAL  => "Invalid argument",
        \ZipArchive::ER_MEMORY => "Malloc failure",
        \ZipArchive::ER_NOENT  => "No such file",
        \ZipArchive::ER_NOZIP  => "Not a zip archive",
        \ZipArchive::ER_OPEN   => "Can't open file",
        \ZipArchive::ER_READ   => "Read error",
        \ZipArchive::ER_SEEK   => "Seek error"
    );

    public function __construct(ResourceManager $manager)
    {
        parent::__construct($manager);
        $this->probe = new ZipExtensionVersionProbe();
    }

    /**
     * @inheritdoc
     */
    protected function doListMembers(ResourceInterface $resource)
    {
        $members = array();
        for ($i = 0; $i < $resource->getResource()->numFiles; $i++) {
            $stat = $resource->getResource()->statIndex($i);
            $members[] = new Member(
                $resource,
                $this,
                $stat['name'],
                $stat['size'],
                new \DateTime('@' . $stat['mtime']),
                0 === strlen($resource->getResource()->getFromIndex($i, 1))
            );
        }

        return $members;
    }

    /**
     * @inheritdoc
     */
    public static function getName()
    {
        return 'zip-extension';
    }

    /**
     * @inheritdoc
     */
    protected function doExtract(ResourceInterface $resource, $to)
    {
        return $this->extractMembers($resource, null, $to);
    }

    /**
     * @inheritdoc
     */
    protected function doExtractMembers(ResourceInterface $resource, $members, $to, $overwrite = false)
    {
        if (null === $to) {
            // if no destination is given, will extract to zip current folder
            $to = dirname(realpath($resource->getResource()->filename));
        }

        if (!is_dir($to)) {
            $resource->getResource()->close();
            throw new InvalidArgumentException(sprintf("%s is not a directory", $to));
        }

        if (!is_writable($to)) {
            $resource->getResource()->close();
            throw new InvalidArgumentException(sprintf("%s is not writable", $to));
        }

        if (null !== $members) {
            $membersTemp = (array) $members;
            if (empty($membersTemp)) {
                $resource->getResource()->close();

                throw new InvalidArgumentException("no members provided");
            }
            $members = array();
            // allows $members to be an array of strings or array of Members
            foreach ($membersTemp as $member) {
                if ($member instanceof Member) {
                    $member = $member->getLocation();
                }

                if ($resource->getResource()->locateName($member) === false) {
                    $resource->getResource()->close();

                    throw new InvalidArgumentException(sprintf('%s is not in the zip file', $member));
                }

                if ($overwrite == false) {
                    if (file_exists($member)) {
                        $resource->getResource()->close();

                        throw new RuntimeException('Target file ' . $member . ' already exists.');
                    }
                }

                $members[] = $member;
            }
        }

        if (!$resource->getResource()->extractTo($to, $members)) {
            $resource->getResource()->close();

            throw new InvalidArgumentException(sprintf('Unable to extract archive : %s', $resource->getResource()->getStatusString()));
        }

        return new \SplFileInfo($to);
    }

    /**
     * @inheritdoc
     */
    protected function doRemove(ResourceInterface $resource, $files)
    {
        $files = (array) $files;

        if (empty($files)) {
            throw new InvalidArgumentException("no files provided");
        }

        // either remove all files or none in case of error
        foreach ($files as $file) {
            if ($resource->getResource()->locateName($file) === false) {
                $resource->getResource()->unchangeAll();
                $resource->getResource()->close();

                throw new InvalidArgumentException(sprintf('%s is not in the zip file', $file));
            }
            if (!$resource->getResource()->deleteName($file)) {
                $resource->getResource()->unchangeAll();
                $resource->getResource()->close();

                throw new RuntimeException(sprintf('unable to remove %s', $file));
            }
        }
        $this->flush($resource->getResource());

        return $files;
    }

    /**
     * @inheritdoc
     */
    protected function doAdd(ResourceInterface $resource, $files, $recursive)
    {
        $files = (array) $files;
        if (empty($files)) {
            $resource->getResource()->close();
            throw new InvalidArgumentException("no files provided");
        }
        $this->addEntries($resource, $files, $recursive);

        return $files;
    }

    /**
     * @inheritdoc
     */
    protected function doCreate($path, $files, $recursive)
    {
        $files = (array) $files;

        if (empty($files)) {
            throw new NotSupportedException("Cannot create an empty zip");
        }

        $resource = $this->getResource($path, \ZipArchive::CREATE);
        $this->addEntries($resource, $files, $recursive);

        return new Archive($resource, $this, $this->manager);
    }

    /**
     * Returns a new instance of the invoked adapter
     *
     * @return AbstractAdapter
     *
     * @throws RuntimeException In case object could not be instanciated
     */
    public static function newInstance()
    {
        return new ZipExtensionAdapter(ResourceManager::create());
    }

    protected function createResource($path)
    {
        return $this->getResource($path, \ZipArchive::CHECKCONS);
    }

    private function getResource($path, $mode)
    {
        $zip = new \ZipArchive();
        $res = $zip->open($path, $mode);

        if ($res !== true) {
            throw new RuntimeException($this->errorCodesMapping[$res]);
        }

        return new ZipArchiveResource($zip);
    }

    private function addEntries(ResourceInterface $zipResource, array $files, $recursive)
    {
        $stack = new \SplStack();

        $error = null;
        $cwd = getcwd();
        $collection = $this->manager->handle($cwd, $files);

        $this->chdir($collection->getContext());

        $adapter = $this;

        try {
            $collection->forAll(function($i, ZippyResource $resource) use ($zipResource, $stack, $recursive, $adapter) {
                $adapter->checkReadability($zipResource->getResource(), $resource->getTarget());
                if (is_dir($resource->getTarget())) {
                    if ($recursive) {
                        $stack->push($resource->getTarget() . ((substr($resource->getTarget(), -1) === DIRECTORY_SEPARATOR) ? '' : DIRECTORY_SEPARATOR));
                    } else {
                        $adapter->addEmptyDir($zipResource->getResource(), $resource->getTarget());
                    }
                } else {
                    $adapter->addFileToZip($zipResource->getResource(), $resource->getTarget());
                }

                return true;
            });

            // recursively add dirs
            while (!$stack->isEmpty()) {
                $dir = $stack->pop();
                // removes . and ..
                $files = array_diff(scandir($dir), array(".", ".."));
                if (count($files) > 0) {
                    foreach ($files as $file) {
                        $file = $dir . $file;
                        $this->checkReadability($zipResource->getResource(), $file);
                        if (is_dir($file)) {
                            $stack->push($file . DIRECTORY_SEPARATOR);
                        } else {
                            $this->addFileToZip($zipResource->getResource(), $file);
                        }
                    }
                } else {
                    $this->addEmptyDir($zipResource->getResource(), $dir);
                }
            }
            $this->flush($zipResource->getResource());

            $this->manager->cleanup($collection);
        } catch (\Exception $e) {
            $error = $e;
        }

        $this->chdir($cwd);

        if ($error) {
            throw $error;
        }
    }

    /**
     * @info is public for PHP 5.3 compatibility, should be private
     *
     * @param \ZipArchive $zip
     * @param string      $file
     */
    public function checkReadability(\ZipArchive $zip, $file)
    {
        if (!is_readable($file)) {
            $zip->unchangeAll();
            $zip->close();

            throw new InvalidArgumentException(sprintf('could not read %s', $file));
        }
    }

    /**
     * @info is public for PHP 5.3 compatibility, should be private
     *
     * @param \ZipArchive $zip
     * @param string      $file
     */
    public function addFileToZip(\ZipArchive $zip, $file)
    {
        if (!$zip->addFile($file)) {
            $zip->unchangeAll();
            $zip->close();

            throw new RuntimeException(sprintf('unable to add %s to the zip file', $file));
        }
    }

    /**
     * @info is public for PHP 5.3 compatibility, should be private
     *
     * @param \ZipArchive $zip
     * @param string      $dir
     */
    public function addEmptyDir(\ZipArchive $zip, $dir)
    {
        if (!$zip->addEmptyDir($dir)) {
            $zip->unchangeAll();
            $zip->close();

            throw new RuntimeException(sprintf('unable to add %s to the zip file', $dir));
        }
    }

    /**
     * Flushes changes to the archive
     *
     * @param \ZipArchive $zip
     */
    private function flush(\ZipArchive $zip) // flush changes by reopening the file
    {
        $path = $zip->filename;
        $zip->close();
        $zip->open($path, \ZipArchive::CHECKCONS);
    }
}
