<?php

namespace AppBundle\Service;

use AppBundle\Entity\FewoLodging;
use AppBundle\Entity\FewoPrice;
use AppBundle\Entity\FewoReservation;
use AppBundle\Entity\FewoSeason;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManager;
use AppBundle\Util\CalendarDayState;

class LodgingCalendarService
{
    private $em;

    public function __construct(EntityManager $entityManager)
    {
        $this->em = $entityManager;
    }



    private function daysInMonth($month, $year)
    {
        return $month == 2 ? ($year % 4 ? 28 : ($year % 100 ? 29 : ($year % 400 ? 28 : 29))) : (($month - 1) % 7 % 2 ? 30 : 31);
    }

    public function getCalendarWithPadding($months)
    {
        $calendar = null;
        $currentMonth = date('n');
        $currentYear = date('Y');
        $weekDays = ['So','Mo','Di','Mi','Do','Fr','Sa'];

        setlocale(LC_TIME, "german");

        $yearTurned = false;
        $yearsTurned = 0;

        for($m = $currentMonth; $m < ($currentMonth + $months); $m++)
        {
            $actualMonth = $m % 12;

            if($actualMonth == 0)
            {
                $actualMonth = 12;
                $yearTurned = true;
            }

            $actualYear = $currentYear + $yearsTurned;

            if($yearTurned)
            {
                $yearsTurned = $yearsTurned + 1;
                $yearTurned = false;
            }

            $date = getdate(mktime(0,0,0, $actualMonth, 1, $actualYear));
            $weekDay = $weekDays[$date['wday']];

            $numberDays = $this->daysInMonth($actualMonth, $actualYear);
            $data = null;

            $actualStartingWeekDay = $date['wday'];

            if($actualStartingWeekDay == 0)
            {
                $actualStartingWeekDay = 7;
            }

            for($fwd = 0; $fwd < $actualStartingWeekDay - 1; $fwd++)
            {
                $day = new CalendarDayState();
                $day->setDay(0);
                $day->setIsBookable(false);
                $day->setIsInSeason(false);
                $day->setIsReserved(false);
                $day->setIsReservationBegin(false);
                $day->setIsReservationEnd(false);

                $data[] = $day;
            }

            for($d = 1; $d <= $numberDays; $d++)
            {
                $day = new CalendarDayState();
                $day->setDay($d);
                $day->setIsBookable(false);
                $day->setIsInSeason(false);
                $day->setIsReserved(false);
                $day->setIsReservationBegin(false);
                $day->setIsReservationEnd(false);

                $data[] = $day;
            }

            $startIndex = count($data);

            for($lwd = $startIndex; $lwd < 42; $lwd++)
            {
                $day = new CalendarDayState();
                $day->setDay(0);
                $day->setIsBookable(false);
                $day->setIsInSeason(false);
                $day->setIsReserved(false);
                $day->setIsReservationBegin(false);
                $day->setIsReservationEnd(false);

                $data[] = $day;
            }

            $calendar[] = [
                'numberDays' => $numberDays,
                'monthNumber' => $actualMonth,
                'monthName' => utf8_encode(strftime("%B", mktime(0, 0, 0, $actualMonth, 1, $actualYear))),
                'year' => $actualYear,
                'startWeekDay' => $weekDay,
                'data' => $data,
                'marked' => 0
            ];
        }

        return $calendar;
    }


    public function getCalendar($lodging)
    {
        $months = $this->getMinCalendarEntriesByLodging($lodging);
        $calendar = null;
        $currentMonth = date('n');
        $currentYear = date('Y');
        $weekDays = ['So','Mo','Di','Mi','Do','Fr','Sa'];

        setlocale(LC_TIME, "german");

        $yearTurned = false;
        $yearsTurned = 0;

        for($m = $currentMonth; $m < ($currentMonth + $months); $m++)
        {
            $actualMonth = $m % 12;

            if($actualMonth == 0)
            {
                $actualMonth = 12;
                $yearTurned = true;
            }

            $actualYear = $currentYear + $yearsTurned;

            if($yearTurned)
            {
                $yearsTurned = $yearsTurned + 1;
                $yearTurned = false;
            }

            $date = getdate(mktime(0,0,0, $actualMonth, 1, $actualYear));
            $weekDay = $weekDays[$date['wday']];

            $numberDays = $this->daysInMonth($actualMonth, $actualYear);

            /** @var CalendarDayState[] $data */
            $data = null;

            // alle Wochentage
            for($d = 1; $d <= $numberDays; $d++)
            {
                // standard-vorbelegung
                $day = new CalendarDayState();
                $day->setDay($d);
                $day->setIsBookable(false);
                $day->setIsInSeason(false);
                $day->setIsReserved(false);
                $day->setIsReservationBegin(false);
                $day->setIsReservationEnd(false);
                $datetime = new \DateTime();
                $datetime->setTimestamp(mktime(0,0,0, $actualMonth, $d, $actualYear));
                $day->setDate($datetime);

                $data[] = $day;
            }

            $calendar[] = [
                'numberDays' => $numberDays,
                'monthNumber' => $actualMonth,
                'monthName' => utf8_encode(strftime("%B", mktime(0, 0, 0, $actualMonth, 1, $actualYear))),
                'year' => $actualYear,
                'startWeekDay' => $weekDay,
                'data' => $data,
                'marked' => 0
            ];
        }

        return $calendar;
    }

    /**
     * Retrieve needed number of months to generate a calendar which includes all available seasons
     *
     * @param FewoLodging $lodging
     * @return int
     */
    public function getMinCalendarEntriesByLodging($lodging)
    {
        $latestSeason = $this->em->getRepository('AppBundle:FewoLodging')->findLatestSeasonEndForLodging($lodging);
        $curMonthDate = new \DateTime();
        $curMonthDate->setDate($curMonthDate->format('Y'), $curMonthDate->format('n'), 1);
        $seasonMonthDate = new \DateTime($latestSeason->getToDate()->format('Y-m-d'));
        $seasonMonthDate->setDate($seasonMonthDate->format('Y'), $seasonMonthDate->format('n'), 1);
        $dateInterval = $seasonMonthDate->diff($curMonthDate);
        $month = intval($dateInterval->format('%m')) + (intval($dateInterval->format('%y')) * 12);
        return 2 + $month;
    }


    /**
     * @param FewoLodging $lodging
     * @return array|null
     */
    public function getReservations(FewoLodging $lodging)
    {
        $reservations = $lodging->getReservations();
        $ret = null;

        for($i = 0; $i < count($reservations); $i++)
        {
            $fromDate = $reservations[$i]->getFromDate();
            $toDate = $reservations[$i]->getToDate();

            $ret[] = [
                'startDay' => $fromDate->format('j'),
                'startMonth' => $fromDate->format('n'),
                'startYear' => $fromDate->format('Y'),
                'endDay' => $toDate->format('j'),
                'endMonth' => $toDate->format('n'),
                'endYear' => $toDate->format('Y')
            ];
        }

        return $ret;
    }

    private function getMonthIndex($calendar, $month, $year)
    {
        $result = 0;
        for($i = 0; $i < count($calendar); $i++)
        {
            if($calendar[$i]['monthNumber'] == $month && $calendar[$i]['year'] == $year)
            {
                $result = $i;
                break;
            }
        }
        return $result;
    }


    /**
     * @param $calendar
     * @param \DateTime $fromDate
     * @param \DateTime $toDate
     * @param $appendix
     * @return mixed
     */
    private function markCalendarDays($calendar, \DateTime $fromDate, \DateTime $toDate,
                                      $reservationMode = false,
                                      FewoPrice $price = null, FewoReservation $reservation = null)
    {
        $startDay = $fromDate->format('j');
        $startMonth = $fromDate->format('n');
        $startYear = $fromDate->format('Y');
        $endDay = $toDate->format('j');
        $endMonth = $toDate->format('n');
        $endYear = $toDate->format('Y');
        $today = new \DateTime();
        $todayDay = $today->format('j');
        $todayMonth = $today->format('n');
        $todayYear = $today->format('Y');


        if ($endMonth >= $todayMonth || ($todayMonth == 12 && $endMonth >= 1 && ($todayYear < $endYear))) //($toDate > $today)
        {
            if($fromDate < $today)
            {
                $startDay = 1;
                $startMonth = $today->format('n');
                $startYear = $today->format('Y');
            }

            // wenn sich der Kalender über mehrere Jahre zieht, könnte es hier zu Problemen kommen
            $startCalendarIndex = $this->getMonthIndex($calendar, $startMonth, $startYear);
            $endCalendarIndex = $this->getMonthIndex($calendar, $endMonth, $endYear);

            // anfang und ende innerhalb eines monats
            if($startCalendarIndex == $endCalendarIndex)
            {
                /** @var CalendarDayState[] $data */
                $data = $calendar[$startCalendarIndex]['data'];

                $calendar[$startCalendarIndex]['marked'] = 1;

                // hier geht man von ungepaddeten Kalendermonaten aus
                $startIndex = $startDay - 1;
                $endIndex = $endDay - 1;

                for ($i = $startIndex; $i <= $endIndex; $i++)
                {
                    if ($reservationMode)
                    {
                        if ($i == $startIndex)
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setIsReservationBegin(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                        }
                        elseif ($i == $endIndex)
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setIsReservationEnd(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                            //$data[$i]->setIsBookable(true);
                        }
                        else
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                        }
                    }
                    else
                    {
                        $data[$i]->setIsReserved(false);
                        $data[$i]->setPrice($price);
                        $data[$i]->setIsInSeason(true);
                        $data[$i]->setIsBookable(false);
                    }
                }
                $calendar[$startCalendarIndex]['data'] = $data;
            }
            else
            {
                // erster monat

                /** @var CalendarDayState[] $data */
                $data = $calendar[$startCalendarIndex]['data'];

                $calendar[$startCalendarIndex]['marked'] = 1;

                $startIndex = $startDay - 1;

                $endIndex = count($data) - 1;
                for($i = $startIndex; $i <= $endIndex; $i++)
                {
                    if ($reservationMode)
                    {
                        if ($i == $startIndex)
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setIsReservationBegin(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                        }
                        else
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                        }
                    }
                    else
                    {
                        $data[$i]->setIsReserved(false);
                        $data[$i]->setPrice($price);
                        $data[$i]->setIsInSeason(true);
                        $data[$i]->setIsBookable(false);
                    }
                }
                $calendar[$startCalendarIndex]['data'] = $data;

                // letzter monat

                /** @var CalendarDayState[] $data */
                $data = $calendar[$endCalendarIndex]['data'];

                $calendar[$endCalendarIndex]['marked'] = 1;

                $startIndex = 0;
                $endIndex = $endDay - 1;
                for($i = $startIndex; $i <= $endIndex; $i++)
                {

                    if ($reservationMode)
                    {
                        if ($i == $endIndex)
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setIsReservationEnd(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                            //$data[$i]->setIsBookable(true);
                        }
                        else
                        {
                            $data[$i]->setIsReserved(true);
                            $data[$i]->setReservation($reservation);
                            $data[$i]->setIsInSeason(true);
                            $data[$i]->setIsBookable(false);
                        }
                    }
                    else
                    {
                        $data[$i]->setIsReserved(false);
                        $data[$i]->setPrice($price);
                        $data[$i]->setIsInSeason(true);
                        $data[$i]->setIsBookable(false);
                    }
                }
                $calendar[$endCalendarIndex]['data'] = $data;


                // alle monate dazwischen
                for($i = $startCalendarIndex + 1; $i < $endCalendarIndex; $i++)
                {
                    /** @var CalendarDayState[] $data */
                    $data = $calendar[$i]['data'];

                    $calendar[$i]['marked'] = 1;

                    $startIndex = 0;
                    $endIndex = count($data) - 1;
                    for($j = $startIndex; $j <= $endIndex; $j++)
                    {
                        if ($reservationMode)
                        {
                            $data[$j]->setIsReserved(true);
                            $data[$j]->setReservation($reservation);
                            $data[$j]->setIsInSeason(true);
                            $data[$j]->setIsBookable(false);
                        }
                        else
                        {
                            $data[$j]->setIsReserved(false);
                            $data[$j]->setPrice($price);
                            $data[$j]->setIsInSeason(true);
                            $data[$j]->setIsBookable(false);
                        }
                    }
                    $calendar[$i]['data'] = $data;
                }
            }
        }

        return $calendar;
    }

    // in der Regel: calendar = pricesCalendar; extensionCalendar = reservationsCalendar
    private function mergeCalendars($calendar, $extensionCalendar)
    {
        for($i = 0; $i < count($calendar); $i++)
        {
            /** @var CalendarDayState[] $calendarData */
            $calendarData = $calendar[$i]['data'];

            /** @var CalendarDayState[] $extensionCalendarData */
            $extensionCalendarData = $extensionCalendar[$i]['data'];

            $endIndex = count($calendarData);

            for($j = 0; $j < $endIndex; $j++)
            {
                if ($extensionCalendarData[$j]->getIsReserved())
                {
                    $price = $calendarData[$j]->getPrice();
                    //$isInSeason = $calendarData[$j]->getIsInSeason();
                    $reservation = $calendarData[$j]->getReservation();
                    $calendarData[$j] = $extensionCalendarData[$j];

                    if ($calendarData[$j]->getIsReservationEnd() && $extensionCalendarData[$j]->getIsReservationBegin())
                    {
                        $calendarData[$j]->setReservation($reservation);
                        $calendarData[$j]->setIsReservationBegin(false);
                        $calendarData[$j]->setIsReservationEnd(false);
                    }

                    /** @var FewoPrice $price */
                    $calendarData[$j]->setIsInSeason(true);
                    $calendarData[$j]->setPrice($price);
                }
            }

            $calendar[$i]['data'] = $calendarData;
        }
        return $calendar;
    }


    /** @var CalendarDayState[] $calendarMonth */
    private function findCalendarDayIndexInMonth($dayNumber, $calendarMonth)
    {
        $result = 0;
        for ($i = 0; $i < count($calendarMonth); $i++)
        {
            if ($calendarMonth[$i]->getDay() == $dayNumber)
            {
                $result = $i;
                break;
            }
        }
        return $result;
    }

    public function mergeWithPaddedCalendar($calendar, $paddedCalendar)
    {
        for($i = 0; $i < count($paddedCalendar); $i++)
        {
            $calendarDataCurrMonth = $calendar[$i]['data'];
            $paddedCalendarDataCurrMonth = $paddedCalendar[$i]['data'];

            $startIndex = $this->findCalendarDayIndexInMonth(1, $paddedCalendarDataCurrMonth);
            //$startIndex = array_search(1, $paddedCalendarDataCurrMonth);

            for($j = 0; $j < count($calendarDataCurrMonth); $j++, $startIndex++)
            {
                $paddedCalendarDataCurrMonth[$startIndex] = $calendarDataCurrMonth[$j];
            }
            $paddedCalendar[$i]['data'] = $paddedCalendarDataCurrMonth;
        }
        return $paddedCalendar;
    }


    private function isNextDay($current, $next)
    {
        $result = false;

        $calculatedNextDate = date('d.m.Y', strtotime($current." + 1 day"));


        if($calculatedNextDate == $next)
        {
            $result = true;
        }

        return $result;
    }

    private function getMonthFromCalendar($calendar, $monthNumber)
    {
        $result = [];

        for($i = 0; $i < count($calendar); $i++)
        {
            if($calendar[$i]['monthNumber'] == $monthNumber)
            {
                $result = $calendar[$i];
            }
        }

        return $result;
    }

    /**
     * @param FewoLodging $lodging
     * @param $calendar
     * @return array|null calendars
     */
    private function filterReservableDays(FewoLodging $lodging, $calendar)
    {
        /** @var CalendarDayState[] $potentiallyReservableDays */
        $potentiallyReservableDays = [];

        for ($currMonthIndex = 0; $currMonthIndex < count($calendar); $currMonthIndex++)
        {
            $currMonth = $calendar[$currMonthIndex];
            /** @var CalendarDayState[] $data */
            $data = $currMonth['data'];

            for ($currDayIndex = 0; $currDayIndex < count($data); $currDayIndex++)
            {
                $currDay = $data[$currDayIndex];

                // We passed the season end? => All days in the stack are not reservable, as their minimum stay
                // requirement would mean their reservation end is outside a season
                if (!$currDay->getIsInSeason())
                {
                    while (!empty($potentiallyReservableDays))
                    {
                        $potentiallyReservableDay = array_pop($potentiallyReservableDays);
                        $potentiallyReservableDay->setIsBookable(false);
                    }
                    $currDay->setIsBookable(false);
                    continue;
                }

                $currSeason = $currDay->getPrice()->getSeason();

                // Process the stack of reservable days and set reservable=true, if their minimum stay requirement could
                // be fulfilled. Set reservable=false if there is no chance for the stack entry to be reservable.
                // Remove potentially reservable days from the stack for which a final decision has been made.
                if (!$currDay->getIsReserved() || $currDay->getIsReservationBegin())
                {
                    foreach ($potentiallyReservableDays as $k => $potentiallyReservableDay)
                    {
                        $nights = intval($potentiallyReservableDay->getDate()->diff($currDay->getDate())->format('%a'));
                        if ($nights >= $currSeason->getMinimumStay())
                        {
                            $potentiallyReservableDay->setIsBookable(true);
                            unset($potentiallyReservableDays[$k]);
                        }
                        elseif ($currDay->getIsReservationBegin())
                        {
                            // Reservation begin => We are absolutely sure that this day is not reservable
                            $potentiallyReservableDay->setIsBookable(false);
                            unset($potentiallyReservableDays[$k]);
                        }
                    }
                }

                // Add the current calendar day to stack, as we need to analyse the following days in order to be able
                // to decide if the day is reservable or not.
                if ((!$currDay->getIsReserved() || $currDay->getIsReservationEnd()) &&
                    ($currSeason->getOnlyWeekday() == null || $currSeason->getOnlyWeekday() == $currDay->getDate()->format('w')))
                {
                    $potentiallyReservableDays[] = $currDay;
                }
                else
                {
                    $currDay->setIsBookable(false);
                }
            }
        }

        // Remaining entries in the stack are definitely not reservable. Otherwise they would have been processed before.
        foreach ($potentiallyReservableDays as $potentiallyReservableDay)
        {
            $potentiallyReservableDay->setIsBookable(false);
        }

        return $calendar;
    }

    /**
     * @param FewoLodging $lodging
     * @param $calendar
     * @return mixed
     *
     * createCalendarAndFillDayStates
     */
    public function createCalendarAndFillDayStates(FewoLodging $lodging)
    {
        $pricesCalendar = $this->getCalendar($lodging);
        $reservationsCalendar = $this->getCalendar($lodging);

        $reservations = $lodging->getReservations();
        $prices = $lodging->getPrices();

        foreach ($prices as $price)
        {
            /** @var FewoSeason $season */
            $season = $price->getSeason();
            $pricesCalendar = $this->markCalendarDays($pricesCalendar, $season->getFromDate(), $season->getToDate(),
                false, $price);
        }

        foreach($reservations as $reservation)
        {
            $fromDate = $reservation->getFromDate();
            $toDate = $reservation->getToDate();

            $reservationsCalendar = $this->markCalendarDays($reservationsCalendar, $fromDate, $toDate,
                true, null, $reservation); //TODO fast fertig
        }

        $mergedPricesAndReservationsCalendar = $this->mergeCalendars($pricesCalendar, $reservationsCalendar);
        $actuallyReservableDaysCalendar = $this->filterReservableDays($lodging, $mergedPricesAndReservationsCalendar);
        //$resultCalendar = $this->mergeCalendars($actuallyReservableDaysCalendar, $reservationsCalendar, true);

        return $actuallyReservableDaysCalendar;
        //return $resultCalendar;
    }


    public function convertDate($date)
    {
        $result = substr($date, 0, 2).'.'.substr($date, 2, 2).'.'.substr($date, 4, 4);
        return $result;
    }

}