<?php
/*
    Condorcet PHP - Election manager and results calculator.
    Designed for the Condorcet method. Integrating a large number of algorithms extending Condorcet. Expandable for all types of voting systems.

    By Julien Boudry and contributors - MIT LICENSE (Please read LICENSE.txt)
    https://github.com/julien-boudry/Condorcet
*/

declare(strict_types=1);

namespace CondorcetPHP\Condorcet\DataManager;

use CondorcetPHP\Condorcet\{Vote};
use CondorcetPHP\Condorcet\Dev\CondorcetDocumentationGenerator\CondorcetDocAttributes\{Throws};
use CondorcetPHP\Condorcet\ElectionProcess\ElectionState;
use CondorcetPHP\Condorcet\Throwable\VoteManagerException;
use CondorcetPHP\Condorcet\Tools\Converters\CondorcetElectionFormat;

class VotesManager extends ArrayManager
{
    /////////// Data CallBack for external drivers ///////////

    protected function decodeOneEntity(string $data): Vote
    {
        $vote = new Vote($data);
        $vote->registerLink($this->getElection());
        $vote->notUpdate = true;
        $this->getElection()->checkVoteCandidate($vote);
        $vote->notUpdate = false;

        return $vote;
    }

    protected function encodeOneEntity(Vote $data): string
    {
        $data->destroyLink($this->getElection());

        return str_replace([' > ', ' = '], ['>', '='], (string) $data);
    }

    protected function preDeletedTask(Vote $object): void
    {
        $object->destroyLink($this->getElection());
    }

    /////////// Array Access - Specials improvements ///////////

    public function offsetGet(mixed $offset): Vote
    {
        return parent::offsetGet($offset);
    }

    #[Throws(VoteManagerException::class)]
    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($value instanceof Vote) {
            parent::offsetSet((\is_int($offset) ? $offset : null), $value);
            $this->UpdateAndResetComputing(key: $this->maxKey, type: 1);
        } else {
            throw new VoteManagerException;
        }

        $this->checkRegularize();
    }

    public function offsetUnset(mixed $offset): void
    {
        $this->UpdateAndResetComputing(key: $offset, type: 2);
        parent::offsetUnset($offset);
    }

    /////////// Internal Election related methods ///////////

    public function UpdateAndResetComputing(int $key, int $type): void
    {
        $election = $this->getElection();


        if ($election->getState() === ElectionState::VOTES_REGISTRATION) {
            if ($type === 1) {
                $election->getPairwise()->addNewVote($key);
            } elseif ($type === 2) {
                $election->getPairwise()->removeVote($key);
            }

            $election->cleanupCalculator();
        } else {
            $election->setStateToVote();
        }
    }


    /////////// Get Votes Methods ///////////

    public function getVoteKey(Vote $vote): ?int
    {
        ($r = array_search(needle: $vote, haystack: $this->Container, strict: true)) !== false || ($r = array_search(needle: $vote, haystack: $this->Cache, strict: true));

        return ($r !== false) ? $r : null;
    }

    protected function getFullVotesListGenerator(): \Generator
    {
        foreach ($this as $voteKey => $vote) {
            yield $voteKey => $vote;
        }
    }

    protected function getPartialVotesListGenerator(array $tags, bool $with): \Generator
    {
        foreach ($this as $voteKey => $vote) {
            $noOne = true;
            foreach ($tags as $oneTag) {
                if (($oneTag === $voteKey) || \in_array(needle: $oneTag, haystack: $vote->getTags(), strict: true)) {
                    if ($with) {
                        yield $voteKey => $vote;
                        break;
                    } else {
                        $noOne = false;
                    }
                }
            }

            if (!$with && $noOne) {
                yield $voteKey => $vote;
            }
        }
    }

    // Get the votes list
    public function getVotesList(?array $tags = null, bool $with = true): array
    {
        if ($tags === null) {
            return $this->getFullDataSet();
        } else {
            $search = [];

            foreach ($this->getPartialVotesListGenerator($tags, $with) as $voteKey => $vote) {
                $search[$voteKey] = $vote;
            }

            return $search;
        }
    }

    // Get the votes list as a generator object
    public function getVotesListGenerator(?array $tags = null, bool $with = true): \Generator
    {
        if ($tags === null) {
            return $this->getFullVotesListGenerator();
        } else {
            return $this->getPartialVotesListGenerator($tags, $with);
        }
    }

    public function getVotesValidUnderConstraintGenerator(?array $tags = null, bool $with = true): \Generator
    {
        $election = $this->getElection();
        $generator = ($tags === null) ? $this->getFullVotesListGenerator() : $this->getPartialVotesListGenerator($tags, $with);

        foreach ($generator as $voteKey => $oneVote) {
            if (!$election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
                continue;
            }

            yield $voteKey => $oneVote;
        }
    }

    public function getVotesListAsString(bool $withContext): string
    {
        $election = $this->getElection();

        $simpleList = '';

        $weight = [];
        $nb = [];

        foreach ($this as $oneVote) {
            $oneVoteString = $oneVote->getSimpleRanking($withContext ? $election : null);

            if (!\array_key_exists(key: $oneVoteString, array: $weight)) {
                $weight[$oneVoteString] = 0;
            }
            if (!\array_key_exists(key: $oneVoteString, array: $nb)) {
                $nb[$oneVoteString] = 0;
            }

            if ($election->isVoteWeightAllowed()) {
                $weight[$oneVoteString] += $oneVote->getWeight();
            } else {
                $weight[$oneVoteString]++;
            }

            $nb[$oneVoteString]++;
        }

        ksort($weight, \SORT_NATURAL);
        arsort($weight);

        $isFirst = true;
        foreach ($weight as $key => $value) {
            if (!$isFirst) {
                $simpleList .= "\n";
            }
            $voteString = ($key === '') ? CondorcetElectionFormat::SPECIAL_KEYWORD_EMPTY_RANKING : $key;
            $simpleList .= $voteString.' * '.$nb[$key];
            $isFirst = false;
        }

        return $simpleList;
    }

    public function countVotes(?array $tag, bool $with): int
    {
        if ($tag === null) {
            return \count($this);
        } else {
            $count = 0;

            foreach ($this as $key => $value) {
                $noOne = true;
                foreach ($tag as $oneTag) {
                    if (($oneTag === $key) || \in_array(needle: $oneTag, haystack: $value->getTags(), strict: true)) {
                        if ($with) {
                            $count++;
                            break;
                        } else {
                            $noOne = false;
                        }
                    }
                }

                if (!$with && $noOne) {
                    $count++;
                }
            }

            return $count;
        }
    }

    public function countInvalidVoteWithConstraints(): int
    {
        $election = $this->getElection();
        $count = 0;

        foreach ($this as $oneVote) {
            if (!$election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
                $count++;
            }
        }

        return $count;
    }

    public function sumVotesWeight(bool $constraint = false): int
    {
        $election = $this->getElection();
        $sum = 0;

        foreach ($this as $oneVote) {
            if (!$constraint || $election->testIfVoteIsValidUnderElectionConstraints($oneVote)) {
                $sum += $election->isVoteWeightAllowed() ? $oneVote->getWeight() : 1;
            }
        }

        return $sum;
    }
}
