<?php
/**
 * @copyright Copyright (c) 2020, Matias De lellis <mati86dl@gmail.com>
 * @copyright Copyright (c) 2019, Branko Kokanovic <branko@kokanovic.org>
 *
 * @author Branko Kokanovic <branko@kokanovic.org>
 *
 * @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 OCA\FaceRecognition\Service;

use OCP\IUser;
use OCP\IUserManager;

use OCA\FaceRecognition\Db\FaceMapper;
use OCA\FaceRecognition\Db\ImageMapper;
use OCA\FaceRecognition\Db\PersonMapper;

use OCA\FaceRecognition\Service\SettingsService;

/**
 * Background service. Both command and cron job are calling this service for long-running background operations.
 * Background processing for face recognition is comprised of several steps, called tasks. Each task is independent,
 * idempotent, DI-aware logic unit that yields. Since tasks are non-preemptive, they should yield from time to time, so we son't end up
 * working for more than given timeout.
 *
 * Tasks can be seen as normal sequential functions, but they are easier to work with,
 * reason about them and test them independently. Other than that, they are really glorified functions.
 */
class FaceManagementService {

	/** @var IUserManager */
	private $userManager;

	/** @var FaceMapper */
	private $faceMapper;

	/** @var ImageMapper */
	private $imageMapper;

	/** @var PersonMapper */
	private $personMapper;

	/** @var SettingsService */
	private $settingsService;

	public function __construct(IUserManager    $userManager,
	                            FaceMapper      $faceMapper,
	                            ImageMapper     $imageMapper,
	                            PersonMapper    $personMapper,
	                            SettingsService $settingsService)
	{
		$this->userManager     = $userManager;
		$this->faceMapper      = $faceMapper;
		$this->imageMapper     = $imageMapper;
		$this->personMapper    = $personMapper;
		$this->settingsService = $settingsService;
	}

	/**
	 * Check if the current model has data on db
	 *
	 * @param IUser|null $user Optional user to check
	 * @param Int $modelId Optional model to check
	 *
	 * @return bool
	 */
	public function hasData(IUser $user = null, int $modelId = -1): bool {
		if ($modelId === -1) {
			$modelId = $this->settingsService->getCurrentFaceModel();
		}
		$eligible_users = $this->getEligiblesUserId($user);
		foreach ($eligible_users as $userId) {
			if ($this->hasDataForUser($userId, $modelId)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Check if the current model has data on db for user
	 *
	 * @param string $user ID of user to check
	 * @param Int $modelId model to check
	 *
	 * @return bool
	 */
	public function hasDataForUser(string $userId, int $modelId): bool {
		$facesCount = $this->faceMapper->countFaces($userId, $modelId);
		return ($facesCount > 0);
	}

	/**
	 * Deletes all faces, images and persons found. IF no user is given, resetting is executed for all users.
	 *
	 * @param IUser|null $user Optional user to execute resetting for
	 *
	 * @return void
	 */
	public function resetAll(IUser $user = null): void {
		$eligible_users = $this->getEligiblesUserId($user);
		foreach($eligible_users as $user) {
			$this->resetAllForUser($user);
		}
	}

	/**
	 * Deletes all faces, images and persons found for a given user.
	 *
	 * @param string $user ID of user to execute resetting for
	 *
	 * @return void
	 */
	public function resetAllForUser(string $userId): void {
		$this->faceMapper->deleteUserFaces($userId);
		$this->personMapper->deleteUserPersons($userId);
		$this->imageMapper->deleteUserImages($userId);

		$this->settingsService->setUserFullScanDone(false, $userId);
	}

	/**
	 * Deletes all faces, images and persons found. If no user is given, resetting is executed for all users.
	 *
	 * @param IUser|null $user Optional user to execute resetting for
	 * @param Int $modelId Optional model to clean
	 *
	 * @return void
	 */
	public function resetModel(IUser $user = null, int $modelId = -1): void {
		if ($modelId === -1) {
			$modelId = $this->settingsService->getCurrentFaceModel();
		}
		$eligible_users = $this->getEligiblesUserId($user);
		foreach($eligible_users as $userId) {
			$this->resetModelForUser($userId, $modelId);
		}
	}

	/**
	 * Deletes all faces, images and persons found for a given user.
	 *
	 * @param string $user ID of user to execute resetting for
	 * @param Int $modelId model to clean
	 *
	 * @return void
	 */
	public function resetModelForUser(string $userId, $modelId): void {
		$this->personMapper->deleteUserModel($userId, $modelId);
		$this->faceMapper->deleteUserModel($userId, $modelId);
		$this->imageMapper->deleteUserModel($userId, $modelId);

		$this->settingsService->setUserFullScanDone(false, $userId);
	}

	/**
	 * Reset error in images in order to re-analyze again.
	 * If no user is given, resetting is executed for all users.
	 *
	 * @param IUser|null $user Optional user to execute resetting for
	 *
	 * @return void
	 */
	public function resetImageErrors(IUser $user = null): void {
		$eligible_users = $this->getEligiblesUserId($user);
		foreach($eligible_users as $userId) {
			$this->imageMapper->resetErrors($userId);
			$this->settingsService->setUserFullScanDone(false, $userId);
		}
	}

	/**
	 * Eliminate all faces relations with person.
	 * If no user is given, resetting is executed for all users.
	 *
	 * @param IUser|null $user Optional user to execute resetting for
	 *
	 * @return void
	 */
	public function resetClusters(IUser $user = null): void {
		$eligible_users = $this->getEligiblesUserId($user);
		foreach($eligible_users as $user) {
			$this->resetClustersForUser($user);
		}
	}

	/**
	 * Eliminate all faces relations with person.
	 *
	 * @param string $user ID of user to execute resetting for
	 *
	 * @return void
	 */
	public function resetClustersForUser(string $userId): void {
		$model = $this->settingsService->getCurrentFaceModel();

		$this->faceMapper->unsetPersonsRelationForUser($userId, $model);
		$this->personMapper->deleteUserPersons($userId);
	}

	/**
	 * Get an array with the eligibles users taking into account the user argument,
	 * or all users.
	 *
	 * @param IUser|null $user Optional user to get specific user.
	 */
	private function getEligiblesUserId(IUser $user = null): array {
		$eligible_users = array();
		if (is_null($user)) {
			$this->userManager->callForAllUsers(function (IUser $user) use (&$eligible_users) {
				$eligible_users[] = $user->getUID();
			});
		} else {
			$eligible_users[] = $user->getUID();
		}
		return $eligible_users;
	}

}