<?php

error_reporting(-1);

define("MANAGER", "manager");
define("MARKETER", "marketer");
define("ENGINEER", "engineer");
define("ANALYST", "analyst");

class CollectionException extends Exception {}
class DepartmentException extends Exception {}

//Класс сотрудник
class Employee
{	
	protected $profession;
	protected $rate;
	protected $litresOfCoffee;
	protected $pgsOfDocs;
	protected $rank;
	protected $boss;

	final public function __construct(string $profession, int $rate, int $litresOfCoffee, int $pgsOfDocs, int $rank, bool $boss = false){
		$this->profession = $profession;
		$this->rate = $rate;
		$this->litresOfCoffee = $litresOfCoffee;
		$this->pgsOfDocs = $pgsOfDocs;
		$this->rank = $rank;
		$this->boss = $boss;
	}

	final public function getProfession():string {
		return $this->profession;
	}

	final public function getRate():int {
		return $this->rate;
	}

	public function getRateWithRank():float {
		switch ($this->rank) {
			case 1:
				$rateWithRank = $this->rate;
				break;
			case 2:
				$rateWithRank = $this->rate * 1.25;
				break;
			case 3:
				$rateWithRank = $this->rate * 1.50;
				break;
		}
		if ($this->boss)
			$rateWithRank *= 1.50;
		return $rateWithRank;
	}

	public function getCoffee():int {
		if ($this->boss) {
			return $this->litresOfCoffee * 2;
		} else {
			return $this->litresOfCoffee;
		}
	}

	public function getPages():int {
		if ($this->boss) {
			return 0;
		} else {
			return $this->pgsOfDocs;
		}
	}

	final public function getRank():int {
		return $this->rank;
	}

	final public function isBoss():bool {
		return $this->boss;
	}

	final public function setProfession(string $profession){
		$this->profession = $profession;
	}

	final public function setRate(int $rate){
		$this->rate = $rate;
	}

	final public function setCoffee(int $litresOfCoffee){
		$this->litresOfCoffee = $litresOfCoffee;
	}

	final public function setPages(int $pages){
		$this->pgsOfDocs = $pages;
	}

	final public function setRank(int $rank):bool {
		if($rank > 0 and $rank < 4){
			$this->rank = $rank;
			return true;	
		} else {
			return false;
		}
	}

	final public function setBossStatus(bool $bossStatus){
		$this->boss = $bossStatus;
	}
}
//Класс коллекция сотрудников - сортирует, отбирает, исключает, берет процент, берет число сотрудников, извлекает копии класса сотрудников. Возвращает объект коллекцию.
class Collection
{
	private $employees;
	private $method;
	private $sortFlag;

	const ASC = 1;//сортировка по возрастанию
	const DESC = 2;//сортировка по убыванию
	const ALL = 3;//все копии
	const RATE = 4;
	const COFFEE = 5;
	const PAGES = 6;
	const RANK = 7;
	const SALARY = 8;
	const PROFESSION = 9;
	const BOSS = 10;

	public function __construct(Employee ...$employees){
		$this->employees = $employees;
	}
	//Сортировка по параметрам
	public function sort(int $sortProperty, int $sortFlag = self::ASC):Collection {
		if ($sortFlag == self::ASC or $sortFlag == self::DESC) {
			$this->sortFlag = $sortFlag;
		} else {
			throw new CollectionException("Флаг сортировки должен принимать значения: Collection::ASC, Collection::DESC\n");	
		}

		if ($this->setMethod($sortProperty) and ($sortProperty < self::PROFESSION)) {
			return usort($this->employees, array($this, "sortByProperty")) ? $this :
				call_user_func(function(){
					throw new CollectionException("Сортировка не произошла!\n");
				})
				;
		} else {
			throw new CollectionException("Сортировка возможна по параметрам: Collection::RATE, Collection::COFFEE, Collection::PAGES, Collection::RANK, Collection::SALARY\n");
		}
	}
	//Выбор по параметрам
	public function select(int $property, $value) {
			if ($this->setMethod($property)) {
				$selectedEmployees = [];
				foreach ($this->employees as $employee) {
					$method = $this->method;
					if($employee->$method() === $value)
						$selectedEmployees[] = $employee;				
				}
				if (empty($selectedEmployees)) {
					throw new CollectionException("Элементы не найдены!\n");
				} else {
					$this->employees = $selectedEmployees;
					return $this;
				}
			} else {
				throw new CollectionException("Выбор возможен по параметрам: Collection::RATE, Collection::COFFEE, Collection::PAGES, Collection::RANK, Collection::SALARY, Collection::PROFESSION, Collection::BOSS");
			}		
	}
	//Исключение по параметрам
	public function exclude(int $property, $value):Collection {	
			if ($this->setMethod($property)) {
				$neededEmployees = [];
				$method = $this->method;
				foreach ($this->employees as $employee) {
					if($employee->$method() != $value)
						$neededEmployees[] = $employee;
				}
				if (empty($neededEmployees)) {
					throw new CollectionException("Исключены все элементы!\n");	
				} else {
					$this->employees = $neededEmployees;
					return $this;
				}
			} else {
				throw new CollectionException("Исключение возможно по параметрам: Collection::RATE, Collection::COFFEE, Collection::PAGES, Collection::RANK, Collection::SALARY, Collection::PROFESSION, Collection::BOSS");
			}		
	}
	//Взять процент от числа сотрудников
	public function takePercent(int $percent):Collection {
		if ($percent > 0) {
			$neededEmployees = [];
			$percent = $percent / 100;
			$numOfEmpl = count($this->employees);
			$neededNumOfEmpl = round($numOfEmpl * $percent);
			if ($neededNumOfEmpl == 0) {
				throw new CoolectionException("Взять данный процент от числа сотрудников не возможно!\n");
			}
			for($i = 0; $i < $neededNumOfEmpl; $i++){
				$neededEmployees[] = $this->employees[$i];
			}
			$this->employees = $neededEmployees;
			return $this;
		} else {
			return false;
		}
	}
	//Извлечь из коллекции копии объектов класса Сотрудник
	public function extractEmployees(int $number = self::ALL) {
		$clonedEmployees = [];
		$numOfEmpls = count($this->employees);
		if ($number == self::ALL) {
			foreach ($this->employees as $employee) {
				$clonedEmployees[] = clone $employee; 
			}
			return $clonedEmployees;
		} else {
			if ($number > $numOfEmpls) {
				throw new CollectionException("{$number} сотрудников не может быть извлечено, в коллекции {$numOfEmpls} сотрудников!\n");	
			} elseif($number == 1) {
				return $this->extractEmployee();
			} else {
				for($i = 0; $i < $numOfEmpls; $i++){
					$clonedEmployees[] = clone $this->employees[$i];
				}
				return $clonedEmployees;			
			}
		}
	}
	//Извлечь одну копию объекта класса Сотрудник
	public function extractEmployee():Employee {
		return clone $this->employees[0];
	}
	//Получить коллекцию из $num элементов
	public function takeNumItems(int $num):Collection {
		$numOfItems = count($this->employees);
		if ($num > $numOfItems) {
			throw new CollectionException("Коллекция из {$num} элементов не может быть создана, в коллекции {$numOfItems} элементов!\n");
		} elseif($num == 1) {
			return $this->takeOneItem();
		} else {
			$collectionOfNumItems = [];
			for($i = 0; $i < $num; $i++){
				$collectionOfNumItems[] = $this->employees[$i];
			}
			$this->employees = $collectionOfNumItems;
			return $this;
		}
	}
	//Получить коллекцию из одного элемента
	public function takeOneItem():Collection {
		$oneItemCollection[] = $this->employees[0];
		$this->employees = $oneItemCollection;
		return $this;
	}
	//Выбор гет-метода свойства по которому происходит сортировка, выборка и т.п.
	private function setMethod(int $property):bool {
		switch($property){
			case self::RATE:
				$this->method = "getRate";
				return true;			
			case self::COFFEE:
				$this->method = "getCoffee";
				return true;
			case self::PAGES:
				$this->method = "getPages";
				return true;			
			case self::RANK:
				$this->method = "getRank";
				return true;
			case self::SALARY:
				$this->method = "getRateWithRank";
				return true;
			case self::PROFESSION:
				$this->method = "getProfession";
				return true;
			case self::BOSS:
				$this->method = "isBoss";
				return true;
			default:
				return false;
		}
	}
	//Универсальная ф-я сортировки
	private function sortByProperty(Employee $employee1, Employee $employee2):int {
		$method = $this->method;
		if ($this->sortFlag == self::ASC) {
			return ($employee1->$method() <=> $employee2->$method());
		} else {
			return -($employee1->$method() <=> $employee2->$method());
		}

	}

}

class Department
{	
	const UPRANK = "up";//повысить ранг
	const DOWNRANK = "down";//понизить ранг

	private $name;
	private $employees = [];

	public function __construct(string $name){
		$this->name = $name;
	}
	//Создать сотрудника
	public function createEmployee(int $number, string $employeeClassName, string $profession, int $rate, int $litresOfCoffee, int $pgsOfDocs, int $rank, bool $boss = false){
		if (class_exists($employeeClassName) and is_a($employeeClassName, "Employee", true)){
			for ($i = 0; $i < $number; $i++) {
				$this->employees[] = new $employeeClassName($profession, $rate, $litresOfCoffee, $pgsOfDocs, $rank, $boss);
			}
		} else {
			throw new DepartmentException("Ошибка создания сотрудника!\n");
			
		}
	}
	//Получить коллекцию
	public function getCollection(){
		if (empty($this->employees)) {
			throw new DepartmentException("В департаменте нет сотрудников! Коллекция не может быть создана!\n");
		}
		return new Collection(...$this->employees);
	}
	//Удалить сотрудника, принимает на вход коллекцию, удаляет сотрудников идентичных сотрудникам из коллекции(сравнение полей)
	public function fireEmployees(Collection $collection){
		$samples = $collection->extractEmployees();
		foreach ($samples as $sample) {
			foreach ($this->employees as $key => $realEmployee) {
				if ($realEmployee == $sample) {
					unset($this->employees[$key]);
					break;			
				}
			}
		}
		$this->employees = array_values($this->employees);
	}
	//Добавить сотрудников из коллекции
	public function addEmployees(Collection $collection){
		foreach ($collection->extractEmployees() as $employee) {
			$this->employees[] = $employee;	
		}
	}
	//Изменить сотрудников, работает аналогично ф-ии удаления
	public function changeEmployees(Collection $collection, int $property, $value){
		switch($property){
			case Collection::RATE:
				$method = "setRate";
				break;			
			case Collection::COFFEE:
				$method = "setCoffee";
				break;
			case Collection::PAGES:
				$method = "setPages";
				break;			
			case Collection::RANK:
				$method = "setRank";
				break;
			case Collection::PROFESSION:
				$method = "setProfession";
				break;
			case Collection::BOSS:
				$method = "setBossStatus";
				break;
			default:
				throw new DepartmentException("Свойство для изменения должно быть следующим: Collection::RATE, Collection::COFFEE, Collection::PAGES, Collection::RANK, Collection::PROFESSION, Collection::BOSS\n");
		}
		$samples = $collection->extractEmployees();
		if ($method == "setRank") {
			foreach ($samples as $sample) {
				foreach ($this->employees as $realEmployee) {
					if ($realEmployee == $sample) {
						if ($value == self::UPRANK) {
							$rank = $realEmployee->getRank();
							$value = (++$rank > 3) ? --$rank : $rank;
						}
						if ($value == self::DOWNRANK){
							$rank = $realEmployee->getRank();
							$value = (--$rank == 0) ? ++$rank : $rank;
						}
						if($realEmployee->$method($value)){
							break;			
						} else {
							throw new DepartmentException("Значения ранга должно быть от 1 до 3!\n");
						}
					}
				}
			}				
		} else {
			foreach ($samples as $sample) {
				foreach ($this->employees as $realEmployee) {
					if ($realEmployee == $sample) {
						$realEmployee->$method($value);
						break;			
					}
				}
			}
		}
	}

	public function getName():string {
		return $this->name;
	}

	public function getNumOfEmployees():int {
		return count($this->employees);
	}

	public function getSalaryOfEmployees():float {
		$salary = 0;
		foreach ($this->employees as $employee) {
			$salary += $employee->getRateWithRank();
		}
		return $salary;
	}

	public function getCoffee():int {
		$coffee = 0;
		foreach ($this->employees as $employee) {
			$coffee += $employee->getCoffee();
		}
		return $coffee;
	}

	public function getPages():int {
		$pages = 0;
		foreach ($this->employees as $employee) {
			$pages += $employee->getPages();
		}
		return $pages;		
	}

	public function getTugricsPerPage():float {
		if ($this->getPages() == 0) {
			return 0;
		} else {
			return round($this->getSalaryOfEmployees() / $this->getPages(), 1);
		}
	}

}

class Company
{
	private $name;
	private $departments;
	private $backupDepartments = [];//клоны департаментов - для восстановления

	public function __construct(string $name, Department ...$departments){
		$this->departments = $departments;
	}

	public function getName():string {
		return $this->name;
	}

	public function getDepartments():array {
		return $this->departments;
	}
	//клонирует департаменты(вместе с сотрудниками)
	private function cloneDepartments(array $baseDepartments):array {
		$copiedDepartments = [];
		foreach ($baseDepartments as $department) {
			$collection = $department->getCollection();
			$backupDepartment = new Department($department->getName());
			$backupDepartment->addEmployees($collection);
			$copiedDepartments[] = $backupDepartment;
		}
		return $copiedDepartments;
	}
	//клонирует исходный массив департаментов
	public function backup(){
		$this->backupDepartments = $this->cloneDepartments($this->departments);
	}
	//восстанавливает исходный массив
	public function restore(){
		$this->departments = $this->cloneDepartments($this->backupDepartments);
	}
}

function padRight($string, $widthOfCol){
	$lengthOfString = mb_strlen($string);
	if ($lengthOfString < $widthOfCol) {
		$formattedString = $string . str_repeat(" ", $widthOfCol - $lengthOfString);
		return $formattedString;
	} else {
		return false;
	}
}

function padLeft($string, $widthOfCol){
	$lengthOfString = mb_strlen($string);
	if ($lengthOfString < $widthOfCol) {
		$formattedString = str_repeat(" ", $widthOfCol - $lengthOfString) . $string;
		return $formattedString;
	} else {
		return false;
	}
}

function displayReport(Company $company){

	$col1 = 15;
	$col2 = 10;
	$col3 = 10;
	$col4 = 10;
	$col5 = 10;
	$col6 = 12;

	echo padRight("Департамент", $col1) .
	     padLeft("Сотр.", $col2) . 
	     padLeft("Тугр.", $col3) . 
	     padLeft("Кофе", $col4) . 
	     padLeft("Стр.", $col5) . 
	     padLeft("Тугр./стр.", $col6) . "\n\n";

	$allEmployees = 0;
	$allSalary = 0;
	$allCoffee = 0;
	$allPages = 0;
	$allTugPerPgs = 0;

	foreach ($company->getDepartments() as $department) {

		echo padRight($department->getName(), $col1) .
		padLeft($department->getNumOfEmployees(), $col2) .
		padLeft($department->getSalaryOfEmployees(), $col3) .
		padLeft($department->getCoffee(), $col4) .
		padLeft($department->getPages(), $col5) . 
		padLeft($department->getTugricsPerPage(), $col6) . "\n";

		$allEmployees += $department->getNumOfEmployees();
		$allSalary += $department->getSalaryOfEmployees();
		$allCoffee += $department->getCoffee();
		$allPages += $department->getPages();
		$allTugPerPgs += $department->getTugricsPerPage();
	}

	$numOfDepartments = count($company->getDepartments());

	echo padRight("Среднее", $col1) .
	     padLeft(round(($allEmployees / $numOfDepartments),1), $col2) . 
	     padLeft(round(($allSalary / $numOfDepartments), 1), $col3) . 
	     padLeft(round(($allCoffee / $numOfDepartments), 1), $col4) . 
	     padLeft(round(($allPages / $numOfDepartments), 1), $col5) .
	     padLeft(round(($allTugPerPgs / $numOfDepartments), 1), $col6) . "\n";

	echo padRight("Всего", $col1) .
	     padLeft($allEmployees, $col2) . 
	     padLeft($allSalary, $col3) . 
	     padLeft($allCoffee, $col4) . 
	     padLeft($allPages, $col5) .
	     padLeft($allTugPerPgs, $col6) . "\n\n";
}

//департаменты
$procurementDep = new Department("Закупок");
$salesDep = new Department("Продаж");
$advDep = new Department("Рекламы");
$logstcDep = new Department("Логистики");

//добавим сотруников в департаменты

$procurementDep->createEmployee(9, "Employee", MANAGER, 500, 20, 200, 1);
$procurementDep->createEmployee(3, "Employee", MANAGER, 500, 20, 200, 2);
$procurementDep->createEmployee(2, "Employee", MANAGER, 500, 20, 200, 3);
$procurementDep->createEmployee(2, "Employee", MARKETER, 400, 15, 150, 1);
$procurementDep->createEmployee(1, "Employee", MANAGER, 500, 20, 200, 2, true);

$salesDep->createEmployee(12, "Employee", MANAGER, 500, 20, 200, 1);
$salesDep->createEmployee(6, "Employee", MARKETER, 400, 15, 150, 1);
$salesDep->createEmployee(3, "Employee", ANALYST, 800, 50, 5, 1);
$salesDep->createEmployee(2, "Employee", ANALYST, 800, 50, 5, 2);
$salesDep->createEmployee(1, "Employee", MARKETER, 400, 15, 150, 2, true);

$advDep->createEmployee(15, "Employee", MARKETER, 400, 15, 150, 1);
$advDep->createEmployee(10, "Employee", MARKETER, 400, 15, 150, 2);
$advDep->createEmployee(8, "Employee", MANAGER, 500, 20, 200, 1);
$advDep->createEmployee(2, "Employee", ENGINEER, 200, 5, 50, 1);
$advDep->createEmployee(1, "Employee", MARKETER, 400, 15, 150, 3, true);

$logstcDep->createEmployee(13, "Employee", MANAGER, 500, 20, 200, 1);
$logstcDep->createEmployee(5, "Employee", MANAGER, 500, 20, 200, 2);
$logstcDep->createEmployee(5, "Employee", ENGINEER, 200, 5, 50, 1);
$logstcDep->createEmployee(1, "Employee", MANAGER, 500, 20, 200, 1, true);

$company = new Company("Вектор", $procurementDep, $salesDep, $advDep, $logstcDep);
$company->backup();

//Начальные данные
echo "Начальные данные:\n";
displayReport($company);
//План 1
foreach($company->getDepartments() as $department){
	try {
		$firedLowRankEngineers = $department->getCollection()->select(Collection::PROFESSION, ENGINEER)->sort(Collection::RANK)->exclude(Collection::BOSS, true)->takePercent(40);
		$department->fireEmployees($firedLowRankEngineers);
	} catch(CollectionException $e){
		//echo "В департаменте " . $department->getName() . " " . $e->getMessage();
		continue;
	} catch(DepartmentException $e){
		echo $e->getMessage();
		exit();
	}
}
echo "План 1:\n";
displayReport($company);
$company->restore();
//План 2
foreach ($company->getDepartments() as $department) {
	try {
		$analysts = $department->getCollection()->select(Collection::PROFESSION, ANALYST);
		$department->changeEmployees($analysts, Collection::RATE, 1100);
		$department->changeEmployees($analysts, Collection::COFFEE, 75);
		$oldBoss = $department->getCollection()->select(Collection::BOSS, true);
		if ($oldBoss->extractEmployee()->getProfession() != ANALYST) {
			$department->changeEmployees($oldBoss, Collection::BOSS, false);
			$highRankAnalyst = $department->getCollection()->select(Collection::PROFESSION, ANALYST)->sort(Collection::RANK, Collection::DESC)->takeOneItem();
			$department->changeEmployees($highRankAnalyst, Collection::BOSS, true);
		}
	} catch(CollectionException $e){
		//echo $e->getMessage();
		continue;
	} catch(DepartmentException $e){
		echo $e->getMessage();
		exit();
	}
}
echo "План 2:\n";
displayReport($company);
$company->restore();
//План 3
foreach ($company->getDepartments() as $department) {
	try {
		$oneTwoRankManagers = $department->getCollection()->select(Collection::PROFESSION, MANAGER)->exclude(Collection::RANK, 3)->takePercent(50);
		$department->changeEmployees($oneTwoRankManagers, Collection::RANK, Department::UPRANK);
	} catch(CollectionException $e){
		//echo $e->getMessage();
		continue;
	} catch(DepartmentException $e){
		echo $e->getMessage();
		exit();
	}	
}
echo "План 3:\n";
displayReport($company);
$company->restore();