<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\PersonBundle\Entity\SocialWork;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;

#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['social_work_social_action' => SocialAction::class])]
#[ORM\Entity]
#[ORM\Table(name: 'chill_person_social_action')]
class SocialAction
{
    /**
     * @var Collection<int, SocialAction>
     */
    #[ORM\OneToMany(mappedBy: 'parent', targetEntity: SocialAction::class)]
    private Collection $children;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATEINTERVAL, nullable: true)]
    private ?\DateInterval $defaultNotificationDelay = null;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_MUTABLE, nullable: true)]
    private ?\DateTimeInterface $desactivationDate = null;

    /**
     * @var Collection<int, Evaluation>
     */
    #[ORM\ManyToMany(targetEntity: Evaluation::class, inversedBy: 'socialActions')]
    #[ORM\JoinTable(name: 'chill_person_social_work_evaluation_action')]
    private Collection $evaluations;

    /**
     * @var Collection<int, Goal>
     */
    #[ORM\ManyToMany(targetEntity: Goal::class, inversedBy: 'socialActions')]
    #[ORM\JoinTable(name: 'chill_person_social_action_goal')]
    private Collection $goals;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)]
    private ?int $id = null;

    #[ORM\ManyToOne(targetEntity: SocialIssue::class, inversedBy: 'socialActions')]
    private ?SocialIssue $issue = null;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::FLOAT, name: 'ordering', options: ['default' => '0.0'])]
    private float $ordering = 0.0;

    #[ORM\ManyToOne(targetEntity: SocialAction::class, inversedBy: 'children')]
    private ?SocialAction $parent = null;

    /**
     * @var Collection<int, Result>
     */
    #[ORM\ManyToMany(targetEntity: Result::class, inversedBy: 'socialActions')]
    #[ORM\JoinTable(name: 'chill_person_social_action_result')]
    private Collection $results;

    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON)]
    private array $title = [];

    public function __construct()
    {
        $this->children = new ArrayCollection();
        $this->goals = new ArrayCollection();
        $this->results = new ArrayCollection();
        $this->evaluations = new ArrayCollection();
    }

    public function addChild(self $child): self
    {
        if (!$this->children->contains($child)) {
            $this->children[] = $child;
            $child->setParent($this)->setIssue($this->getIssue());
        }

        return $this;
    }

    public function addEvaluation(Evaluation $evaluation): self
    {
        if (!$this->evaluations->contains($evaluation)) {
            $this->evaluations[] = $evaluation;
            $evaluation->addSocialAction($this);
        }

        return $this;
    }

    public function addGoal(Goal $goal): self
    {
        if (!$this->goals->contains($goal)) {
            $this->goals[] = $goal;
        }

        return $this;
    }

    public function addResult(Result $result): self
    {
        if (!$this->results->contains($result)) {
            $this->results[] = $result;
        }

        return $this;
    }

    /**
     * In a SocialIssues's collection, find the elements which are an ancestor of
     * other elements.
     *
     * The difference of the given list (thus, the elements which are **not** kept
     * in the returned collection) are the most-grand-child elements of the list.
     *
     * Removing those elements of the Collection (which is not done by this method)
     * will ensure that only the most descendent elements are present in the collection,
     * (any ancestor of another element are present).
     *
     * @param Collection|SocialAction[] $socialActions
     *
     * @return Collection|SocialAction[] a list with the elements of the given list which are parent of other elements in the given list
     */
    public static function findAncestorSocialActions(array|Collection $socialActions): Collection
    {
        $ancestors = new ArrayCollection();

        foreach ($socialActions as $candidateChild) {
            if ($ancestors->contains($candidateChild)) {
                continue;
            }

            foreach ($socialActions as $candidateParent) {
                if ($ancestors->contains($candidateParent)) {
                    continue;
                }

                if ($candidateChild->isDescendantOf($candidateParent)) {
                    $ancestors->add($candidateParent);
                }
            }
        }

        return $ancestors;
    }

    /**
     * @return Collection|self[]
     */
    public function getChildren(): Collection
    {
        return $this->children;
    }

    public function getDefaultNotificationDelay(): ?\DateInterval
    {
        return $this->defaultNotificationDelay;
    }

    public function getDesactivationDate(): ?\DateTimeInterface
    {
        return $this->desactivationDate;
    }

    /**
     * @return Collection|self[] All the descendants (children, children of children, ...)
     */
    public function getDescendants(): Collection
    {
        $descendants = new ArrayCollection();

        foreach ($this->getChildren() as $child) {
            if (!$descendants->contains($child)) {
                $descendants->add($child);

                foreach ($child->getDescendants() as $descendantsOfChild) {
                    if (!$descendants->contains($descendantsOfChild)) {
                        $descendants->add($descendantsOfChild);
                    }
                }
            }
        }

        return $descendants;
    }

    /**
     * @return Collection|self[] All the descendants including the current entity (this)
     */
    public function getDescendantsWithThis(): Collection
    {
        $descendants = $this->getDescendants();

        if (!$descendants->contains($this)) {
            $descendants->add($this);
        }

        return $descendants;
    }

    /**
     * @param Collection|SocialAction[] $socialActions
     */
    public static function getDescendantsWithThisForActions(array|Collection $socialActions): Collection
    {
        $unique = [];

        foreach ($socialActions as $action) {
            foreach ($action->getDescendantsWithThis() as $child) {
                $unique[spl_object_hash($child)] = $child;
            }
        }

        return new ArrayCollection(array_values($unique));
    }

    public function getEvaluations(): Collection
    {
        return $this->evaluations;
    }

    /**
     * @return Collection|Goal[]
     */
    public function getGoals(): Collection
    {
        return $this->goals;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getIssue(): ?SocialIssue
    {
        return $this->issue;
    }

    public function getOrdering(): float
    {
        return $this->ordering;
    }

    public function getParent(): ?self
    {
        return $this->parent;
    }

    /**
     * @return Collection|Result[]
     */
    public function getResults(): Collection
    {
        return $this->results;
    }

    public function getTitle(): array
    {
        return $this->title;
    }

    public function hasChildren(): bool
    {
        return 0 < $this->getChildren()->count();
    }

    public function isDesactivated(\DateTime $atDate): bool
    {
        if (null !== $this->desactivationDate && $this->desactivationDate < $atDate) {
            return true;
        }

        if ($this->hasParent()) {
            return $this->parent->isDesactivated($atDate);
        }

        return false;
    }

    public function hasParent(): bool
    {
        return $this->getParent() instanceof self;
    }

    /**
     * Recursive method which return true if the current $action
     * is a descendant of the $action given in parameter.
     */
    public function isDescendantOf(SocialAction $action): bool
    {
        if (!$this->hasParent()) {
            return false;
        }

        if ($this->getParent() === $action) {
            return true;
        }

        return $this->getParent()->isDescendantOf($action);
    }

    public function removeChild(self $child): self
    {
        if ($this->children->removeElement($child)) {
            // set the owning side to null (unless already changed)
            if ($child->getParent() === $this) {
                $child->setParent(null);
            }
        }

        return $this;
    }

    public function removeEvaluation(Evaluation $evaluation): self
    {
        $this->evaluations->removeElement($evaluation);
        $evaluation->removeSocialAction($this);

        return $this;
    }

    public function removeGoal(Goal $goal): self
    {
        $this->goals->removeElement($goal);

        return $this;
    }

    public function removeResult(Result $result): self
    {
        $this->results->removeElement($result);

        return $this;
    }

    public function setDefaultNotificationDelay(\DateInterval $defaultNotificationDelay): self
    {
        $this->defaultNotificationDelay = $defaultNotificationDelay;

        return $this;
    }

    public function setDesactivationDate(?\DateTimeInterface $desactivationDate): self
    {
        $this->desactivationDate = $desactivationDate;

        return $this;
    }

    public function setIssue(?SocialIssue $issue): self
    {
        $this->issue = $issue;

        foreach ($this->getChildren() as $child) {
            $child->setIssue($issue);
        }

        return $this;
    }

    public function setOrdering(float $ordering): SocialAction
    {
        $this->ordering = $ordering;

        return $this;
    }

    /**
     * @internal use $parent->addChild() instead (@see{self::addChild()})
     */
    public function setParent(?self $parent): self
    {
        $this->parent = $parent;

        $parent->addChild($this);

        return $this;
    }

    public function setTitle(array $title): self
    {
        $this->title = $title;

        return $this;
    }

    public static function filterRemoveDeactivatedActions(array|ReadableCollection $actions, \DateTime $comparisonDate): array|ReadableCollection
    {
        $filterFn = fn (SocialAction $socialAction) => !$socialAction->isDesactivated($comparisonDate);

        return match ($actions instanceof ReadableCollection) {
            true => $actions->filter($filterFn),
            false => array_filter($actions, $filterFn),
        };
    }
}
