<?php

namespace Vich\UploaderBundle\Mapping;

use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Vich\UploaderBundle\Naming\DirectoryNamerInterface;
use Vich\UploaderBundle\Naming\NamerInterface;

/**
 * PropertyMapping.
 *
 * @author Dustin Dobervich <ddobervich@gmail.com>
 */
class PropertyMapping
{
    /**
     * @var NamerInterface
     */
    protected $namer;

    /**
     * @var DirectoryNamerInterface
     */
    protected $directoryNamer;

    /**
     * @var array
     */
    protected $mapping;

    /**
     * @var string
     */
    protected $mappingName;

    /**
     * @var string[]
     */
    protected $propertyPaths = [
        'file' => null,
        'name' => null,
        'size' => null,
        'mimeType' => null,
        'originalName' => null,
        'dimensions' => null,
    ];

    /**
     * @var PropertyAccessor
     */
    protected $accessor;

    /**
     * @param string         $filePropertyPath     The path to the "file" property
     * @param string         $fileNamePropertyPath The path to the "filename" property
     * @param array|string[] $propertyPaths        The paths to other properties
     */
    public function __construct(string $filePropertyPath, string $fileNamePropertyPath, array $propertyPaths = [])
    {
        $this->propertyPaths = array_merge(
            $this->propertyPaths,
            ['file' => $filePropertyPath, 'name' => $fileNamePropertyPath],
            $propertyPaths
        );
    }

    /**
     * Gets the file property value for the given object.
     *
     * @param object $obj The object
     *
     * @return File|null The file
     *
     * @throws \InvalidArgumentException
     */
    public function getFile($obj): ?File
    {
        return $this->readProperty($obj, 'file');
    }

    /**
     * Modifies the file property value for the given object.
     *
     * @param object $obj  The object
     * @param File   $file The new file
     *
     * @throws \InvalidArgumentException
     * @throws \TypeError
     */
    public function setFile($obj, File $file): void
    {
        $this->writeProperty($obj, 'file', $file);
    }

    /**
     * Gets the fileName property of the given object.
     *
     * @param object $obj The object
     *
     * @return string The filename
     *
     * @throws \InvalidArgumentException
     */
    public function getFileName($obj): ?string
    {
        return $this->readProperty($obj, 'name');
    }

    /**
     * Modifies the fileName property of the given object.
     *
     * @param object $obj   The object
     * @param string $value
     *
     * @throws \InvalidArgumentException
     * @throws \TypeError
     */
    public function setFileName($obj, string $value): void
    {
        $this->writeProperty($obj, 'name', $value);
    }

    /**
     * Removes value for each file-related property of the given object.
     *
     * @param object $obj The object
     *
     * @throws \InvalidArgumentException
     * @throws \TypeError
     */
    public function erase($obj): void
    {
        foreach (['name', 'size', 'mimeType', 'originalName', 'dimensions'] as $property) {
            $this->writeProperty($obj, $property, null);
        }
    }

    /**
     * Reads property of the given object.
     *
     * @internal
     *
     * @param object $obj      The object from which read
     * @param string $property The property to read
     *
     * @return mixed
     *
     * @throws \InvalidArgumentException
     */
    public function readProperty($obj, $property)
    {
        if (!array_key_exists($property, $this->propertyPaths)) {
            throw new \InvalidArgumentException(sprintf('Unknown property %s', $property));
        }

        if (!$this->propertyPaths[$property]) {
            // not configured
            return null;
        }

        $propertyPath = $this->fixPropertyPath($obj, $this->propertyPaths[$property]);

        return $this->getAccessor()->getValue($obj, $propertyPath);
    }

    /**
     * Modifies property of the given object.
     *
     * @internal
     *
     * @param object $obj      The object to which write
     * @param string $property The property to write
     * @param mixed  $value    The value which should be written
     *
     * @throws \InvalidArgumentException
     * @throws \TypeError
     */
    public function writeProperty($obj, string $property, $value): void
    {
        if (!array_key_exists($property, $this->propertyPaths)) {
            throw new \InvalidArgumentException(sprintf('Unknown property %s', $property));
        }

        if (!$this->propertyPaths[$property]) {
            // not configured
            return;
        }

        $propertyPath = $this->fixPropertyPath($obj, $this->propertyPaths[$property]);
        $this->getAccessor()->setValue($obj, $propertyPath, $value);
    }

    /**
     * Gets the configured file property name.
     *
     * @return string The name
     */
    public function getFilePropertyName(): string
    {
        return $this->propertyPaths['file'];
    }

    /**
     * Gets the configured filename property name.
     *
     * @return string The name
     */
    public function getFileNamePropertyName(): string
    {
        return $this->propertyPaths['name'];
    }

    /**
     * Gets the configured namer.
     *
     * @return NamerInterface|null The namer
     */
    public function getNamer(): ?NamerInterface
    {
        return $this->namer;
    }

    /**
     * Sets the namer.
     *
     * @param NamerInterface $namer The namer
     */
    public function setNamer(NamerInterface $namer): void
    {
        $this->namer = $namer;
    }

    /**
     * Determines if the mapping has a custom namer configured.
     *
     * @return bool True if has namer, false otherwise
     */
    public function hasNamer(): bool
    {
        return null !== $this->namer;
    }

    /**
     * Gets the configured directory namer.
     *
     * @return DirectoryNamerInterface|null The directory namer
     */
    public function getDirectoryNamer(): ?DirectoryNamerInterface
    {
        return $this->directoryNamer;
    }

    /**
     * Sets the directory namer.
     *
     * @param DirectoryNamerInterface $directoryNamer The directory namer
     */
    public function setDirectoryNamer(DirectoryNamerInterface $directoryNamer): void
    {
        $this->directoryNamer = $directoryNamer;
    }

    /**
     * Determines if the mapping has a custom directory namer configured.
     *
     * @return bool True if has directory namer, false otherwise
     */
    public function hasDirectoryNamer(): bool
    {
        return null !== $this->directoryNamer;
    }

    /**
     * Sets the configured configuration mapping.
     *
     * @param array $mapping The mapping;
     */
    public function setMapping(array $mapping): void
    {
        $this->mapping = $mapping;
    }

    /**
     * Gets the configured configuration mapping name.
     *
     * @return string The mapping name
     */
    public function getMappingName(): string
    {
        return $this->mappingName;
    }

    /**
     * Sets the configured configuration mapping name.
     *
     * @param string $mappingName The mapping name
     */
    public function setMappingName($mappingName): void
    {
        $this->mappingName = $mappingName;
    }

    /**
     * Gets the upload name for a given file (uses The file namers).
     *
     * @param object $obj
     *
     * @return string The upload name
     */
    public function getUploadName($obj): string
    {
        if (!$this->hasNamer()) {
            return $this->getFile($obj)->getClientOriginalName();
        }

        return $this->getNamer()->name($obj, $this);
    }

    /**
     * Gets the upload directory for a given file (uses the directory namers).
     *
     * @param object $obj
     *
     * @return string|null The upload directory
     */
    public function getUploadDir($obj): ?string
    {
        if (!$this->hasDirectoryNamer()) {
            return '';
        }

        $dir = $this->getDirectoryNamer()->directoryName($obj, $this);

        // strip the trailing directory separator if needed
        $dir = $dir ? rtrim($dir, '/\\') : $dir;

        return $dir;
    }

    /**
     * Gets the base upload directory.
     *
     * @return string The configured upload directory
     */
    public function getUploadDestination(): string
    {
        return $this->mapping['upload_destination'];
    }

    /**
     * Get uri prefix.
     *
     * @return string
     */
    public function getUriPrefix(): string
    {
        return $this->mapping['uri_prefix'];
    }

    /**
     * Fixes a given propertyPath to make it usable both with arrays and
     * objects.
     * Ie: if the given object is in fact an array, the property path must
     * look like [myPath].
     *
     * @param object|array $object       The object to inspect
     * @param string       $propertyPath The property path to fix
     *
     * @return string The fixed property path
     */
    protected function fixPropertyPath($object, string $propertyPath): string
    {
        if (!is_array($object)) {
            return $propertyPath;
        }

        return '[' === $propertyPath[0] ? $propertyPath : sprintf('[%s]', $propertyPath);
    }

    protected function getAccessor(): PropertyAccessor
    {
        //TODO: reuse original property accessor from forms
        if (null !== $this->accessor) {
            return $this->accessor;
        }

        return $this->accessor = PropertyAccess::createPropertyAccessor();
    }
}
