<?php

/*
 * This file is part of the Kimai time-tracking app.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace App\Entity;

use App\Export\ExportItemInterface;
use App\Validator\Constraints as Constraints;
use DateTime;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use JMS\Serializer\Annotation as Serializer;
use Swagger\Annotations as SWG;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Table(name="kimai2_timesheet",
 *     indexes={
 *          @ORM\Index(columns={"user"}),
 *          @ORM\Index(columns={"activity_id"}),
 *          @ORM\Index(columns={"user","start_time"}),
 *          @ORM\Index(columns={"start_time"}),
 *          @ORM\Index(columns={"start_time","end_time"}),
 *          @ORM\Index(columns={"start_time","end_time","user"}),
 *          @ORM\Index(columns={"date_tz","user"}),
 *     }
 * )
 * @ORM\Entity(repositoryClass="App\Repository\TimesheetRepository")
 * @ORM\HasLifecycleCallbacks()
 * @Constraints\Timesheet
 *
 * @Serializer\ExclusionPolicy("all")
 * @Serializer\VirtualProperty(
 *      "ActivityAsId",
 *      exp="object.getActivity() === null ? null : object.getActivity().getId()",
 *      options={
 *          @Serializer\SerializedName("activity"),
 *          @Serializer\Type(name="integer"),
 *          @Serializer\Groups({"Not_Expanded"})
 *      }
 * )
 * @Serializer\VirtualProperty(
 *      "ProjectAsId",
 *      exp="object.getProject() === null ? null : object.getProject().getId()",
 *      options={
 *          @Serializer\SerializedName("project"),
 *          @Serializer\Type(name="integer"),
 *          @Serializer\Groups({"Not_Expanded"})
 *      }
 * )
 * @Serializer\VirtualProperty(
 *      "UserAsId",
 *      exp="object.getUser().getId()",
 *      options={
 *          @Serializer\SerializedName("user"),
 *          @Serializer\Type(name="integer"),
 *          @Serializer\Groups({"Default"})
 *      }
 * )
 * @Serializer\VirtualProperty(
 *      "TagsAsArray",
 *      exp="object.getTagsAsArray()",
 *      options={
 *          @Serializer\SerializedName("tags"),
 *          @Serializer\Type(name="array<string>"),
 *          @Serializer\Groups({"Default"})
 *      }
 * )
 */
class Timesheet implements EntityWithMetaFields, ExportItemInterface
{
    /**
     * Category: Normal work-time (default category)
     */
    public const WORK = 'work';
    /**
     * Category: Holiday
     */
    public const HOLIDAY = 'holiday';
    /**
     * Category: Sickness
     */
    public const SICKNESS = 'sickness';
    /**
     * Category: Parental leave
     */
    public const PARENTAL = 'parental';
    /**
     * Category: Overtime reduction
     */
    public const OVERTIME = 'overtime';

    public const BILLABLE_AUTOMATIC = 'auto';
    public const BILLABLE_YES = 'yes';
    public const BILLABLE_NO = 'no';
    public const BILLABLE_DEFAULT = 'default';

    /**
     * @var int|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;
    /**
     * Reflects the date in the users timezone (not in UTC).
     * This value is automatically set through the begin column and ONLY used in statistic queries.
     *
     * @var \DateTime
     *
     * @ORM\Column(name="date_tz", type="date", nullable=false)
     * @Assert\NotNull()
     */
    private $date;
    /**
     * @var DateTime
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     * @Serializer\Type(name="DateTime")
     * @Serializer\Accessor(getter="getBegin")
     *
     * Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
     *
     * @ORM\Column(name="start_time", type="datetime", nullable=false)
     * @Assert\NotNull()
     */
    private $begin;
    /**
     * @var DateTime|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     * @Serializer\Type(name="DateTime")
     * @Serializer\Accessor(getter="getEnd")
     *
     * Attention: Accessor MUST be used, otherwise date will be serialized in UTC.
     *
     * @ORM\Column(name="end_time", type="datetime", nullable=true)
     */
    private $end;
    /**
     * @var string
     * @internal for storing the timezone of "begin" and "end" date
     *
     * @ORM\Column(name="timezone", type="string", length=64, nullable=false)
     * @Assert\Timezone
     */
    private $timezone;
    /**
     * @var bool
     * @internal for storing the localized state of dates (see $timezone)
     */
    private $localized = false;
    /**
     * @var int|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="duration", type="integer", nullable=true)
     * @Assert\GreaterThanOrEqual(0)
     */
    private $duration = 0;
    /**
     * @var User
     *
     * @ORM\ManyToOne(targetEntity="App\Entity\User")
     * @ORM\JoinColumn(name="`user`", referencedColumnName="id", onDelete="CASCADE", nullable=false)
     * @Assert\NotNull()
     */
    private $user;
    /**
     * @var Activity
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Subresource", "Expanded"})
     * @SWG\Property(ref="#/definitions/ActivityExpanded")
     *
     * @ORM\ManyToOne(targetEntity="App\Entity\Activity")
     * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
     * @Assert\NotNull()
     */
    private $activity;
    /**
     * @var Project
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Subresource", "Expanded"})
     * @SWG\Property(ref="#/definitions/ProjectExpanded")
     *
     * @ORM\ManyToOne(targetEntity="App\Entity\Project")
     * @ORM\JoinColumn(onDelete="CASCADE", nullable=false)
     * @Assert\NotNull()
     */
    private $project;
    /**
     * @var string|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="description", type="text", nullable=true)
     */
    private $description;
    /**
     * @var float
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="rate", type="float", nullable=false)
     * @Assert\GreaterThanOrEqual(0)
     */
    private $rate = 0.00;
    /**
     * @var float|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="internal_rate", type="float", nullable=true)
     */
    private $internalRate;
    /**
     * @var float|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Entity"})
     *
     * @ORM\Column(name="fixed_rate", type="float", nullable=true)
     * @Assert\GreaterThanOrEqual(0)
     */
    private $fixedRate = null;
    /**
     * @var float|null
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Entity"})
     *
     * @ORM\Column(name="hourly_rate", type="float", nullable=true)
     * @Assert\GreaterThanOrEqual(0)
     */
    private $hourlyRate = null;
    /**
     * @var bool
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="exported", type="boolean", nullable=false, options={"default": false})
     * @Assert\NotNull()
     */
    private $exported = false;
    /**
     * @var bool
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Default"})
     *
     * @ORM\Column(name="billable", type="boolean", nullable=false, options={"default": true})
     * @Assert\NotNull()
     */
    private $billable = true;
    /**
     * Internal property used to determine whether the billable field should be calculated automatically.
     * @var string
     */
    private $billableMode = self::BILLABLE_DEFAULT;
    /**
     * @var string
     *
     * @ORM\Column(name="category", type="string", length=10, nullable=false, options={"default": "work"})
     * @Assert\NotNull()
     */
    private $category = self::WORK;
    /**
     * @var DateTime|null
     * @internal used for limiting queries, eg. via API sync
     *
     * @Gedmo\Timestampable
     * @ORM\Column(name="modified_at", type="datetime", nullable=true)
     */
    private $modifiedAt;
    /**
     * Tags
     *
     * @var Tag[]|ArrayCollection
     *
     * @ORM\ManyToMany(targetEntity="App\Entity\Tag", inversedBy="timesheets", cascade={"persist"})
     * @ORM\JoinTable(
     *  name="kimai2_timesheet_tags",
     *  joinColumns={
     *      @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id", onDelete="CASCADE")
     *  },
     *  inverseJoinColumns={
     *      @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE")
     *  }
     * )
     * @Assert\Valid()
     */
    private $tags;
    /**
     * Meta fields
     *
     * All visible meta (custom) fields registered with this timesheet
     *
     * @var TimesheetMeta[]|Collection
     *
     * @Serializer\Expose()
     * @Serializer\Groups({"Timesheet"})
     * @Serializer\Type(name="array<App\Entity\TimesheetMeta>")
     * @Serializer\SerializedName("metaFields")
     * @Serializer\Accessor(getter="getVisibleMetaFields")
     *
     * @ORM\OneToMany(targetEntity="App\Entity\TimesheetMeta", mappedBy="timesheet", cascade={"persist"})
     */
    private $meta;

    public function __construct()
    {
        $this->tags = new ArrayCollection();
        $this->meta = new ArrayCollection();
    }

    /**
     * Get entry id, returns null for new entities which were not persisted.
     *
     * @return int|null
     */
    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * Make sure begin and end date have the correct timezone.
     * This will be called once for each item after being loaded from the database.
     */
    protected function localizeDates()
    {
        if ($this->localized) {
            return;
        }

        if (null !== $this->begin) {
            $this->begin->setTimeZone(new DateTimeZone($this->timezone));
        }

        if (null !== $this->end) {
            $this->end->setTimeZone(new DateTimeZone($this->timezone));
        }

        $this->localized = true;
    }

    public function getBegin(): ?DateTime
    {
        $this->localizeDates();

        return $this->begin;
    }

    /**
     * @param DateTime $begin
     * @return Timesheet
     */
    public function setBegin(DateTime $begin): Timesheet
    {
        $this->begin = $begin;
        $this->timezone = $begin->getTimezone()->getName();
        // make sure that the original date is always kept in UTC
        $this->date = new DateTime($begin->format('Y-m-d 00:00:00'), new DateTimeZone('UTC'));

        return $this;
    }

    public function getEnd(): ?DateTime
    {
        $this->localizeDates();

        return $this->end;
    }

    public function isRunning(): bool
    {
        return $this->end === null;
    }

    /**
     * @param DateTime $end
     * @return Timesheet
     */
    public function setEnd(?DateTime $end): Timesheet
    {
        $this->end = $end;

        if (null === $end) {
            $this->duration = 0;
            $this->rate = 0.00;
        } else {
            $this->timezone = $end->getTimezone()->getName();
        }

        return $this;
    }

    /**
     * @param int|null $duration
     * @return Timesheet
     */
    public function setDuration(?int $duration): Timesheet
    {
        $this->duration = $duration;

        return $this;
    }

    /**
     * Do not rely on the results of this method for running records.
     *
     * @param bool $calculate
     * @return int|null
     */
    public function getDuration(bool $calculate = true): ?int
    {
        // only auto calculate if manually set duration is null - the result is important for eg. validations
        if ($calculate && $this->duration === null && $this->begin !== null && $this->end !== null) {
            return $this->end->getTimestamp() - $this->begin->getTimestamp();
        }

        return $this->duration;
    }

    /**
     * @param User $user
     * @return Timesheet
     */
    public function setUser(User $user): Timesheet
    {
        $this->user = $user;

        return $this;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    /**
     * @param Activity $activity
     * @return Timesheet
     */
    public function setActivity($activity): Timesheet
    {
        $this->activity = $activity;

        return $this;
    }

    public function getActivity(): ?Activity
    {
        return $this->activity;
    }

    public function getProject(): ?Project
    {
        return $this->project;
    }

    /**
     * @param Project $project
     * @return Timesheet
     */
    public function setProject(Project $project): Timesheet
    {
        $this->project = $project;

        return $this;
    }

    /**
     * @param string $description
     * @return Timesheet
     */
    public function setDescription($description): Timesheet
    {
        $this->description = $description;

        return $this;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    /**
     * @param float $rate
     * @return Timesheet
     */
    public function setRate($rate): Timesheet
    {
        $this->rate = $rate;

        return $this;
    }

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

    public function setInternalRate(?float $rate): Timesheet
    {
        $this->internalRate = $rate;

        return $this;
    }

    public function getInternalRate(): ?float
    {
        return $this->internalRate;
    }

    /**
     * @param Tag $tag
     * @return Timesheet
     */
    public function addTag(Tag $tag): Timesheet
    {
        if ($this->tags->contains($tag)) {
            return $this;
        }
        $this->tags->add($tag);
        $tag->addTimesheet($this);

        return $this;
    }

    /**
     * @param Tag $tag
     */
    public function removeTag(Tag $tag)
    {
        if (!$this->tags->contains($tag)) {
            return;
        }
        $this->tags->removeElement($tag);
        $tag->removeTimesheet($this);
    }

    /**
     * @return Collection<Tag>
     */
    public function getTags(): Collection
    {
        return $this->tags;
    }

    /**
     * @return string[]
     */
    public function getTagsAsArray(): array
    {
        return array_map(
            function (Tag $element) {
                return $element->getName();
            },
            $this->getTags()->toArray()
        );
    }

    /**
     * @return bool
     */
    public function isExported(): bool
    {
        return $this->exported;
    }

    /**
     * @param bool $exported
     * @return Timesheet
     */
    public function setExported(bool $exported): Timesheet
    {
        $this->exported = $exported;

        return $this;
    }

    /**
     * @return string
     */
    public function getTimezone(): ?string
    {
        return $this->timezone;
    }

    /**
     * BE WARNED: this method should NOT be used from outside.
     * It is reserved for some very rare use-cases.
     *
     * @internal
     * @param string $timezone
     * @return Timesheet
     */
    public function setTimezone(string $timezone): Timesheet
    {
        $this->timezone = $timezone;

        return $this;
    }

    /**
     * This method returns ALWAYS: "timesheet"
     *
     * @return string
     */
    public function getType(): string
    {
        return 'timesheet';
    }

    public function getCategory(): string
    {
        return $this->category;
    }

    public function setCategory(string $category): Timesheet
    {
        $allowed = [self::WORK, self::HOLIDAY, self::SICKNESS, self::PARENTAL, self::OVERTIME];

        if (!\in_array($category, $allowed)) {
            throw new \InvalidArgumentException(sprintf('Invalid timesheet category "%s" given, expected one of: %s', $category, implode(', ', $allowed)));
        }

        $this->category = $category;

        return $this;
    }

    public function resetRates(): void
    {
        $this->rate = 0.00;
        $this->internalRate = null;
        $this->hourlyRate = null;
        $this->fixedRate = null;
    }

    public function isBillable(): bool
    {
        return $this->billable;
    }

    public function getBillable(): bool
    {
        return $this->billable;
    }

    public function setBillable(bool $billable): Timesheet
    {
        $this->billable = $billable;

        return $this;
    }

    public function getBillableMode(): string
    {
        return $this->billableMode;
    }

    public function setBillableMode(string $billableMode): void
    {
        $this->billableMode = $billableMode;
    }

    public function getFixedRate(): ?float
    {
        return $this->fixedRate;
    }

    public function setFixedRate(?float $fixedRate): Timesheet
    {
        $this->fixedRate = $fixedRate;

        return $this;
    }

    public function getHourlyRate(): ?float
    {
        return $this->hourlyRate;
    }

    public function setHourlyRate(?float $hourlyRate): Timesheet
    {
        $this->hourlyRate = $hourlyRate;

        return $this;
    }

    public function getModifiedAt(): ?DateTime
    {
        return $this->modifiedAt;
    }

    /**
     * @return Collection|MetaTableTypeInterface[]
     */
    public function getMetaFields(): Collection
    {
        return $this->meta;
    }

    /**
     * @return MetaTableTypeInterface[]
     */
    public function getVisibleMetaFields(): array
    {
        $all = [];
        foreach ($this->meta as $meta) {
            if ($meta->isVisible()) {
                $all[] = $meta;
            }
        }

        return $all;
    }

    public function getMetaField(string $name): ?MetaTableTypeInterface
    {
        foreach ($this->meta as $field) {
            if (strtolower($field->getName()) === strtolower($name)) {
                return $field;
            }
        }

        return null;
    }

    /**
     * @param string $name
     * @return bool|int|string|null
     */
    public function getMetaFieldValue(string $name)
    {
        $field = $this->getMetaField($name);
        if ($field === null) {
            return null;
        }

        return $field->getValue();
    }

    public function setMetaField(MetaTableTypeInterface $meta): EntityWithMetaFields
    {
        if (null === ($current = $this->getMetaField($meta->getName()))) {
            $meta->setEntity($this);
            $this->meta->add($meta);

            return $this;
        }

        $current->merge($meta);

        return $this;
    }

    public function createCopy(?Timesheet $timesheet = null): Timesheet
    {
        if (null === $timesheet) {
            $timesheet = new Timesheet();
        }

        $values = get_object_vars($this);
        foreach ($values as $k => $v) {
            $timesheet->$k = $v;
        }

        $timesheet->meta = new ArrayCollection();

        /** @var TimesheetMeta $meta */
        foreach ($this->meta as $meta) {
            $timesheet->setMetaField(clone $meta);
        }

        $timesheet->tags = new ArrayCollection();

        /** @var Tag $tag */
        foreach ($this->tags as $tag) {
            $timesheet->addTag($tag);
        }

        return $timesheet;
    }

    public function __clone()
    {
        if ($this->id) {
            $this->id = null;
        }

        $this->exported = false;

        $currentMeta = $this->meta;
        $this->meta = new ArrayCollection();
        /** @var TimesheetMeta $meta */
        foreach ($currentMeta as $meta) {
            $newMeta = clone $meta;
            $newMeta->setEntity($this);
            $this->setMetaField($newMeta);
        }

        $currentTags = $this->tags;
        $this->tags = new ArrayCollection();
        /** @var Tag $tag */
        foreach ($currentTags as $tag) {
            $this->addTag($tag);
        }
    }
}
