<?php
/**
 * @author Ulrich Hecht <ulrich.hecht@hecht-software.de>
 * @date 11/03/2016
 */

namespace AppBundle\Entity;
use AppBundle\Util\DepartureUtil;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * TravelDate is a wrapper for TravelPeriod + start date and end date. This entity doesn't represent a database
 * table. It was introduced because of requirement to keep the bad original database design in which a TravelPeriod
 * serves two purposes:
 * - Representing a season
 * - Representing a single travel date (by being linked to a TravelPeriodDate-instance)
 * Also TravelPeriodDate has two purposes:
 * - Representing a time period of a season. From this time period, multiple travel dates are derived by taking
 *   the possible weekdays into account
 * - Holding the start and end date for a TravelPeriod that is not a season (see above)
 *
 * To avoid further confusion and to slowly move away from this design, "TravelDate" has been invented, which has single
 * clearly defined purpose:
 * Representing a single travel date, i.e.
 * - time period a traveller starts and ends his/her journey
 * - prices
 * - availability
 * - departure locations and their prices
 *
 * "TravelPeriod" would be a more fitting name, but this name is already in use.
 *
 * @package AppBundle\Entity
 */
final class TravelDate
{
    /** @var String $key */
    private $key;

    /** @var TravelPeriod $travelPeriod */
    private $travelPeriod;

    /** @var \DateTime $start */
    private $start;

    /** @var \DateTime $end */
    private $end;

    private $index;

    /** @var FlightPeriod $flightPeriod */
    private $flightPeriod;

    private $currencyFactor;

    private $travelProgram;

    /** @var TravelDeparturePoint[]|null $departures Departures cache */
    private $departures = null;

    /** @var TravelPeriodPrice[]|null $prices Prices cache */
    private $prices = null;

    private $calculatedEffectivePrices = false;

    /**
     * TravelDate constructor.
     *
     * @param String $key
     * @param TravelPeriod $travelPeriod
     * @param FlightPeriod|null $flightPeriod
     * @param float $currencyFactor
     * @param \DateTime $start
     * @param \DateTime $end Optional. Computed from travel duration, if not specified.
     * @param null $index
     *
     * @todo Make this object immutable
     */
    public function __construct($key, TravelPeriod $travelPeriod, FlightPeriod $flightPeriod = null, $currencyFactor,
      \DateTime $start = null, \DateTime $end = null, $index = null)
    {
        if ($travelPeriod->getIsSeason())
        {
            if ($index === null)
            {
                throw new \InvalidArgumentException('Expected numeric value for argument $index due to virtual travel date. Got null.');
            }
            if ($start === null)
            {
                throw new \InvalidArgumentException('Expected DateTime value for argument $start due to virtual travel date. Got null.');
            }
            $this->start = $start;
            $this->index = $index;
            $this->prices = [];
            foreach ($travelPeriod->getPrices() as $price)
            {
                $this->prices[$price->getPriceTypeId()] = clone $price;
            }
        }
        else
        {
            $this->start = $travelPeriod->getStartDate();
            $this->prices = $travelPeriod->getPrices();
        }
        $this->flightPeriod = $flightPeriod;
        $this->travelProgram = $travelPeriod->getProgram();
        $this->key = $key;
        $this->travelPeriod = $travelPeriod;
        if ($end === null)
        {
            $this->end = clone $this->start;
            $this->end->modify('+'. $this->travelProgram->getProgramDuration() .' day');
        }
        else
        {
            $this->end = $end;
        }
        $this->currencyFactor = $currencyFactor;
    }

    public static function createForNonSeasonTravelPeriod($key, TravelPeriod $travelPeriod,
      FlightPeriod $flightPeriod = null, $currencyFactor)
    {
        if ($travelPeriod->getIsSeason())
        {
            throw new \Exception('Expected non-season TravelPeriod instance');
        }
        return new TravelDate($key, $travelPeriod, $flightPeriod, $currencyFactor);
    }

    public static function createForSeasonTravelPeriod($key, TravelPeriod $travelPeriod, $index, \DateTime $start,
      \DateTime $end = null, FlightPeriod $flightPeriod = null, $currencyFactor)
    {
        if (!$travelPeriod->getIsSeason())
        {
            throw new \Exception('Expected season TravelPeriod instance');
        }
        return new TravelDate($key, $travelPeriod, $flightPeriod, $currencyFactor, $start, $end, $index);
    }

    /**
     * @return String
     */
    public function getKey()
    {
        return $this->key;
    }

    /**
     * @return \DateTime
     */
    public function getStart()
    {
        return $this->start;
    }

    /**
     * @return \DateTime
     */
    public function getEnd()
    {
        return $this->end;
    }

    public function getName()
    {
        return $this->travelPeriod->getIsSeason()
          ? (trim($this->travelProgram->getProgramCode()) . $this->index)
          : $this->travelPeriod->getName()
          ;
    }

    public function getStatus()
    {
        return $this->travelPeriod->getStatus();
    }

    public function getEffectiveStatus()
    {
        if ($this->getStatus() == 2 && $this->getStart()->getTimestamp() < time() + 2419200)
        {
            return 1;
        }
        return $this->getStatus();
    }

    public function getDepartures()
    {
        if ($this->departures === null)
        {
            if ($this->travelProgram->getIsMediated())
            {
                $defaultDepartures = $this->travelProgram->getDepartures();
                $departures = $this->travelPeriod->getDepartures();
            }
            else
            {
                $defaultDepartures = $this->travelProgram->getTravelArrivalPoint()->getDepartures();
                $departures = $this->flightPeriod === null ? [] : $this->flightPeriod->getDepartures();
            }
            $defaultDepartures = DepartureUtil::filterDeparturesByPeriod($defaultDepartures, $this->start, $this->end, true);
            $this->departures = DepartureUtil::mergeDeparturesWithDefaults($departures, $defaultDepartures, true);
        }
        return $this->departures;
    }

    public function getFlightPrice()
    {
        if ($this->travelProgram->getIsMediated())
        {
            return 0;
        }
        $flightPrice = null;
        if ($this->flightPeriod !== null)
        {
            $flightPrice = $this->flightPeriod->getPrice();
        }
        if ($flightPrice === null)
        {
            $flightPrice = $this->travelProgram->getDefaultFlightPrice();
        }
        return $flightPrice;
    }

    /**
     * @return TravelPeriodPrice[]|\Doctrine\Common\Collections\Collection
     */
    public function getPrices()
    {
        if (!$this->calculatedEffectivePrices)
        {
            $this->calculatedEffectivePrices = true;
            $flightPrice = $this->getFlightPrice();
            if ($this->travelProgram->getIsMediated())
            {
                $profitMargin = 1;
            }
            else
            {
                $profitMargin = $this->travelProgram->getProfitMargin() / 100 + 1;
            }
            $currencyFactor = $this->travelProgram->getNettoPricesInEuro() ? 1 : $this->currencyFactor;
            foreach ($this->prices as &$price)
            {
                $price->setEffectivePrice(round(($flightPrice + $price->getPrice() * $currencyFactor) * $profitMargin));
                $price->setEffectiveComfortPrice(round($price->getPriceComfort() * $currencyFactor * $profitMargin));
                $price->setEffectiveChildPrice(round($price->getPriceChildren() * $currencyFactor * $profitMargin));
            }
        }
        return $this->prices;
    }

    public function getLowestPrice()
    {
        $lowest = -1;
        foreach ($this->getPrices() as $price)
        {
            if ($price->getPriceTypeId() == 3)
            {
                // Use double room if available (#1076)
                return $price->getEffectiveDiscountPrice() ?? $price->getEffectivePrice();
            }
            if ($lowest < 0 || $price->getEffectivePrice() < 0)
            {
                $lowest = $price->getEffectivePrice();
            }
        }
        return $lowest == -1 ? null : $lowest;
    }

    public function hasComfortCategory()
    {
        foreach ($this->getPrices() as $price)
        {
            if ($price->getPriceComfort() > 0)
            {
                return true;
            }
        }
        return false;
    }

    /**
     * @return TravelProgram
     */
    public function getTravelProgram(): TravelProgram
    {
        return $this->travelProgram;
    }

    /**
     * @return TravelPeriod
     * @internal
     */
    public function __getTravelPeriod()
    {
        return $this->travelPeriod;
    }
}