<?php

namespace Ignite\Core\Files;

use Ignite\Core\Contracts\Files\FileProcessor;
use Ignite\Core\Contracts\Files\FileTransmitter as FileTransmitterContract;
use Ignite\Core\Exceptions\Files\SourceFileNotFound;
use Ignite\Core\Files\Processors\AdhocFileProcessor;

class FileTransmitter implements FileTransmitterContract
{
    /**
     * Where we'll pull the file from, or how we'll generate it.
     *
     * @var \Ignite\Core\Contracts\Files\FileGenerator|File
     */
    protected $source;

    /**
     * Where we'll put the file (if none, we'll use a TemporaryFile).
     *
     * @var File|null
     */
    protected $destination = null;

    /**
     * Where we've determined the source file will come from (if generating, this is after it has been generated).
     *
     * @var File|TemporaryFile
     */
    protected $resolvedSource;

    /**
     * Where we've determined the file will be placed. If we weren't given a destination, this will be a temporary file.
     *
     * @var File|TemporaryFile
     */
    protected $resolvedDestination;

    /**
     * The source after we've run the preprocessors on it.
     * This will be what we copy to the destination before post-processing.
     *
     * @var File|TemporaryFile
     */
    protected $preProcessedSource;

    /**
     * The source file after we've run the post processors on it.
     *
     * This can be, but doesn't have to be, the same as the resolvedSource. See note on $postProcessedDestination.
     *
     * @var File|TemporaryFile
     */
    protected $postProcessedSource;

    /**
     * The destination file after we've run the post processors on it.
     *
     * If no post processors are run this will be the resolvedDestination file. This doesn't have to be the
     * resolvedDestination file, however. Post processors can do anything to a destination file including leaving it
     * as is and working with a copy of it or moving it to a different location. resolvedDestination is just where it
     * will land when transmitted from the pre-processed source.
     *
     * @var File|TemporaryFile
     */
    protected $postProcessedDestination;

    /**
     * @see \Ignite\Core\Contracts\Files\FileTransmitter::addPreProcessor()
     *
     * @var array<FileProcessor>
     */
    protected $preProcessors;

    /**
     * @see \Ignite\Core\Contracts\Files\FileTransmitter::addPostProcessor()
     *
     * @var array{source: array<FileProcessor>, destination: array<FileProcessor>}
     */
    protected $postProcessors = ['source' => [], 'destination' => []];

    /**
     * {@inheritdoc}
     */
    public function source($file): FileTransmitterContract
    {
        $this->source = $file;
        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function destination(File $file): FileTransmitterContract
    {
        $this->destination = $file;
        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addPreProcessor($fileProcessor): FileTransmitterContract
    {
        $this->preProcessors[] = $fileProcessor instanceof FileProcessor
            ? $fileProcessor
            : new AdhocFileProcessor($fileProcessor);

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addPostProcessor(string $type, $fileProcessor): FileTransmitterContract
    {
        if (! array_key_exists($type, $this->postProcessors)) {
            throw new \InvalidArgumentException(
                "Invalid post processor type given: $type. Must be one of: ".
                join(', ', array_keys($this->postProcessors))
            );
        }

        $this->postProcessors[$type][] = $fileProcessor instanceof FileProcessor
            ? $fileProcessor
            : new AdhocFileProcessor($fileProcessor);

        return $this;
    }

    /**
     * {@inheritdoc}
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    public function transmit(): File
    {
        $this->beforeTransmission();

        if (! $this->preProcessedSource->copyToFile($this->resolvedDestination)) {
            throw new \RuntimeException(
                "Unable to copy file from ".
                "{$this->preProcessedSource->getRelativePath()} ({$this->preProcessedSource->getDisk()}) to ".
                "{$this->resolvedDestination->getRelativePath()} ({$this->resolvedDestination->getDisk()})"
            );
        }

        $this->afterTransmission();

        return $this->resolvedDestination;
    }

    /**
     * Do things before the transmission starts.
     *
     * @return void
     * @throws SourceFileNotFound
     * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
     */
    protected function beforeTransmission(): void
    {
        $this->resolveSource();
        $this->resolveDestination();
        $this->preProcessedSource = empty($this->preProcessors)
            ? $this->resolvedSource
            : $this->runFileProcessors($this->resolvedSource->copyToTemporary(), $this->preProcessors);
    }

    /**
     * Do things after the transmission is finished.
     *
     * @return void
     */
    protected function afterTransmission(): void
    {
        $this->postProcessedSource = $this->runFileProcessors(
            $this->resolvedSource,
            $this->postProcessors['source']
        );

        $this->postProcessedDestination = $this->runFileProcessors(
            $this->resolvedDestination,
            $this->postProcessors['destination']
        );
    }

    /**
     * Resolve the source file.
     *
     * @return File
     * @throws SourceFileNotFound
     */
    protected function resolveSource(): File
    {
        if (! isset($this->source)) {
            throw new \BadMethodCallException('Unable to transmit file. Source file has not been set.');
        }

        $this->resolvedSource = $this->source instanceof FileGenerator
            ? $this->source->destination(TemporaryFileBuilder::start()->make())->generate()
            : $this->source;

        if (! $this->resolvedSource instanceof File || ! $this->resolvedSource->exists()) {
            throw new SourceFileNotFound(
                'Could not find source file for: '.static::class.'.'.
                ($this->resolvedSource instanceof File
                    ? " Expected at `{$this->resolvedSource->getRelativePath()}` on `{$this->resolvedSource->getDisk()}`."
                    : '')
            );
        }

        return $this->resolvedSource;
    }

    /**
     * Determine where we will transmit the source file to.
     *
     * @return File|TemporaryFile
     */
    protected function resolveDestination()
    {
        return $this->resolvedDestination = $this->destination ?? TemporaryFileBuilder::start()->prefix($this)->make();
    }

    /**
     * Run the given file processors on the given file.
     *
     * @param File $file This should be the appropriate file for the given type.
     * @param array<FileProcessor> $fileProcessors
     * @return File
     */
    protected function runFileProcessors(File $file, array $fileProcessors): File
    {
        foreach ($fileProcessors as $postProcessor) {
            $file = $postProcessor->run($file);
        }

        return $file;
    }
}
