<?php

namespace Livewire;

use Exception;
use Livewire\Testing\TestableLivewire;
use Illuminate\Contracts\Auth\Authenticatable;
use Livewire\Exceptions\ComponentNotFoundException;

class LivewireManager
{
    protected $listeners = [];
    protected $componentAliases = [];
    protected $queryParamsForTesting = [];
    protected $missingComponentResolvers = [];

    protected $shouldDisableBackButtonCache = false;

    protected $persistentMiddleware = [
        \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
        \Laravel\Jetstream\Http\Middleware\AuthenticateSession::class,
        \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        \App\Http\Middleware\RedirectIfAuthenticated::class,
        \Illuminate\Auth\Middleware\Authenticate::class,
        \Illuminate\Auth\Middleware\Authorize::class,
        \App\Http\Middleware\Authenticate::class,
    ];

    public static $isLivewireRequestTestingOverride = false;

    public static $currentCompilingViewPath;
    public static $currentCompilingChildCounter;

    public function component($alias, $viewClass = null)
    {
        if (is_null($viewClass)) {
            $viewClass = $alias;
            $alias = $viewClass::getName();
        }

        if (is_object($viewClass)) {
            $viewClass = get_class($viewClass);
        }

        $this->componentAliases[$alias] = $viewClass;
    }

    public function getAlias($class, $default = null)
    {
        $alias = array_search($class, $this->componentAliases);

        return $alias === false ? $default : $alias;
    }

    public function getComponentAliases()
    {
        return $this->componentAliases;
    }

    public function resolveMissingComponent($resolver)
    {
        $this->missingComponentResolvers[] = $resolver;
    }

    public function getClass($alias)
    {
        $finder = app(LivewireComponentsFinder::class);

        $class = false;

        $class = $class ?: (
            // Let's first check if the user registered the component using:
            // Livewire::component('name', [Livewire component class]);
            // If not, we'll look in the auto-discovery manifest.
            $this->componentAliases[$alias] ?? $finder->find($alias)
        );

        if (! $class) {
            // If we haven't found the component through the normal channels
            // we'll look through user-land missing-component JIT hooks...
            foreach ($this->missingComponentResolvers as $resolve) {
                if ($resolved = $resolve($alias)) {
                    $this->component($alias, $resolved);

                    $class = $this->componentAliases[$alias];
                    break;
                }
            }
        }

        $class = $class ?: (
            // If none of the above worked, our last-ditch effort will be
            // to re-generate the auto-discovery manifest and look again.
            $finder->build()->find($alias)
        );

        throw_unless($class, new ComponentNotFoundException(
            "Unable to find component: [{$alias}]"
        ));

        return $class;
    }

    public function getInstance($component, $id)
    {
        $componentClass = $this->getClass($component);

        throw_unless(class_exists($componentClass), new ComponentNotFoundException(
            "Component [{$component}] class not found: [{$componentClass}]"
        ));

        return new $componentClass($id);
    }

    public function mount($name, $params = [])
    {
        // This is if a user doesn't pass params, BUT passes key() as the second argument.
        if (is_string($params)) $params = [];

        $id = str()->random(20);

        if (class_exists($name)) {
            $name = $name::getName();
        }

        return LifecycleManager::fromInitialRequest($name, $id)
            ->boot()
            ->initialHydrate()
            ->mount($params)
            ->renderToView()
            ->initialDehydrate()
            ->toInitialResponse();
    }

    public function dummyMount($id, $tagName)
    {
        return "<{$tagName} wire:id=\"{$id}\"></{$tagName}>";
    }

    public function test($name, $params = [])
    {
        return new TestableLivewire($name, $params, $this->queryParamsForTesting);
    }

    public function visit($browser, $class, $queryString = '')
    {
        $url = '/livewire-dusk/'.urlencode($class).$queryString;

        return $browser->visit($url)->waitForLivewireToLoad();
    }

    public function actingAs(Authenticatable $user, $driver = null)
    {
        // This is a helper to be used during testing.

        if (isset($user->wasRecentlyCreated) && $user->wasRecentlyCreated) {
            $user->wasRecentlyCreated = false;
        }

        auth()->guard($driver)->setUser($user);

        auth()->shouldUse($driver);

        return $this;
    }

    public function addPersistentMiddleware($middleware)
    {
        $this->persistentMiddleware = array_merge($this->persistentMiddleware, (array) $middleware);
    }

    public function setPersistentMiddleware($middleware)
    {
        $this->persistentMiddleware = (array) $middleware;
    }

    public function getPersistentMiddleware()
    {
        return $this->persistentMiddleware;
    }

    public function styles($options = [])
    {
        $debug = config('app.debug');

        $styles = $this->cssAssets($options);

        // HTML Label.
        $html = $debug ? ['<!-- Livewire Styles -->'] : [];

        // CSS assets.
        $html[] = $debug ? $styles : $this->minify($styles);

        return implode("\n", $html);
    }

    public function scripts($options = [])
    {
        $debug = config('app.debug');

        $scripts = $this->javaScriptAssets($options);

        // HTML Label.
        $html = $debug ? ['<!-- Livewire Scripts -->'] : [];

        // JavaScript assets.
        $html[] = $debug ? $scripts : $this->minify($scripts);

        return implode("\n", $html);
    }

    protected function cssAssets($options = [])
    {
        $nonce = isset($options['nonce']) ? "nonce=\"{$options['nonce']}\"" : '';

        return <<<HTML
<style {$nonce}>
    [wire\:loading], [wire\:loading\.delay], [wire\:loading\.inline-block], [wire\:loading\.inline], [wire\:loading\.block], [wire\:loading\.flex], [wire\:loading\.table], [wire\:loading\.grid], [wire\:loading\.inline-flex] {
        display: none;
    }

    [wire\:loading\.delay\.shortest], [wire\:loading\.delay\.shorter], [wire\:loading\.delay\.short], [wire\:loading\.delay\.long], [wire\:loading\.delay\.longer], [wire\:loading\.delay\.longest] {
        display:none;
    }

    [wire\:offline] {
        display: none;
    }

    [wire\:dirty]:not(textarea):not(input):not(select) {
        display: none;
    }

    input:-webkit-autofill, select:-webkit-autofill, textarea:-webkit-autofill {
        animation-duration: 50000s;
        animation-name: livewireautofill;
    }

    @keyframes livewireautofill { from {} }
</style>
HTML;
    }

    protected function javaScriptAssets($options)
    {
        $jsonEncodedOptions = $options ? json_encode($options) : '';

        $assetsUrl = config('livewire.asset_url') ?: rtrim($options['asset_url'] ?? '', '/');

        $appUrl = config('livewire.app_url')
            ?: rtrim($options['app_url'] ?? '', '/')
            ?: $assetsUrl;

        $jsLivewireToken = app()->has('session.store') ? "'" . csrf_token() . "'" : 'null';

        $manifest = json_decode(file_get_contents(__DIR__.'/../dist/manifest.json'), true);
        $versionedFileName = $manifest['/livewire.js'];

        // Default to dynamic `livewire.js` (served by a Laravel route).
        $fullAssetPath = "{$assetsUrl}/livewire{$versionedFileName}";
        $assetWarning = null;

        $nonce = isset($options['nonce']) ? "nonce=\"{$options['nonce']}\"" : '';

        // Use static assets if they have been published
        if (file_exists(public_path('vendor/livewire/manifest.json'))) {
            $publishedManifest = json_decode(file_get_contents(public_path('vendor/livewire/manifest.json')), true);
            $versionedFileName = $publishedManifest['/livewire.js'];

            $fullAssetPath = ($this->isRunningServerless() ? config('app.asset_url') : $assetsUrl).'/vendor/livewire'.$versionedFileName;

            if ($manifest !== $publishedManifest) {
                $assetWarning = <<<HTML
<script {$nonce}>
    console.warn("Livewire: The published Livewire assets are out of date\n See: https://laravel-livewire.com/docs/installation/")
</script>
HTML;
            }
        }

	    $devTools = null;
	    $windowLivewireCheck = null;
	    $windowAlpineCheck = null;
        if (config('app.debug')) {
	        $devTools = 'window.livewire.devTools(true);';

	        $windowLivewireCheck = <<<'HTML'
if (window.livewire) {
	    console.warn('Livewire: It looks like Livewire\'s @livewireScripts JavaScript assets have already been loaded. Make sure you aren\'t loading them twice.')
	}
HTML;

	        $windowAlpineCheck = <<<'HTML'
/* Make sure Livewire loads first. */
	if (window.Alpine) {
	    /* Defer showing the warning so it doesn't get buried under downstream errors. */
	    document.addEventListener("DOMContentLoaded", function () {
	        setTimeout(function() {
	            console.warn("Livewire: It looks like AlpineJS has already been loaded. Make sure Livewire\'s scripts are loaded before Alpine.\\n\\n Reference docs for more info: http://laravel-livewire.com/docs/alpine-js")
	        })
	    });
	}

	/* Make Alpine wait until Livewire is finished rendering to do its thing. */
HTML;

        }

        // Adding semicolons for this JavaScript is important,
        // because it will be minified in production.
        return <<<HTML
{$assetWarning}
<script src="{$fullAssetPath}" data-turbo-eval="false" data-turbolinks-eval="false" {$nonce}></script>
<script data-turbo-eval="false" data-turbolinks-eval="false" {$nonce}>
    {$windowLivewireCheck}

    window.livewire = new Livewire({$jsonEncodedOptions});
    {$devTools}
    window.Livewire = window.livewire;
    window.livewire_app_url = '{$appUrl}';
    window.livewire_token = {$jsLivewireToken};

	{$windowAlpineCheck}
    window.deferLoadingAlpine = function (callback) {
        window.addEventListener('livewire:load', function () {
            callback();
        });
    };

    let started = false;

    window.addEventListener('alpine:initializing', function () {
        if (! started) {
            window.livewire.start();

            started = true;
        }
    });

    document.addEventListener("DOMContentLoaded", function () {
        if (! started) {
            window.livewire.start();

            started = true;
        }
    });
</script>
HTML;
    }

    protected function minify($subject)
    {
        return preg_replace('~(\v|\t|\s{2,})~m', '', $subject);
    }

    public function isLivewireRequest()
    {
        return $this->isProbablyLivewireRequest();
    }

    public function isDefinitelyLivewireRequest()
    {
        $route = request()->route();

        if (! $route) return false;

        return $route->named('livewire.message') || $route->named('livewire.message-localized');
    }

    public function isProbablyLivewireRequest()
    {
        if (static::$isLivewireRequestTestingOverride) return true;

        return request()->hasHeader('X-Livewire');
    }

    public function originalUrl()
    {
        if ($this->isDefinitelyLivewireRequest()) {
            return url()->to($this->originalPath());
        }

        return url()->current();
    }

    public function originalPath()
    {
        if ($this->isDefinitelyLivewireRequest()) {
            // @depricted: "url" usage was removed in v2.3.17
            // This can be removed after a period of time
            // as users will have refreshed all pages
            // that still used "url".
            if (isset(request('fingerprint')['url'])) {
                return str(request('fingerprint')['url'])->after(request()->root());
            }

            return request('fingerprint.path');
        }

        return request()->path();
    }

    public function originalMethod()
    {
        if ($this->isDefinitelyLivewireRequest()) {
            // @depricted: "url" usage was removed in v2.3.17
            // This can be removed after a period of time
            // as users will have refreshed all pages
            // that still used "url".
            if (isset(request('fingerprint')['url'])) {
                return 'GET';
            }

            return request('fingerprint.method', 'POST');
        }

        return request()->method();
    }

    public function getRootElementTagName($dom)
    {
        preg_match('/<([a-zA-Z0-9\-]*)/', $dom, $matches, PREG_OFFSET_CAPTURE);

        return $matches[1][0];
    }

    public function dispatch($event, ...$params)
    {
        foreach ($this->listeners[$event] ?? [] as $listener) {
            $listener(...$params);
        }
    }

    public function listen($event, $callback)
    {
        $this->listeners[$event][] = $callback;
    }

    public function isOnVapor()
    {
        return $this->isRunningServerless();
    }

    public function isRunningServerless()
    {
        return in_array($_ENV['SERVER_SOFTWARE'] ?? null, [
            'vapor',
            'bref',
        ]);
    }

    public function withQueryParams($queryParams)
    {
        $this->queryParamsForTesting = $queryParams;

        return $this;
    }

    public function setBackButtonCache()
    {
        /**
         * Reverse this boolean so that the middleware is only applied when it is disabled.
         */
        $this->shouldDisableBackButtonCache = ! config('livewire.back_button_cache', false);
    }

    public function disableBackButtonCache()
    {
        $this->shouldDisableBackButtonCache = true;
    }

    public function enableBackButtonCache()
    {
        $this->shouldDisableBackButtonCache = false;
    }

    public function shouldDisableBackButtonCache()
    {
        return $this->shouldDisableBackButtonCache;
    }

    public function flushState()
    {
        static::$isLivewireRequestTestingOverride = false;
        static::$currentCompilingChildCounter = null;
        static::$currentCompilingViewPath = null;

        $this->shouldDisableBackButtonCache = false;

        $this->dispatch('flush-state');
    }
}
