<?php

namespace Recurr\Transformer;

use Recurr\Rule;

class TextTransformer
{
    protected $fragments = array();
    protected $translator;

    public function __construct(?TranslatorInterface $translator = null)
    {
        $this->translator = $translator ?: new Translator('en');
    }

    public function transform(Rule $rule)
    {
        $this->fragments = array();

        switch ($rule->getFreq()) {
            case 0:
                $this->addYearly($rule);
                break;
            case 1:
                $this->addMonthly($rule);
                break;
            case 2:
                $this->addWeekly($rule);
                break;
            case 3:
                $this->addDaily($rule);
                break;
            case 4:
                $this->addHourly($rule);
                break;
            case 5:
            case 6:
                return $this->translator->trans('Unable to fully convert this rrule to text.');
        }

        $until = $rule->getUntil();
        $count = $rule->getCount();
        if ($until instanceof \DateTimeInterface) {
            $dateFormatted = $this->translator->trans('day_date', array('date' => $until->format('U')));
            $this->addFragment($this->translator->trans('until %date%', array('date' => $dateFormatted)));
        } else if (!empty($count)) {
            if ($this->isPlural($count)) {
                $this->addFragment($this->translator->trans('for %count% times', array('count' => $count)));
            } else {
                $this->addFragment($this->translator->trans('for one time'));
            }
        }

        if (!$this->isFullyConvertible($rule)) {
            $this->addFragment($this->translator->trans('(~ approximate)'));
        }

        return implode(' ', $this->fragments);
    }

    protected function isFullyConvertible(Rule $rule)
    {
        if ($rule->getFreq() >= 5) {
            return false;
        }

        $until = $rule->getUntil();
        $count = $rule->getCount();
        if (!empty($until) && !empty($count)) {
            return false;
        }

        $bySecond = $rule->getBySecond();
        $byMinute = $rule->getByMinute();
        $byHour   = $rule->getByHour();

        if (!empty($bySecond) || !empty($byMinute) || !empty($byHour)) {
            return false;
        }

        $byWeekNum = $rule->getByWeekNumber();
        $byYearDay = $rule->getByYearDay();
        if ($rule->getFreq() != 0 && (!empty($byWeekNum) || !empty($byYearDay))) {
            return false;
        }

        return true;
    }

    protected function addYearly(Rule $rule)
    {
        $interval = $rule->getInterval();
        $byMonth = $rule->getByMonth();
        $byMonthDay = $rule->getByMonthDay();
        $byDay = $rule->getByDay();
        $byYearDay = $rule->getByYearDay();
        $byWeekNum = $rule->getByWeekNumber();

        if (!empty($byMonth) && count($byMonth) > 1 && $interval == 1) {
            $this->addFragment($this->translator->trans('every_month_list'));
        } else {
            $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% years' : 'every year', array('count' => $interval)));
        }

        $hasNoOrOneByMonth = is_null($byMonth) || count($byMonth) <= 1;
        if ($hasNoOrOneByMonth && empty($byMonthDay) && empty($byDay) && empty($byYearDay) && empty($byWeekNum)) {
            $this->addFragment($this->translator->trans('on'));
            $monthNum = (is_array($byMonth) && count($byMonth)) ? $byMonth[0] : $rule->getStartDate()->format('n');
            $this->addFragment(
                $this->translator->trans('day_month', array('month' => $monthNum, 'day' => $rule->getStartDate()->format('d')))
            );
        } elseif (!empty($byMonth)) {
            if ($interval != 1) {
                $this->addFragment($this->translator->trans('in_month'));
            }

            $this->addByMonth($rule);
        }

        if (!empty($byMonthDay)) {
            $this->addByMonthDay($rule);
            $this->addFragment($this->translator->trans('of_the_month'));
        } else if (!empty($byDay)) {
            $this->addByDay($rule);
        }

        if (!empty($byYearDay)) {
            $this->addFragment($this->translator->trans('on the'));
            $this->addFragment($this->getByYearDayAsText($byYearDay));
            $this->addFragment($this->translator->trans('day'));
        }

        if (!empty($byWeekNum)) {
            $this->addFragment($this->translator->trans('in_week'));
            $this->addFragment($this->translator->trans($this->isPlural(count($byWeekNum)) ? 'weeks' : 'week'));
            $this->addFragment($this->getByWeekNumberAsText($byWeekNum));
        }

        if (empty($byMonthDay) && empty($byYearDay) && empty($byDay) && !empty($byWeekNum)) {
            $this->addDayOfWeek($rule);
        }
    }

    protected function addMonthly(Rule $rule)
    {
        $interval = $rule->getInterval();
        $byMonth = $rule->getByMonth();

        if (!empty($byMonth) && $interval == 1) {
            $this->addFragment($this->translator->trans('every_month_list'));
        } else {
            $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% months' : 'every month', array('count' => $interval)));
        }

        if (!empty($byMonth)) {
            if ($interval != 1) {
                $this->addFragment($this->translator->trans('in_month'));
            }

            $this->addByMonth($rule);
        }

        $byMonthDay = $rule->getByMonthDay();
        $byDay      = $rule->getByDay();
        if (!empty($byMonthDay)) {
            $this->addByMonthDay($rule);
        } else if (!empty($byDay)) {
            $this->addByDay($rule);
        }
    }

    protected function addWeekly(Rule $rule)
    {
        $interval = $rule->getInterval();
        $byMonth = $rule->getByMonth();
        $byMonthDay = $rule->getByMonthDay();
        $byDay = $rule->getByDay();

        $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% weeks' : 'every week', array('count' => $interval)));

        if (empty($byMonthDay) && empty($byDay)) {
            $this->addDayOfWeek($rule);
        }

        if (!empty($byMonth)) {
            $this->addFragment($this->translator->trans('in_month'));
            $this->addByMonth($rule);
        }

        if (!empty($byMonthDay)) {
            $this->addByMonthDay($rule);
            $this->addFragment($this->translator->trans('of_the_month'));
        } else if (!empty($byDay)) {
            $this->addByDay($rule);
        }
    }

    protected function addDaily(Rule $rule)
    {
        $interval = $rule->getInterval();
        $byMonth = $rule->getByMonth();

        $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% days' : 'every day', array('count' => $interval)));

        if (!empty($byMonth)) {
            $this->addFragment($this->translator->trans('in_month'));
            $this->addByMonth($rule);
        }

        $byMonthDay = $rule->getByMonthDay();
        $byDay      = $rule->getByDay();
        if (!empty($byMonthDay)) {
            $this->addByMonthDay($rule);
            $this->addFragment($this->translator->trans('of_the_month'));
        } else if (!empty($byDay)) {
            $this->addByDay($rule);
        }
    }
    
    protected function addHourly(Rule $rule)
    {
        $interval = $rule->getInterval();
        $byMonth = $rule->getByMonth();

        $this->addFragment($this->translator->trans($this->isPlural($interval) ? 'every %count% hours' : 'every hour', array('count' => $interval)));

        if (!empty($byMonth)) {
            $this->addFragment($this->translator->trans('in_month'));
            $this->addByMonth($rule);
        }

        $byMonthDay = $rule->getByMonthDay();
        $byDay      = $rule->getByDay();
        if (!empty($byMonthDay)) {
            $this->addByMonthDay($rule);
            $this->addFragment($this->translator->trans('of_the_month'));
        } else if (!empty($byDay)) {
            $this->addByDay($rule);
        }
    }

    protected function addByMonth(Rule $rule)
    {
        $byMonth = $rule->getByMonth();

        if (empty($byMonth)) {
            return;
        }

        $this->addFragment($this->getByMonthAsText($byMonth));
    }

    protected function addByMonthDay(Rule $rule)
    {
        $byMonthDay = $rule->getByMonthDay();
        $byDay      = $rule->getByDay();

        if (!empty($byDay)) {
            $this->addFragment($this->translator->trans('on'));
            $this->addFragment($this->getByDayAsText($byDay, 'or'));
            $this->addFragment($this->translator->trans('the_for_monthday'));
            $this->addFragment($this->getByMonthDayAsText($byMonthDay, 'or'));
        } else {
            $this->addFragment($this->translator->trans('on the'));
            $this->addFragment($this->getByMonthDayAsText($byMonthDay, 'and'));
        }
    }

    protected function addByDay(Rule $rule)
    {
        $byDay = $rule->getByDay();

        $this->addFragment($this->translator->trans('on'));
        $this->addFragment($this->getByDayAsText($byDay));
    }

    protected function addDayOfWeek(Rule $rule)
    {
        $this->addFragment($this->translator->trans('on'));
        $dayNames = $this->translator->trans('day_names');
        $this->addFragment($dayNames[$rule->getStartDate()->format('w')]);
    }

    public function getByMonthAsText($byMonth)
    {
        if (empty($byMonth)) {
            return '';
        }

        if (count($byMonth) > 1) {
            sort($byMonth);
        }

        $monthNames = $this->translator->trans('month_names');
        $byMonth = array_map(
            function ($monthInt) use ($monthNames) {
                return $monthNames[$monthInt - 1];
            },
            $byMonth
        );

        return $this->getListStringFromArray($byMonth);
    }

    public function getByDayAsText($byDay, $listSeparator = 'and')
    {
        if (empty($byDay)) {
            return '';
        }

        $map = array(
            'SU' => null,
            'MO' => null,
            'TU' => null,
            'WE' => null,
            'TH' => null,
            'FR' => null,
            'SA' => null
        );

        $dayNames = $this->translator->trans('day_names');
        $timestamp = mktime(1, 1, 1, 1, 12, 2014); // A Sunday
        foreach (array_keys($map) as $short) {
            $long        = $dayNames[date('w', $timestamp)];
            $map[$short] = $long;
            $timestamp += 86400;
        }

        $numOrdinals = 0;
        foreach ($byDay as $key => $short) {
            $day    = strtoupper($short);
            $string = '';

            if (preg_match('/([+-]?)([0-9]*)([A-Z]+)/', $short, $parts)) {
                $symbol = $parts[1];
                $nth    = $parts[2];
                $day    = $parts[3];

                if (!empty($nth)) {
                    ++$numOrdinals;
                    $string .= $this->getOrdinalNumber($symbol == '-' ? -$nth : $nth);
                }
            }

            if (!isset($map[$day])) {
                throw new \RuntimeException("byDay $short could not be transformed");
            }

            if (!empty($string)) {
                $string .= ' ';
            }

            $byDay[$key] = ltrim($string.$map[$day]);
        }

        $output = $numOrdinals ? $this->translator->trans('the_for_weekday') . ' ' : '';
        if ($output == ' ') {
            $output = '';
        }
        $output .= $this->getListStringFromArray($byDay, $listSeparator);

        return $output;
    }

    public function getByMonthDayAsText($byMonthDay, $listSeparator = 'and')
    {
        if (empty($byMonthDay)) {
            return '';
        }

        // sort negative indices in reverse order so we get e.g. 1st, 2nd, 4th, 3rd last, last day
        usort($byMonthDay, function ($a, $b) {
            if (($a < 0 && $b < 0) || ($a >= 0 && $b >= 0)) {
                return $a - $b;
            }

            return $b - $a;
        });

        // generate ordinal numbers and insert a "on the" for clarity in the middle if we have both
        // positive and negative ordinals. This is to avoid confusing situations like:
        //
        // monthly on the 1st and 2nd to the last day
        //
        // which gets clarified to:
        //
        // monthly on the 1st day and on the 2nd to the last day
        $hadPositives = false;
        $hadNegatives = false;
        foreach ($byMonthDay as $index => $day) {
            $prefix = '';
            if ($day >= 0) {
                $hadPositives = true;
            }
            if ($day < 0) {
                if ($hadPositives && !$hadNegatives && $listSeparator === 'and') {
                    $prefix = $this->translator->trans('on the') . ' ';
                }
                $hadNegatives = true;
            }
            $byMonthDay[$index] = $prefix . $this->getOrdinalNumber($day, end($byMonthDay) < 0, true);
        }

        return $this->getListStringFromArray($byMonthDay, $listSeparator);
    }

    public function getByYearDayAsText($byYearDay)
    {
        if (empty($byYearDay)) {
            return '';
        }

        // sort negative indices in reverse order so we get e.g. 1st, 2nd, 4th, 3rd last, last day
        usort($byYearDay, function ($a, $b) {
            if (($a < 0 && $b < 0) || ($a >= 0 && $b >= 0)) {
                return $a - $b;
            }

            return $b - $a;
        });

        $byYearDay = array_map(
            array($this, 'getOrdinalNumber'),
            $byYearDay,
            array_fill(0, count($byYearDay), end($byYearDay) < 0)
        );

        return $this->getListStringFromArray($byYearDay);
    }

    public function getByWeekNumberAsText($byWeekNum)
    {
        if (empty($byWeekNum)) {
            return '';
        }

        if (count($byWeekNum) > 1) {
            sort($byWeekNum);
        }

        return $this->getListStringFromArray($byWeekNum);
    }

    protected function addFragment($fragment)
    {
        if ($fragment && $fragment !== ' ') {
            $this->fragments[] = $fragment;
        }
    }

    public function resetFragments()
    {
        $this->fragments = array();
    }

    protected function isPlural($number)
    {
        return $number % 100 != 1;
    }


    protected function getOrdinalNumber($number, $hasNegatives = false, $dayInMonth = false)
    {
        if (!preg_match('{^-?\d+$}D', $number)) {
            throw new \RuntimeException('$number must be a whole number');
        }

        return $this->translator->trans('ordinal_number', array('number' => $number, 'has_negatives' => $hasNegatives, 'day_in_month' => $dayInMonth));
    }

    protected function getListStringFromArray($values, $separator = 'and')
    {
        $separator = $this->translator->trans($separator);

        if (!is_array($values)) {
            throw new \RuntimeException('$values must be an array.');
        }

        $numValues = count($values);

        if (!$numValues) {
            return '';
        }

        if ($numValues == 1) {
            reset($values);

            return current($values);
        }

        if ($numValues == 2) {
            return implode(" $separator ", $values);
        }

        $lastValue = array_pop($values);
        $output    = implode(', ', $values);
        $output .= " $separator ".$lastValue;

        return $output;
    }
}
