<?php

error_reporting(-1);
/** профессия менеджер */
const MANAGER = "manager";
/** профессия маркетолог */
const MARKETER = "marketer";
/** профессия инженер */
const ENGINEER = "engineer";
/** профессия аналитик */
const ANALYST = "analyst";

/** Исключение метода сортировки */
class UsortErrorException extends Exception {}
/** Исключение метода создания сотрудника */
class CreateEmployeeErrorException extends Exception {}
/** Исключение метода получения коллекции */
class CreateCollectionErrorException extends Exception {}

/** Класс сотрудник */
class Employee
{	
	/** @var string $profession профессия */
	protected $profession;
	/** @var int $rate ставка */
	protected $rate;
	/** @var int $litresOfCoffee выпитые литры кофе */
	protected $litresOfCoffee;
	/** @var int $pgsOfDocs кол-во страниц */
	protected $pgsOfDocs;
	/** @var int $rank ранг */
	protected $rank;
	/** @var bool $boss босс или не босс */
	protected $boss;

	/**
	 * Конструктор класса Сотрудник
	 * @param string $profession профессия
	 * @param int $rate ставка
	 * ...
	 */
	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;
	}

	/**
	 * Вернуть профессию
	 * @return string
	 */
	final public function getProfession():string {
		return $this->profession;
	}

	/**
	 * Получить ставку
	 * @return int
	 */
	final public function getRate():int {
		return $this->rate;
	}

	/**
	 * Получить ставку с учетом ранга
	 * @return float
	 */
	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;
	}
	/**
	 * Установить ранг
	 * @param int $rank Ранг
	 * @throws InvalidArgumentException
	 */
	final public function setRank(int $rank){
		if($rank > 0 and $rank < 4){
			$this->rank = $rank;	
		} else {
			throw new InvalidArgumentException("Ранг должен быть равен значению от 1 до 3!\n");	
		}
	}

	final public function setBossStatus(bool $bossStatus){
		$this->boss = $bossStatus;
	}
}

/**
 * Класс Коллекция Сотрудников
 */
class Collection
{	
	/** @var array $employees массив сотрудников */
	private $employees;

	/** как закомментить константу я не знаю( */
	const ALL = -1;

	/**
	 * Конструктор класса коллекции
	 * @param array $employees массив сотрудников
	 */
	public function __construct(array $employees){
		$this->employees = $employees;
	}

	/**
	 * метод для сортировки сотрудников
	 * работает аналогично usort
	 * @param callable $sortFunction callback ф-я
	 * @throws UsortErrorException
	 * @return $this
	 */
	public function sort(callable $sortFunction):Collection {
		if (usort($this->employees, $sortFunction)) {
		} else {
			throw new UsortErrorException("Функция usort() не была выполнена!\n");
		}
		return $this;
	}

	/**
	 * метод для фильтрации сотрудников
	 * работает аналогично array_filter
	 * @param callable $filterFunction callback ф-я
	 * @return $this
	 */
	public function filter(callable $filterFunction):Collection {
		$selectedEmployees = [];
		$selectedEmployees = array_filter($this->employees, $filterFunction);
		$this->employees = $selectedEmployees;	
		return $this;
	}

	/**
	 * метод позволяет взять X сотрудников, начиная с Y
	 * @param int $num сколько взять
	 * @param int $from начиная со скольки
	 * @throws InvalidArgumentException
	 * @return $this
	 */
	public function takeNumItems(int $num, int $from = 1):Collection {
		$numOfEmpls = count($this->employees);
		if ($numOfEmpls == 0) {
			return $this;
		}
		if ($from <= 0 or $from > $numOfEmpls) {
			throw new InvalidArgumentException("Аргумент \$from не должен быть меньше 1 и больше чем общее число сотрудников!\n");	
		}
		if ($num > ($numOfEmpls - $from) + 1 or $num <= 0) {
			throw new InvalidArgumentException("Взять ({$num}) сотрудников, начиная с ({$from}) из (" . $numOfEmpls . ") нельзя!\n");
		}
		$numEmployees = [];
		$this->employees = array_values($this->employees);
		for($i = 0, $key = --$from; $i < $num; $i++, $key++){
			$numEmployees[] = $this->employees[$key];
		}
		$this->employees = $numEmployees;
		return $this;
	}

	/**
	 * Извлечь копии сотрудников
	 * @param int $number сколько сотрудников
	 * @throws InvalidArgumentException
	 * @return array
	 */
	public function extractNumEmployees(int $number = self::ALL):array {
		$clonedEmployees = [];
		$numOfEmpls = count($this->employees);
		if ($numOfEmpls == 0) {
			return [];
		}
		if (($number <= 0 and $number != -1) or $number > $numOfEmpls) {
			throw new InvalidArgumentException("\$number должен быть больше 0 и не больше кол-ва элементов в коллекции!\n");
		}
		if ($number == self::ALL) {
			$number = count($this->employees);
		}
		for($i = 0; $i < $number; $i++){
			$clonedEmployees[] = clone $this->employees[$i];
		}
		return $clonedEmployees;
	}

	/**
	 * Извлечь копию первого элемента
	 * @return array
	 */
	public function extractFirstEmployee():array {
		if (empty($this->employees)) {
			return [];
		}
		return [clone $this->employees[0]];
	}

	/**
	 * Получить массив сотрудников
	 * @return array
	 */
	public function getEmployees():array {
		return $this->employees;
	}

	/**
	* Посчитать элементы коллекции
	* @return int
	*/
	public function countItems():int {
		return count($this->employees);
	}
}

class Department
{	
	/** @var string $name Имя департамента */
	private $name;
	/** @var array $employees массив сотрудников */
	private $employees = [];
	/** ... */
	public function __construct(string $name){
		$this->name = $name;
	}

	/**
	 * Создать сотрудника
	 * @param int $number число сотрудников данного типа
	 * @param string $employeeClassName имя класса-сотрудника
	 * @param string $profession профессия
	 * @param int $rate ставка
	 * ...
	 * @throws CreateEmployeeErrorException
	 */
	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 CreateEmployeeErrorException("Ошибка создания сотрудника!\n");
		}
	}

	/**
	 * Добавить сотрудников
	 * @param array $newEmployees массив объектов типа Employee
	 */
	public function addEmployees(array $newEmployees){
		if (empty($this->employees)) {
			$this->employees = $newEmployees;
		} else {
			foreach ($newEmployees as $newEmployee) {
				foreach ($this->employees as $employee) {
					if ($newEmployee === $employee) {
						continue 2;
					}
					$this->employees[] = $newEmployee;
				}
			}
		}
	}

	/**
	 * Получить коллекцию
	 * @return Collection
	 * @throws CreateCollectionErrorException
	 */
	public function getCollection():Collection {
		if (empty($this->employees)) {
			throw new CreateCollectionErrorException("В департаменте нет сотрудников! Коллекция не может быть создана!\n");
		}
		return new Collection($this->employees);
	}

	/**
	 * Удалить сотрудника
	 * @param Collection $colection удаляемая коллекция
	 */
	public function fireEmployees(Collection $collection){
		$firedEmployees = $collection->getEmployees();
		foreach ($firedEmployees as $firedEmployee) {
			foreach ($this->employees as $key => $employee) {
				if ($employee === $firedEmployee) {
					unset($this->employees[$key]);
					break;			
				}
			}
		}
		$this->employees = array_values($this->employees);
	}

	/**
	 * Изменить сотрудников
	 * @param Collection $collection коллекция для изменения
	 * @param callable $changeFunction callback ф-я
	 */
	public function changeEmployees(Collection $collection, callable $changeFunction){
		foreach ($collection->getEmployees() as $employee) {
			$changeFunction($employee);	
		}
	}

	/**
	 * Разжаловать босса
	 */
	public function devoteBoss(){
		foreach ($this->employees as $employee) {
			if ($employee->isBoss()) {
				$employee->setBossStatus(false);
			}
		}
	}

	/**
	 * Получить имя департамента
	 * @return string
	 */
	public function getName():string {
		return $this->name;
	}

	/**
	 * число сотрудников в департаменте
	 * @return int
	 */
	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
{
	/** @var string $name имя компании */
	private $name;
	/** @var array $departments массив департаментов */
	private $departments;

	/**
	 * Конструктор класса Компании
	 * @param string $name имя компании
	 * @param Department $departments,... департаменты через запятую
	 */
	public function __construct(string $name, Department ...$departments){
		$this->name = $name;
		$this->departments = $departments;
	}

	public function __clone(){
		foreach ($this->departments as $department) {
			$clonedEmployees = [];
			foreach ($department->getCollection()->getEmployees() as $employee) {
				$clonedEmployees[] = clone $employee;	
			}
			$clonedDepartment = new Department($department->getName());
			$clonedDepartment->addEmployees($clonedEmployees);
			$clonedDepartments[] = $clonedDepartment;
		}
		$this->departments = $clonedDepartments;
	}

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

	public function getDepartments():array {
		return $this->departments;
	}
}

/**
 * Добавить пады справа
 * @param string $string форматируемая строка
 * @param int $widthOfCol ширина колонки
 */
function padRight($string, $widthOfCol){
	$lengthOfString = mb_strlen($string);
	if ($lengthOfString <= $widthOfCol) {
		$formattedString = $string . str_repeat(" ", $widthOfCol - $lengthOfString);
		return $formattedString;
	} else {
		return mb_substr($string, 0, $widthOfCol);
	}
}

/**
 * Добавить пады слева
 * @param string $string форматируемая строка
 * @param int $widthOfCol ширина колонки
 */
function padLeft($string, $widthOfCol){
	$lengthOfString = mb_strlen($string);
	if ($lengthOfString <= $widthOfCol) {
		$formattedString = str_repeat(" ", $widthOfCol - $lengthOfString) . $string;
		return $formattedString;
	} else {
		return mb_substr($string, 0, $widthOfCol);
	}
}

/**
 * Вывести отчет по компании
 * @param Company $company объект-компания
 */
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("Логистики1234567");

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

$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);

//Копии для опробирования мер
$company1 = clone $company;
$company2 = clone $company;
$company3 = clone $company;
//Начальные данные
echo "Начальные данные:\n";
displayReport($company);
//План 1
foreach ($company1->getDepartments() as $department) {
	$collection = $department->getCollection()->filter(function($e){return $e->getProfession() == ENGINEER;})->sort(function($e1,$e2){return ($e1->getRank() <=> $e2->getRank());});
	$numOfFired = round($collection->countItems() * 0.40);
	if ($numOfFired != 0) {
		$collection = $collection->takeNumItems($numOfFired);
		$department->fireEmployees($collection);
	}
}
echo "План 1:\n";
displayReport($company1);
//План 2
foreach ($company2->getDepartments() as $department) {
	$analysts = $department->getCollection()->filter(function($e){return $e->getProfession() == ANALYST;})->sort(function($e1,$e2){return -($e1->getRank() <=> $e2->getRank());});
	$department->changeEmployees($analysts, function($e){$e->setRate(1100);$e->setCoffee(75);});
	$bossAndNotAnalyst = $department->getCollection()->filter(function($e){return ($e->isBoss() and $e->getProfession() != ANALYST);});
	if ($bossAndNotAnalyst->countItems() and $analysts->countItems()) {
		$department->devoteBoss();
		$higherAnalyst = $analysts->takeNumItems(1);
		$department->changeEmployees($higherAnalyst, function($e){$e->setBossStatus(true);});
	}
}
echo "План 2:\n";
displayReport($company2);
//План 3
foreach ($company3->getDepartments() as $department) {
	$oneAndTwoRankManagers = $department->getCollection()->filter(function($e){return ($e->getRank() == 1 or $e->getRank() == 2) and $e->getProfession() == MANAGER;});
	$half = round($oneAndTwoRankManagers->countItems() * 0.50);
	if ($half != 0) {
		$raisedManagers = $oneAndTwoRankManagers->takeNumItems($half);
		$department->changeEmployees($raisedManagers, function($e){$e->setRank($e->getRank() + 1);});
	}
}
echo "План 3:\n";
displayReport($company3);