<?php
declare(strict_types=1);
namespace App\Entity\Core;
use App\Entity\Core\Topic\Topic;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
use App\Services\Student\Transform;
#[ORM\Table('mathproblem')]
#[ORM\Entity(repositoryClass: MathproblemRepository::class)]
class Mathproblem implements JsonSerializable, HasMediaInterface
{
use HasMediaTraits;
const MATH_PROBLEM_QUIZ = 1;
const MATH_PROBLEM_WORKSHEET = 2;
const MATH_PROBLEM_QUIZ_AND_WORKSHEET = 3;
const STATUS_PUBLISHED = 0;
const STATUS_UNPUBLISHED = 1;
const STATUS_DELETED = 2;
/**
* @var int
*/
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private $id;
/**
* @var bool
*/
#[ORM\Column(name: 'validated', type: 'boolean', nullable: true)]
private $validated;
/**
* @var string
*/
#[ORM\Column(name: 'name', type: 'string', nullable: false)]
private $name;
/**
* @var string
*/
#[ORM\Column(name: 'description', type: 'string', length: 8000, nullable: true)]
private $description;
/**
* The possible choices for this Mathproblem, will only be populated when type is 'multi'.
*
* @var ArrayCollection
*/
#[ORM\OneToMany(targetEntity: Answer::class, mappedBy: 'mathproblem', cascade: ['persist', 'remove'])]
private $answers;
/**
* @var ArrayCollection
*/
#[ORM\OneToMany(targetEntity: BugReport::class, mappedBy: 'problem', cascade: ['persist', 'remove'])]
private $bugReports;
#[ORM\JoinColumn(nullable: true)]
#[ORM\ManyToOne(targetEntity: Video::class)]
private ?Video $video;
/**
* @var string
*/
#[ORM\Column(name: 'hint', type: 'string', length: 2000, nullable: true)]
private $hint;
/**
* @var string
*/
#[ORM\Column(name: 'video_hint', type: 'string', length: 300, nullable: true)]
private $videoHint;
/**
* @var string
*/
#[ORM\Column(name: 'solution', type: 'string', length: 3000, nullable: true)]
private $solution;
/**
* @var string
*/
#[ORM\Column(name: 'free_text', type: 'text', nullable: true)]
private $freeText;
/**
* @var string
*/
#[ORM\Column(name: 'intro_text', type: 'text', nullable: true)]
private $introText;
/**
* Mathproblem template
*/
#[ORM\JoinColumn(name: 'template_id', referencedColumnName: 'id')]
#[ORM\ManyToOne(targetEntity: TemplateMathproblem::class, inversedBy: 'mathproblems')]
private $template;
/**
* Relative path to the GeoGebra problem script file.
*
* @var string
*/
#[ORM\Column(type: 'string', nullable: true)]
private $geogebra;
/**
* 0 - available/published
* 1 - unpublished
* 2 - deleted
*
* @var ?int
*/
#[ORM\Column(name: 'status', type: 'integer', nullable: true)]
private $status;
/**
* @var int
*/
#[ORM\Column(name: 'subquestion_nr', type: 'smallint', nullable: true)]
private $subquestionNr;
/**
* Flag used to check if there's any next or previous problem
* @var bool
*/
private $hasNext;
/**
* Flag used to control if the problem should be searchable in a quiz
* @var bool
*/
#[ORM\Column(name: 'allow_in_quiz_or_worksheet', type: 'boolean', nullable: true)]
private $allowInQuizOrWorksheet;
#[ORM\OneToMany(targetEntity: RelationQuizMathproblem::class, mappedBy: 'mathproblem')]
private Collection $quizzes;
/**
* @var ArrayCollection
*/
#[ORM\JoinTable(name: 'problems_appendices')]
#[ORM\JoinColumn(name: 'problem_id', referencedColumnName: 'id')]
#[ORM\InverseJoinColumn(name: 'appendix_id', referencedColumnName: 'id')]
#[ORM\ManyToMany(targetEntity: Appendix::class, inversedBy: 'problems', cascade: ['persist', 'remove'])]
private $appendices;
#[ORM\JoinColumn(name: 'video_reference', referencedColumnName: 'id', nullable: true)]
#[ORM\OneToOne(targetEntity: VideoReference::class, inversedBy: 'mathproblem')]
private ?VideoReference $videoReference;
/**
* @var DateTime
*/
#[ORM\Column(name: 'createdAt', type: 'datetime', nullable: false)]
private DateTime $createdAt;
/**
* 0 = default view
* 1,2,3,... = alternate views, that can vary by problem type
*
* @var int
*/
#[ORM\Column(name: 'view_mode', type: 'smallint', options: ['default' => 0])]
private int $viewMode = 0;
public array $excludeKeys = [];
protected bool $useTransform = false;
protected Transform $transform;
#[ORM\OneToOne(targetEntity: ProblemDnd::class, mappedBy: 'problem')]
private $problemDnd;
static function setTransform(Transform $transform){
self::$transform = $transform;
}
public function __construct()
{
$this->answers = new ArrayCollection();
$this->appendices = new ArrayCollection();
$this->quizzes = new ArrayCollection();
$this->imageReference = null;
$this->videoReference = null;
$this->audioReference = null;
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
public function setName(string $name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description ?? '';
}
public function setDescription(string $description)
{
$this->description = $description;
}
public function getAnswers(): Collection
{
return $this->answers;
}
public function getHint(): ?string
{
return $this->hint;
}
public function setHint(?string $hint)
{
$this->hint = $hint;
}
public function getVideoHint(): ?string
{
return $this->videoHint;
}
public function setVideoHint(?string $videoHint): void
{
$this->videoHint = $videoHint;
}
public function getSolution(): ?string
{
return $this->solution;
}
public function setSolution(?string $solution)
{
$this->solution = $solution;
}
/**
* @return TemplateMathproblem
*/
public function getTemplate()
{
return $this->template;
}
public function setTemplate(TemplateMathproblem $template)
{
$this->template = $template;
}
/**
* @return ?int
*/
public function getStatus()
{
return $this->status;
}
/**
* @param ?int $status
*/
public function setStatus($status)
{
$this->status = $status;
}
/**
* Status helper function
*/
public function isPublished(): bool
{
// Treating null as published since status is nullable, and it would have been treated like this before, though it should not happen.
return $this->status === self::STATUS_PUBLISHED || $this->status === null;
}
/**
* Status helper function
*/
public function setPublished(): void
{
$this->status = self::STATUS_PUBLISHED;
}
/**
* Status helper function
*/
public function isUnpublished(): bool
{
return $this->status === self::STATUS_UNPUBLISHED;
}
/**
* Status helper function
*/
public function setUnpublished(): void
{
$this->status = self::STATUS_UNPUBLISHED;
}
/**
* Status helper function
*/
public function isDeleted(): bool
{
return $this->status === self::STATUS_DELETED;
}
/**
* Status helper function
*/
public function setDeleted(): void
{
$this->status = self::STATUS_DELETED;
}
/**
* @return int
*/
public function getSubquestionNr()
{
return $this->subquestionNr;
}
/**
* @param int $subquestionNr
*
* Specify if the math problem is part of a bigger question with several subquestions
*
*/
public function setSubquestionNr($subquestionNr)
{
$this->subquestionNr = $subquestionNr;
}
public function getGeogebra(): ?string
{
return $this->geogebra;
}
public function getFreeText(): ?string
{
return $this->freeText;
}
public function setFreeText(?string $freeText): void
{
$this->freeText = $freeText;
}
public function getIntroText(): ?string
{
return $this->introText;
}
public function setIntroText(?string $introText): void
{
$this->introText = $introText;
}
public function getCorrectAnswer(): ?Answer
{
$correctAnswer = null;
foreach ($this->answers as $answer) {
if ($answer->getIsCorrect()) {
$correctAnswer = $answer;
break;
}
}
return $correctAnswer;
}
public function getWrongAnswer(): ?Answer
{
$wrongAnswer = null;
foreach ($this->answers as $answer) {
if (!$answer->getIsCorrect()) {
$wrongAnswer = $answer;
break;
}
}
return $wrongAnswer;
}
public function getCorrectAnswers(): ?array
{
// TODO - Jon's part, try to optimize for reusing
$answerString = $this->getCorrectAnswer()->getAnswerText();
$type = $this->getTemplate()->getType();
if ($type === TemplateMathproblem::TYPE_DROPDOWN) {
// Get correct answers dynamically
$stringArray = explode("#", $answerString);
$correctAnswers = [];
$index = 0;
foreach ($stringArray as $str) {
// if string contains '|', it contains dropdown options
if (!(strpos($str, '|') === false)) {
$dropdownChoices = explode("|", $str);
$correctAnswers[$index] = $dropdownChoices[0];
foreach ($dropdownChoices as $choice){
if(preg_match("#^\*#", $choice) || preg_match("#\*$#", $choice)){
$choice = preg_replace("#^\*#", "", $choice);
$choice = preg_replace("#\*$#", "", $choice);
$correctAnswers[$index] = $choice;
}
}
$index++;
}
}
return $correctAnswers;
} elseif ($type === TemplateMathproblem::TYPE_WORD_OPTIONS) {
$stringArray = explode("#", $answerString);
$correctAnswers = [];
for ($i = 1; $i < count($stringArray); $i += 2) {
$str = $stringArray[$i];
if (in_array($str, ['<br>', '<br/>'])) {
continue;
}
if (strpos($str, '|') === false) {
$correctAnswers[] = 'default';
} else {
$correctAnswers[] = explode('|', $str)[1];
}
}
return $correctAnswers;
} else {
// TODO: perhaps make this possible for other template types too.
return null;
}
}
/**
* @return bool
*/
public function getHasNext(){
return $this->hasNext;
}
public function setHasNext($hasNext){
$this->hasNext = $hasNext;
}
public function getAppendices(): Collection
{
return $this->appendices;
}
public function setAppendices(ArrayCollection $appendices): void
{
$this->appendices = $appendices;
}
public function getVideo(): ?Video
{
return $this->video;
}
function jsonSerialize(): mixed
{
// Initialize the $video property if it's not already initialized.
$this->video ??= null;
$data = [
"id" => $this->getId(),
"name" => $this->getName(),
"description" => $this->getDescription(),
"introText" => $this->getIntroText(),
"freeText" => $this->getFreeText(),
"hint" => $this->getHint(),
"correctAnswer" => $this->getCorrectAnswer(),
"template" => $this->getTemplate(),
"status" => $this->getStatus(),
"subquestionNr" => $this->getSubquestionNr(),
"georgeBra" => $this->getGeogebra(),
"solution" => $this->getSolution(),
"image" => $this->getImagePath(),
"appendices" => $this->getAppendices()->toArray(),
"hasNext" => $this->getHasNext(),
"video" => $this->getVideo(),
"wrongAnswer" => $this->getWrongAnswer(),
"topicDisplayName" => $this->getTemplate()->getTopic()->getDisplayName(),
"options" => $this->getCorrectAnswer() && $this->getCorrectAnswer()->getAnswerText() ? explode("#", $this->getCorrectAnswer()->getAnswerText()) : [],
"viewMode" => $this->getViewMode(),
];
for ($i=0, $count = count($this->excludeKeys); $i < $count; $i++){
if(isset( $data[ $this->excludeKeys[$i] ] )){
unset( $data[ $this->excludeKeys[$i] ] );
}
}
$answers = $this->getAnswers()->toArray();
if($this->getTemplate()->getShouldShuffle()){
shuffle($answers);
}
$data['answers'] = $answers;
return $data;
}
/**
* @return $this
*/
function exclude(array $keys): self{
$this->excludeKeys = $keys;
return $this;
}
/**
* @return $this
*/
function setUseTransform(bool $useTransform = true) : self{
$this->useTransform = $useTransform;
return $this;
}
public function getAllowInQuizOrWorksheet(): ?bool
{
return $this->allowInQuizOrWorksheet;
}
public function setAllowInQuizOrWorksheet(?bool $allowInQuizOrWorksheet): void
{
$this->allowInQuizOrWorksheet = $allowInQuizOrWorksheet;
}
public function getViewMode(): int
{
return $this->viewMode;
}
public function getDifficulty() {
return $this->template->getDifficulty();
}
public function getCorrectAnswerText() {
return $this->getCorrectAnswer()?->getAnswerText();
}
/*
* Used for EasyAdmin problem form
*
* */
public function getTemplateId() {
return $this->getTemplate()->getId();
}
public function getType() {
return $this?->getTemplate()?->getType();
}
public function setType(int $type) {
$this->getTemplate()->setType($type);
}
public function getQuizzes(): Collection
{
$quizzes = new ArrayCollection();
foreach ($this->quizzes as $quiz) {
$quizzes->add($quiz->getQuiz());
}
return $quizzes;
}
public function getQuizId(): ?int {
$quizzes = $this->getQuizzes();
return $quizzes[0]?->getId();
}
public function getPublicationName(): string {
$quizzes = $this->getQuizzes();
return $quizzes[0]?->getPublication()?->getName();
}
public function isValidated(): bool
{
return $this->validated ?? false;
}
public function setValidated(bool $validated): void
{
$this->validated = $validated;
}
public function getProblemText(): string {
return '';
}
public function setProblemText(string $text) {
}
public function __toString(): string
{
return $this->name;
}
public function setViewMode(int $viewMode): void
{
$this->viewMode = $viewMode;
}
public function getSnippet(): string
{
// Placeholder for EasyAdmin, value will be overwritten, so it does not matter what is returned.
return '';
}
/**
* @return ?ProblemDnd
*/
public function getProblemDnd()
{
return $this->problemDnd;
}
public function getVideoReference(): ?VideoReference
{
return $this->videoReference;
}
public function setVideoReference(?VideoReference $videoReference): void
{
$this->videoReference = $videoReference;
}
public function isValidatedAndHasMediaFiles(): bool
{
if (!$this->isValidated()) {
return false;
}
// detect if problem has media files
if ($this->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($this->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
// detect if answers have media files
foreach ($this->getAnswers() as $answer) {
if ($answer->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($answer->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
}
// detect if containers have media files if dnd
if ($this->getType() === 4) {
$bins = $this->getProblemDnd()->getBins();
foreach ($bins as $bin) {
if ($bin->mediaCouplingExistButWithoutFile(ImageReference::class)) {
return false;
}
if ($bin->mediaCouplingExistButWithoutFile(AudioReference::class)) {
return false;
}
}
}
return $this->isValidated();
}
public function getActiveBugReports(): array
{
$bugReports = $this->bugReports;
$activeBugReports = [];
foreach ($bugReports as $bugReport) {
if ($bugReport->getDateResolved() === null) {
$activeBugReports[] = $bugReport->getId();
}
}
return $activeBugReports;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
public function getTopic(): ?string
{
return $this->getTemplate()?->getTopic()?->getName();
}
public function setTopic(Topic $topic)
{
$this->getTemplate()?->setTopic($topic);
}
public function getCategory(): ?string
{
return $this->getTemplate()?->getTopic()?->getCategory()?->getName();
}
public function getImage(){
return $this->getImagePath();
}
public function getCmsImagePath(): string
{
if (!$this->imageReference) {
return 'Mangler';
}
if (!$this->imageReference->getFilePath()) {
return 'Mangler fil';
}
return $this->getImagePath();
}
public function getCmsAudioPath(): string
{
if (!$this->audioReference) {
return 'Mangler';
}
if (!$this->audioReference->getFilePath()) {
return 'Mangler fil';
}
return $this->getAudioPath();
}
}