<?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\Archive\Archive;
use Alchemy\Zippy\Archive\Member;
use Alchemy\Zippy\Exception\RuntimeException;
use Alchemy\Zippy\Exception\InvalidArgumentException;
use Alchemy\Zippy\Resource\Resource as ZippyResource;
use Symfony\Component\Process\Exception\ExceptionInterface as ProcessException;

abstract class AbstractTarAdapter extends AbstractBinaryAdapter
{
    /**
     * @inheritdoc
     */
    protected function doCreate($path, $files, $recursive)
    {
        return $this->doTarCreate($this->getLocalOptions(), $path, $files, $recursive);
    }

    /**
     * @inheritdoc
     */
    protected function doListMembers(ResourceInterface $resource)
    {
        return $this->doTarListMembers($this->getLocalOptions(), $resource);
    }

    /**
     * @inheritdoc
     */
    protected function doAdd(ResourceInterface $resource, $files, $recursive)
    {
        return $this->doTarAdd($this->getLocalOptions(), $resource, $files, $recursive);
    }

    /**
     * @inheritdoc
     */
    protected function doRemove(ResourceInterface $resource, $files)
    {
        return $this->doTarRemove($this->getLocalOptions(), $resource, $files);
    }

    /**
     * @inheritdoc
     */
    protected function doExtractMembers(ResourceInterface $resource, $members, $to, $overwrite = false)
    {
        return $this->doTarExtractMembers($this->getLocalOptions(), $resource, $members, $to, $overwrite);
    }

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

    /**
     * @inheritdoc
     */
    protected function doGetInflatorVersion()
    {
        $process = $this
            ->inflator
            ->create()
            ->add('--version')
            ->getProcess();

        $process->run();

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(), $process->getErrorOutput()
            ));
        }

        return $this->parser->parseInflatorVersion($process->getOutput() ?: '');
    }

    /**
     * @inheritdoc
     */
    protected function doGetDeflatorVersion()
    {
        return $this->getInflatorVersion();
    }

    protected function doTarCreate($options, $path, $files = null, $recursive = true)
    {
        $files = (array) $files;

        $builder = $this
            ->inflator
            ->create();

        if (!$recursive) {
            $builder->add('--no-recursion');
        }

        $builder->add('-c');

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        if (0 === count($files)) {
            $nullFile = defined('PHP_WINDOWS_VERSION_BUILD') ? 'NUL' : '/dev/null';

            $builder->add('-f');
            $builder->add($path);
            $builder->add('-T');
            $builder->add($nullFile);

            $process = $builder->getProcess();
            $process->run();

        } else {

            $builder->add(sprintf('--file=%s', $path));

            if (!$recursive) {
                $builder->add('--no-recursion');
            }

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

            $builder->setWorkingDirectory($collection->getContext());

            $collection->forAll(function($i, ZippyResource $resource) use ($builder) {
                return $builder->add($resource->getTarget());
            });

            $process = $builder->getProcess();

            try {
                $process->run();
            } catch (ProcessException $e) {
                $this->manager->cleanup($collection);
                throw $e;
            }

            $this->manager->cleanup($collection);
        }

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

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

    protected function doTarListMembers($options, ResourceInterface $resource)
    {
        $builder = $this
            ->inflator
            ->create();

        foreach ($this->getListMembersOptions() as $option) {
            $builder->add($option);
        }

        $builder
            ->add('--list')
            ->add('-v')
            ->add(sprintf('--file=%s', $resource->getResource()));

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        $process = $builder->getProcess();
        $process->run();

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

        $members = array();

        foreach ($this->parser->parseFileListing($process->getOutput() ?: '') as $member) {
            $members[] = new Member(
                    $resource,
                    $this,
                    $member['location'],
                    $member['size'],
                    $member['mtime'],
                    $member['is_dir']
            );
        }

        return $members;
    }

    protected function doTarAdd($options, ResourceInterface $resource, $files, $recursive = true)
    {
        $files = (array) $files;

        $builder = $this
            ->inflator
            ->create();

        if (!$recursive) {
            $builder->add('--no-recursion');
        }

        $builder
            ->add('--append')
            ->add(sprintf('--file=%s', $resource->getResource()));

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        // there will be an issue if the file starts with a dash
        // see --add-file=FILE
        $collection = $this->manager->handle(getcwd(), $files);

        $builder->setWorkingDirectory($collection->getContext());

        $collection->forAll(function($i, ZippyResource $resource) use ($builder) {
            return $builder->add($resource->getTarget());
        });

        $process = $builder->getProcess();

        try {
            $process->run();
        } catch (ProcessException $e) {
            $this->manager->cleanup($collection);
            throw $e;
        }

        $this->manager->cleanup($collection);

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

        return $files;
    }

    protected function doTarRemove($options, ResourceInterface $resource, $files)
    {
        $files = (array) $files;

        $builder = $this
            ->inflator
            ->create();

        $builder
            ->add('--delete')
            ->add(sprintf('--file=%s', $resource->getResource()));

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        if (!$this->addBuilderFileArgument($files, $builder)) {
            throw new InvalidArgumentException('Invalid files');
        }

        $process = $builder->getProcess();

        $process->run();

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

        return $files;
    }

    protected function doTarExtract($options, ResourceInterface $resource, $to = null)
    {
        if (null !== $to && !is_dir($to)) {
            throw new InvalidArgumentException(sprintf("%s is not a directory", $to));
        }

        $builder = $this
            ->inflator
            ->create();

        $builder
            ->add('--extract')
            ->add(sprintf('--file=%s', $resource->getResource()));

        foreach ($this->getExtractOptions() as $option) {
            $builder
                ->add($option);
        }

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        if (null !== $to) {
            $builder
                ->add('--directory')
                ->add($to);
        }

        $process = $builder->getProcess();

        $process->run();

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

        return new \SplFileInfo($to ?: $resource->getResource());
    }

    /**
     * @param array             $options
     * @param ResourceInterface $resource
     * @param array             $members
     * @param string            $to
     * @param bool              $overwrite
     *
     * @return array
     */
    protected function doTarExtractMembers($options, ResourceInterface $resource, $members, $to = null, $overwrite = false)
    {
        if (null !== $to && !is_dir($to)) {
            throw new InvalidArgumentException(sprintf("%s is not a directory", $to));
        }

        $members = (array) $members;

        $builder = $this
            ->inflator
            ->create();

        if ($overwrite == false) {
            $builder->add('-k');
        }

        $builder
            ->add('--extract')
            ->add(sprintf('--file=%s', $resource->getResource()));

        foreach ($this->getExtractMembersOptions() as $option) {
            $builder
                ->add($option);
        }

        foreach ((array) $options as $option) {
            $builder->add((string) $option);
        }

        if (null !== $to) {
            $builder
                ->add('--directory')
                ->add($to);
        }

        if (!$this->addBuilderFileArgument($members, $builder)) {
            throw new InvalidArgumentException('Invalid files');
        }

        $process = $builder->getProcess();

        $process->run();

        if (!$process->isSuccessful()) {
            throw new RuntimeException(sprintf(
                'Unable to execute the following command %s {output: %s}',
                $process->getCommandLine(),
                $process->getErrorOutput()
            ));
        }

        return $members;
    }

    /**
     * Returns an array of option for the listMembers command
     *
     * @return array
     */
    abstract protected function getListMembersOptions();

    /**
     * Returns an array of option for the extract command
     *
     * @return array
     */
    abstract protected function getExtractOptions();

    /**
     * Returns an array of option for the extractMembers command
     *
     * @return array
     */
    abstract protected function getExtractMembersOptions();

    /**
     * Gets adapter specific additional options
     *
     * @return array
     */
    abstract protected function getLocalOptions();
}
