<?php

declare(strict_types=1);

/**
 * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
 *
 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
 * @author Joas Schilling <coding@schilljs.com>
 * @author Julius Härtl <jus@bitgrid.net>
 *
 * @license GNU AGPL version 3 or any later version
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace OC\AppFramework\Bootstrap;

use Closure;
use OC\Support\CrashReport\Registry;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Dashboard\IManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ILogger;
use Throwable;

class RegistrationContext {

	/** @var array[] */
	private $capabilities = [];

	/** @var array[] */
	private $crashReporters = [];

	/** @var array[] */
	private $dashboardPanels = [];

	/** @var array[] */
	private $services = [];

	/** @var array[] */
	private $aliases = [];

	/** @var array[] */
	private $parameters = [];

	/** @var array[] */
	private $eventListeners = [];

	/** @var array[] */
	private $middlewares = [];

	/** @var array[] */
	private $searchProviders = [];

	/** @var array[] */
	private $alternativeLogins = [];

	/** @var ILogger */
	private $logger;

	public function __construct(ILogger $logger) {
		$this->logger = $logger;
	}

	public function for(string $appId): IRegistrationContext {
		return new class($appId, $this) implements IRegistrationContext {
			/** @var string */
			private $appId;

			/** @var RegistrationContext */
			private $context;

			public function __construct(string $appId, RegistrationContext $context) {
				$this->appId = $appId;
				$this->context = $context;
			}

			public function registerCapability(string $capability): void {
				$this->context->registerCapability(
					$this->appId,
					$capability
				);
			}

			public function registerCrashReporter(string $reporterClass): void {
				$this->context->registerCrashReporter(
					$this->appId,
					$reporterClass
				);
			}

			public function registerDashboardWidget(string $widgetClass): void {
				$this->context->registerDashboardPanel(
					$this->appId,
					$widgetClass
				);
			}

			public function registerService(string $name, callable $factory, bool $shared = true): void {
				$this->context->registerService(
					$this->appId,
					$name,
					$factory,
					$shared
				);
			}

			public function registerServiceAlias(string $alias, string $target): void {
				$this->context->registerServiceAlias(
					$this->appId,
					$alias,
					$target
				);
			}

			public function registerParameter(string $name, $value): void {
				$this->context->registerParameter(
					$this->appId,
					$name,
					$value
				);
			}

			public function registerEventListener(string $event, string $listener, int $priority = 0): void {
				$this->context->registerEventListener(
					$this->appId,
					$event,
					$listener,
					$priority
				);
			}

			public function registerMiddleware(string $class): void {
				$this->context->registerMiddleware(
					$this->appId,
					$class
				);
			}

			public function registerSearchProvider(string $class): void {
				$this->context->registerSearchProvider(
					$this->appId,
					$class
				);
			}

			public function registerAlternativeLogin(string $class): void {
				$this->context->registerAlternativeLogin(
					$this->appId,
					$class
				);
			}
		};
	}

	public function registerCapability(string $appId, string $capability): void {
		$this->capabilities[] = [
			'appId' => $appId,
			'capability' => $capability
		];
	}

	public function registerCrashReporter(string $appId, string $reporterClass): void {
		$this->crashReporters[] = [
			'appId' => $appId,
			'class' => $reporterClass,
		];
	}

	public function registerDashboardPanel(string $appId, string $panelClass): void {
		$this->dashboardPanels[] = [
			'appId' => $appId,
			'class' => $panelClass
		];
	}

	public function registerService(string $appId, string $name, callable $factory, bool $shared = true): void {
		$this->services[] = [
			"appId" => $appId,
			"name" => $name,
			"factory" => $factory,
			"shared" => $shared,
		];
	}

	public function registerServiceAlias(string $appId, string $alias, string $target): void {
		$this->aliases[] = [
			"appId" => $appId,
			"alias" => $alias,
			"target" => $target,
		];
	}

	public function registerParameter(string $appId, string $name, $value): void {
		$this->parameters[] = [
			"appId" => $appId,
			"name" => $name,
			"value" => $value,
		];
	}

	public function registerEventListener(string $appId, string $event, string $listener, int $priority = 0): void {
		$this->eventListeners[] = [
			"appId" => $appId,
			"event" => $event,
			"listener" => $listener,
			"priority" => $priority,
		];
	}

	public function registerMiddleware(string $appId, string $class): void {
		$this->middlewares[] = [
			"appId" => $appId,
			"class" => $class,
		];
	}

	public function registerSearchProvider(string $appId, string $class) {
		$this->searchProviders[] = [
			'appId' => $appId,
			'class' => $class,
		];
	}

	public function registerAlternativeLogin(string $appId, string $class): void {
		$this->alternativeLogins[] = [
			'appId' => $appId,
			'class' => $class,
		];
	}

	/**
	 * @param App[] $apps
	 */
	public function delegateCapabilityRegistrations(array $apps): void {
		foreach ($this->capabilities as $registration) {
			try {
				$apps[$registration['appId']]
					->getContainer()
					->registerCapability($registration['capability']);
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during capability registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	/**
	 * @param App[] $apps
	 */
	public function delegateCrashReporterRegistrations(array $apps, Registry $registry): void {
		foreach ($this->crashReporters as $registration) {
			try {
				$registry->registerLazy($registration['class']);
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during crash reporter registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	/**
	 * @param App[] $apps
	 */
	public function delegateDashboardPanelRegistrations(array $apps, IManager $dashboardManager): void {
		foreach ($this->dashboardPanels as $panel) {
			try {
				$dashboardManager->lazyRegisterWidget($panel['class']);
			} catch (Throwable $e) {
				$appId = $panel['appId'];
				$this->logger->logException($e, [
					'message' => "Error during dashboard registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	public function delegateEventListenerRegistrations(IEventDispatcher $eventDispatcher): void {
		foreach ($this->eventListeners as $registration) {
			try {
				if (isset($registration['priority'])) {
					$eventDispatcher->addServiceListener(
						$registration['event'],
						$registration['listener'],
						$registration['priority']
					);
				} else {
					$eventDispatcher->addServiceListener(
						$registration['event'],
						$registration['listener']
					);
				}
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during event listener registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	/**
	 * @param App[] $apps
	 */
	public function delegateContainerRegistrations(array $apps): void {
		foreach ($this->services as $registration) {
			try {
				/**
				 * Register the service and convert the callable into a \Closure if necessary
				 */
				$apps[$registration['appId']]
					->getContainer()
					->registerService(
						$registration['name'],
						Closure::fromCallable($registration['factory']),
						$registration['shared'] ?? true
					);
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during service registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}

		foreach ($this->aliases as $registration) {
			try {
				$apps[$registration['appId']]
					->getContainer()
					->registerAlias(
						$registration['alias'],
						$registration['target']
					);
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during service alias registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}

		foreach ($this->parameters as $registration) {
			try {
				$apps[$registration['appId']]
					->getContainer()
					->registerParameter(
						$registration['name'],
						$registration['value']
					);
			} catch (Throwable $e) {
				$appId = $registration['appId'];
				$this->logger->logException($e, [
					'message' => "Error during service alias registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	/**
	 * @param App[] $apps
	 */
	public function delegateMiddlewareRegistrations(array $apps): void {
		foreach ($this->middlewares as $middleware) {
			try {
				$apps[$middleware['appId']]
					->getContainer()
					->registerMiddleWare($middleware['class']);
			} catch (Throwable $e) {
				$appId = $middleware['appId'];
				$this->logger->logException($e, [
					'message' => "Error during capability registration of $appId: " . $e->getMessage(),
					'level' => ILogger::ERROR,
				]);
			}
		}
	}

	/**
	 * @return array[]
	 */
	public function getSearchProviders(): array {
		return $this->searchProviders;
	}

	/**
	 * @return array[]
	 */
	public function getAlternativeLogins(): array {
		return $this->alternativeLogins;
	}
}
