<?php
/**
 * webtrees: online genealogy
 * Copyright (C) 2017 webtrees development team
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
namespace Fisharebest\Webtrees\Controller;

use Fisharebest\Algorithm\Dijkstra;
use Fisharebest\Webtrees\Database;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\Individual;

/**
 * Controller for the relationships calculations
 */
class RelationshipController extends PageController {
	/**
	 * Calculate the shortest paths - or all paths - between two individuals.
	 *
	 * @param Individual $individual1
	 * @param Individual $individual2
	 * @param int        $recursion   How many levels of recursion to use
	 * @param bool       $ancestor    Restrict to relationships via a common ancestor
	 *
	 * @return string[][]
	 */
	public function calculateRelationships(Individual $individual1, Individual $individual2, $recursion, $ancestor = false) {
		$rows = Database::prepare(
			"SELECT l_from, l_to FROM `##link` WHERE l_file = :tree_id AND l_type IN ('FAMS', 'FAMC')"
		)->execute([
			'tree_id' => $individual1->getTree()->getTreeId(),
		])->fetchAll();

		// Optionally restrict the graph to the ancestors of the individuals.
		if ($ancestor) {
			$ancestors = $this->allAncestors($individual1->getXref(), $individual2->getXref(), $individual1->getTree()->getTreeId());
			$exclude   = $this->excludeFamilies($individual1->getXref(), $individual2->getXref(), $individual1->getTree()->getTreeId());
		} else {
			$ancestors = [];
			$exclude   = [];
		}

		$graph = [];

		foreach ($rows as $row) {
			if (empty($ancestors) || in_array($row->l_from, $ancestors) && !in_array($row->l_to, $exclude)) {
				$graph[$row->l_from][$row->l_to] = 1;
				$graph[$row->l_to][$row->l_from] = 1;
			}
		}

		$xref1    = $individual1->getXref();
		$xref2    = $individual2->getXref();
		$dijkstra = new Dijkstra($graph);
		$paths    = $dijkstra->shortestPaths($xref1, $xref2);

		// Only process each exclusion list once;
		$excluded = [];

		$queue = [];
		foreach ($paths as $path) {
			// Insert the paths into the queue, with an exclusion list.
			$queue[] = ['path' => $path, 'exclude' => []];
			// While there are un-extended paths
			while (list(, $next) = each($queue)) {
				// For each family on the path
				for ($n = count($next['path']) - 2; $n >= 1; $n -= 2) {
					$exclude = $next['exclude'];
					if (count($exclude) >= $recursion) {
						continue;
					}
					$exclude[] = $next['path'][$n];
					sort($exclude);
					$tmp = implode('-', $exclude);
					if (in_array($tmp, $excluded)) {
						continue;
					} else {
						$excluded[] = $tmp;
					}
					// Add any new path to the queue
					foreach ($dijkstra->shortestPaths($xref1, $xref2, $exclude) as $new_path) {
						$queue[] = ['path' => $new_path, 'exclude' => $exclude];
					}
				}
			}
		}
		// Extract the paths from the queue, removing duplicates.
		$paths = [];
		foreach ($queue as $next) {
			$paths[implode('-', $next['path'])] = $next['path'];
		}

		return $paths;
	}

	/**
	 * Convert a path (list of XREFs) to an "old-style" string of relationships.
	 *
	 * Return an empty array, if privacy rules prevent us viewing any node.
	 *
	 * @param GedcomRecord[] $path Alternately Individual / Family
	 *
	 * @return string[]
	 */
	public function oldStyleRelationshipPath(array $path) {
		$spouse_codes  = ['M' => 'hus', 'F' => 'wif', 'U' => 'spo'];
		$parent_codes  = ['M' => 'fat', 'F' => 'mot', 'U' => 'par'];
		$child_codes   = ['M' => 'son', 'F' => 'dau', 'U' => 'chi'];
		$sibling_codes = ['M' => 'bro', 'F' => 'sis', 'U' => 'sib'];
		$relationships = [];

		for ($i = 1; $i < count($path); $i += 2) {
			$family = Family::getInstance($path[$i], $this->tree());
			$prev   = Individual::getInstance($path[$i - 1], $this->tree());
			$next   = Individual::getInstance($path[$i + 1], $this->tree());
			if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $prev->getXref() . '@/', $family->getGedcom(), $match)) {
				$rel1 = $match[1];
			} else {
				return [];
			}
			if (preg_match('/\n\d (HUSB|WIFE|CHIL) @' . $next->getXref() . '@/', $family->getGedcom(), $match)) {
				$rel2 = $match[1];
			} else {
				return [];
			}
			if (($rel1 === 'HUSB' || $rel1 === 'WIFE') && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
				$relationships[$i] = $spouse_codes[$next->getSex()];
			} elseif (($rel1 === 'HUSB' || $rel1 === 'WIFE') && $rel2 === 'CHIL') {
				$relationships[$i] = $child_codes[$next->getSex()];
			} elseif ($rel1 === 'CHIL' && ($rel2 === 'HUSB' || $rel2 === 'WIFE')) {
				$relationships[$i] = $parent_codes[$next->getSex()];
			} elseif ($rel1 === 'CHIL' && $rel2 === 'CHIL') {
				$relationships[$i] = $sibling_codes[$next->getSex()];
			}
		}

		return $relationships;
	}

	/**
	 * Find all ancestors of a list of individuals
	 *
	 * @param string $xref1
	 * @param string $xref2
	 * @param int    $tree_id
	 *
	 * @return string[]
	 */
	private function allAncestors($xref1, $xref2, $tree_id) {
		$ancestors = [$xref1, $xref2];

		$queue = [$xref1, $xref2];
		while (!empty($queue)) {
			$placeholders = implode(',', array_fill(0, count($queue), '?'));
			$parameters   = $queue;
			$parameters[] = $tree_id;

			$parents = Database::prepare(
				"SELECT l2.l_from" .
				" FROM `##link` AS l1" .
				" JOIN `##link` AS l2 USING (l_to, l_file) " .
				" WHERE l1.l_type = 'FAMC' AND l2.l_type = 'FAMS' AND l1.l_from IN (" . $placeholders . ") AND l_file = ?"
			)->execute(
				$parameters
			)->fetchOneColumn();

			$queue = [];
			foreach ($parents as $parent) {
				if (!in_array($parent, $ancestors)) {
					$ancestors[] = $parent;
					$queue[]     = $parent;
				}
			}
		}

		return $ancestors;
	}

	/**
	 * Find all families of two individuals
	 *
	 * @param string $xref1
	 * @param string $xref2
	 * @param int    $tree_id
	 *
	 * @return string[]
	 */
	private function excludeFamilies($xref1, $xref2, $tree_id) {
		return Database::prepare(
			"SELECT l_to" .
			" FROM `##link` AS l1" .
			" JOIN `##link` AS l2 USING (l_type, l_to, l_file) " .
			" WHERE l_type = 'FAMS' AND l1.l_from = :spouse1 AND l2.l_from = :spouse2 AND l_file = :tree_id"
		)->execute([
			'spouse1' => $xref1,
			'spouse2' => $xref2,
			'tree_id' => $tree_id,
		])->fetchOneColumn();
	}
}
