<?php

namespace League\Flysystem\ZipArchive;

use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Adapter\Polyfill\NotSupportingVisibilityTrait;
use League\Flysystem\Adapter\Polyfill\StreamedCopyTrait;
use League\Flysystem\Adapter\Polyfill\StreamedWritingTrait;
use League\Flysystem\Config;
use League\Flysystem\Util;
use LogicException;
use ZipArchive;

class ZipArchiveAdapter extends AbstractAdapter
{
    use StreamedWritingTrait;
    use StreamedCopyTrait;
    use NotSupportingVisibilityTrait;

    /**
     * @var array
     */
    protected static $resultMap = [
        'size'  => 'size',
        'mtime' => 'timestamp',
        'name'  => 'path',
    ];

    /**
     * @var ZipArchive
     */
    protected $archive;

    /**
     * @param             $location
     * @param ZipArchive  $archive
     * @param string|null $prefix
     */
    public function __construct($location, ZipArchive $archive = null, $prefix = null)
    {
        $this->setArchive($archive ?: new ZipArchive());
        $this->openArchive($location);
        $this->setPathPrefix($prefix);
    }

    /**
     * Re-open an archive to ensure persistence.
     */
    protected function reopenArchive()
    {
        $path = $this->archive->filename;
        $this->archive->close();
        $this->openArchive($path);
    }

    /**
     * ZipArchive setter.
     *
     * @param ZipArchive $archive
     */
    public function setArchive(ZipArchive $archive)
    {
        $this->archive = $archive;
    }

    /**
     * Get the used ZipArchive.
     *
     * @return ZipArchive
     */
    public function getArchive()
    {
        return $this->archive;
    }

    /**
     * Open a zip file.
     *
     * @param $location
     */
    public function openArchive($location)
    {
        $location = str_replace('/', DIRECTORY_SEPARATOR, $location);

        if (($response = $this->archive->open($location, ZipArchive::CREATE)) !== true) {
            throw new LogicException('Could not open zip archive at:'.$location.', error: '.$response);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function write($path, $contents, Config $config)
    {
        $location = $this->applyPathPrefix($path);
        $dirname = Util::dirname($path);

        if (! empty($dirname) && ! $this->has($dirname)) {
            $this->createDir($dirname, $config);
        }

        if (! $this->archive->addFromString($location, $contents)) {
            return false;
        }

        $result = compact('path', 'contents');

        if ($config && $config->get('visibility')) {
            throw new LogicException(get_class($this).' does not support visibility settings.');
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function update($path, $contents, Config $config)
    {
        $this->delete($path);

        return $this->write($path, $contents, $config);
    }

    /**
     * {@inheritdoc}
     */
    public function rename($path, $newpath)
    {
        $source = $this->applyPathPrefix($path);
        $destination = $this->applyPathPrefix($newpath);

        return $this->archive->renameName($source, $destination);
    }

    /**
     * {@inheritdoc}
     */
    public function delete($path)
    {
        $location = $this->applyPathPrefix($path);

        return $this->archive->deleteName($location);
    }

    /**
     * {@inheritdoc}
     */
    public function deleteDir($dirname)
    {
        // This is needed to ensure the right number of
        // files are set to the $numFiles property.
        $this->reopenArchive();

        $location = $this->applyPathPrefix($dirname);
        $path = Util::normalizePrefix($location, '/');
        $length = strlen($path);

        for ($i = 0; $i < $this->archive->numFiles; $i++) {
            $info = $this->archive->statIndex($i);

            if (substr($info['name'], 0, $length) === $path) {
                $this->archive->deleteIndex($i);
            }
        }

        return $this->archive->deleteName($dirname);
    }

    /**
     * {@inheritdoc}
     */
    public function createDir($dirname, Config $config)
    {
        if (! $this->has($dirname)) {
            $location = $this->applyPathPrefix($dirname);
            $this->archive->addEmptyDir($location);
        }

        return ['path' => $dirname];
    }

    /**
     * {@inheritdoc}
     */
    public function has($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * {@inheritdoc}
     */
    public function read($path)
    {
        $this->reopenArchive();
        $location = $this->applyPathPrefix($path);

        if (! $contents = $this->archive->getFromName($location)) {
            return false;
        }

        return compact('contents');
    }

    /**
     * {@inheritdoc}
     */
    public function readStream($path)
    {
        $this->reopenArchive();
        $location = $this->applyPathPrefix($path);

        if (! $stream = $this->archive->getStream($location)) {
            return false;
        }

        return compact('stream');
    }

    /**
     * {@inheritdoc}
     */
    public function listContents($dirname = '', $recursive = false)
    {
        $result = [];

        // This is needed to ensure the right number of
        // files are set to the $numFiles property.
        $this->reopenArchive();

        for ($i = 0; $i < $this->archive->numFiles; $i++) {
            if ($info = $this->archive->statIndex($i)) {
                $result[] = $info;
            }
        }

        $pathPrefix = $this->getPathPrefix();
        $prefixLength = strlen($pathPrefix);

        return array_filter(array_map(function ($item) use ($pathPrefix, $prefixLength) {
            if ($pathPrefix && (substr($item['name'], 0, $prefixLength) !== $pathPrefix || $item['name'] === $pathPrefix)) {
                return false;
            }

            return $this->normalizeObject($item);
        }, $result));
    }

    /**
     * {@inheritdoc}
     */
    public function getMetadata($path)
    {
        $location = $this->applyPathPrefix($path);

        if (! $info = $this->archive->statName($location)) {

            // Check if $location is a directory.
            if (! $info = $this->archive->statName($location . '/')) {
                return false;
            }
        }

        return $this->normalizeObject($info);
    }

    /**
     * Normalize a zip response array.
     *
     * @param array $object
     *
     * @return array
     */
    protected function normalizeObject(array $object)
    {
        if (substr($object['name'], -1) === '/') {
            return [
                'path' => $this->removePathPrefix(trim($object['name'], '/')),
                'type' => 'dir',
            ];
        }

        $result = ['type' => 'file'];
        $normalised = Util::map($object, static::$resultMap);
        $normalised['path'] = $this->removePathPrefix($normalised['path']);

        return array_merge($result, $normalised);
    }

    /**
     * {@inheritdoc}
     */
    public function getSize($path)
    {
        return $this->getMetadata($path);
    }

    /**
     * {@inheritdoc}
     */
    public function getMimetype($path)
    {
        if (! $data = $this->read($path)) {
            return false;
        }

        $data['mimetype'] = Util::guessMimeType($path, $data['contents']);

        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function getTimestamp($path)
    {
        return $this->getMetadata($path);
    }
}
