<?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
{
    /**
     * @var \Ignite\Core\Contracts\Files\FileGenerator|File
     */
    protected $source;

    /**
     * @var File|null
     */
    protected $destination = null;

    /**
     * @var File|TemporaryFile
     */
    protected $resolvedSource;

    /**
     * @var File|TemporaryFile
     */
    protected $preProcessedSource;

    /**
     * @var File|TemporaryFile
     */
    protected $resolvedDestination;

    /**
     * @var array<FileProcessor>
     */
    protected $preProcessors;

    /**
     * @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;
    }

    /**
     * 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();
        }

        return $this->resolvedSource;
    }

    /**
     * Resolve the destination file.
     *
     * @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;
    }

    /**
     * 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->runFileProcessors($this->resolvedSource, $this->postProcessors['source']);
        $this->runFileProcessors($this->resolvedDestination, $this->postProcessors['destination']);
    }
}
