<?php

namespace Leantime\Core\Support;

use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\Date;
use Leantime\Core\Language;
use PHPUnit\Exception;

/**
 * Class Format
 *
 * This class provides various formatting methods for simple data types (strings, ints, floats)
 */
class Format
{
    private mixed $value = '';

    private mixed $value2 = '';

    private DateTimeHelper $dateTimeHelper;

    private Language $language;

    /**
     * Creates a new instance of the class.
     *
     * PSA: This class will NOT throw exceptions or error messages since it is a user facing string formatting class.
     * If you need to evaluate correct parsing of datetimes use the datetime helper and not this format class.
     *
     * @param  string|int|float  $value  The value to be assigned. If empty, the constructor will return early.
     * @param  null|string|int|float  $value2  The second value to be assigned. It can be null. Used for certain cases
     *                                         as specified by $fromFormat.
     * @param  FromFormat|null  $fromFormat  The format of the values. Can be one of the constants defined in the
     *                                       FromFormat class.
     * @return void
     */
    public function __construct(
        string|int|float|null|\DateTimeInterface|CarbonInterface $value,
        string|int|float|null|\DateTimeInterface|CarbonInterface $value2,
        ?FromFormat $fromFormat = FromFormat::DbDate
    ) {

        $this->dateTimeHelper = app()->make(DateTimeHelper::class);
        $this->language = app()->make(Language::class);

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

        if ($value instanceof \DateTime) {
            $value = CarbonImmutable::create($value);
            $this->value = CarbonImmutable::create($value);
        }

        if ($value2 instanceof \DateTime) {
            $value2 = CarbonImmutable::create($value2);
            $this->value = CarbonImmutable::create($value2);
        }

        try {
            switch ($fromFormat) {
                case FromFormat::DbDate:
                    $this->value = $this->dateTimeHelper->parseDbDateTime($value);
                    break;
                case FromFormat::UserDateTime:
                    $this->value = $this->dateTimeHelper->parseUserDateTime($value, $value2);
                    break;
                case FromFormat::User24hTime:
                    $this->value = $this->dateTimeHelper->parseUser24hTime($value);
                    break;
                case FromFormat::Db24hTime:
                    $this->value = $this->dateTimeHelper->parseDb24hTime($value);
                    break;
                case FromFormat::UserDateStartOfDay:
                    $this->value = $this->dateTimeHelper->parseUserDateTime($value, 'start');
                    break;
                case FromFormat::UserDateEndOfDay:
                    $this->value = $this->dateTimeHelper->parseUserDateTime($value, 'end');
                    break;
                default:
                    $this->value = $value;
                    break;
            }
        } catch (\Throwable $e) {
            // Several things can throw exceptions in the date parsing scripts.
            // Most common is an invalid date format. This could also be an empty string or a 0000-00... date.
            // Since this format class is purely for user facing purposes we will not show an error message
            // but return an empty string.
            $this->value = $value;

            return;
        }
    }

    /**
     * Returns the user formatted date string based on the 'value' property.
     *
     * @param  string  $emptyOutput  The output to be returned when the 'value' property is empty.
     *                               Defaults to an empty string.
     * @return string The formatted date string or the $emptyOutput if the 'value' property is empty or
     *                the formatted date string is empty.
     */
    public function date(string $emptyOutput = ''): string
    {

        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return $emptyOutput;
        }

        $formattedDate = $this->value->formatDateForUser();

        return $formattedDate !== '' ? $formattedDate : $emptyOutput;
    }

    /**
     * Retrieve the formatted time string from the ISO value.
     * Returns an empty string if 'value' is null.
     *
     * @return string The formatted time string.
     */
    public function time(): string
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->formatTimeForUser();
    }

    /**
     * Generates an ISO 8601 formatted date and time string in user timezone
     *
     * @return string The ISO 8601 formatted date and time string. Returns an empty string if the value is null.
     */
    public function isoDateTime(): string
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->setToUserTimezone()->format('Y-m-d H:i:s');
    }

    /**
     * Generates an ISO 8601 formatted date and time string in UTC timezone
     *
     * @return string The ISO 8601 formatted date and time string. Returns an empty string if the value is null.
     */
    public function isoDateTimeUTC(): string
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->formatDateTimeForDb();
    }

    /**
     * @deprecated
     *
     * This method is deprecated and only included because of plugin backwards compatibility.
     * Once all plugins are updated this will be removed.
     *
     * @return string The ISO 8601 formatted date string. Returns an empty string if the value is null.
     */
    public function isoDate(): string
    {

        if (empty($this->value)) {
            return '';
        }

        // This method should not be used anymore however we have plugins that are still using it and they will not have
        // the new enum values. So they are still calling format($var)->isoDate() without a enum modifier that would
        // indicate that this is a user date (which it was historically).
        // So now we have to shuffle things around and since the format was probably not correct anyways, let's reparse

        if (! $this->value instanceof CarbonImmutable) {

            try {
                $this->value = $this->dateTimeHelper->parseUserDateTime($this->value, 'start');
            } catch (Exception $e) {
                report($e);

                return '';
            }

        } else {

            // If for some reason Carbon was able to parse the date we'll need to make sure the timezone is set to the
            // users timezone.

            // Date was falsly parsed as UTC but is actually user date. Shift timezone.
            $userTimezone = session('usersettings.timezone');
            // Carbon shift timezone will change timezone without actually changing the numbers
            $this->value->shiftTimezone($userTimezone);
        }

        return $this->value->formatDateTimeForDb();
    }

    /**
     * Generate unix timestamp from date.
     */
    public function timestamp(): int|bool
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->getTimestamp();
    }

    /**
     * Generate unix timestamp from date in miliseconds for javascript usage
     */
    public function jsTimestamp(): int|bool
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->getTimestampMs();
    }

    /**
     * Retrieves the 24-hour time string from the ISO formatted value property.
     *
     * @return string The 24-hour time string. If the value property is null, an empty string is returned.
     */
    public function time24(): string
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->format24HTimeForUser();
    }

    /**
     * Converts a 24-hour formatted time string to a user-friendly time string.
     *
     * @return string The user-friendly time string in the format "H:i A". Returns an empty string if the value is null.
     */
    public function userTime24toUserTime(): string
    {
        if (empty($this->value) || ! $this->value instanceof CarbonImmutable) {
            return '';
        }

        return $this->value->formatTimeForUser();

    }

    /**
     * Generates a string representation of currency.
     *
     * @return string The string representation of currency.
     */
    public function currency(): string
    {
        if ($this->value == null) {
            return '';
        }

        return $this->language->__('language.currency').''.number_format($this->value, 2);
    }

    /**
     * Generates a string representation of a percentage.
     *
     * @return string The percentage string. Returns an empty string if the value is null.
     *                If the second value is empty, the first value is returned with a "%" sign appended.
     *                If both values are set, the percentage is calculated and formatted to two decimal places, followed by a "%" sign.
     */
    public function percent(): string
    {
        // First value empty, just return empty string
        if ($this->value == null) {
            return '';
        }

        // Second value empty return first value with % sign
        if (empty($this->value2)) {
            return number_format($this->value, 2).'%';
        }

        // Both values set. Return percent calculation
        $percent = ($this->value / $this->value2) * 100;

        return number_format($percent, 2).'%';
    }

    /**
     * Formats a decimal number with two decimal places.
     *
     * @return string The decimal number formatted with two decimal places.
     */
    public function decimal(): string
    {
        return number_format((float) $this->value, 2);
    }

    public function diffForHumans(): string
    {
        if ($this->value->isToday()) {
            return $this->language->__('dates.today');
        } elseif ($this->value->isYesterday()) {
            return $this->language->__('dates.yesterday');
        } elseif ($this->value->isTomorrow()) {
            return $this->language->__('dates.tomorrow');
        } else {
            return $this->value->endOfDay()->diffForHumans();
        }
    }

    /**
     * Format bytes to human readable format
     *
     * @param  int  $bytes  The number of bytes
     * @param  int  $precision  The number of decimal places
     * @return string The formatted size
     */
    public function formatBytes(): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];

        $bytes = max($this->value, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);

        return round($bytes / (1024 ** $pow), 2).' '.$units[$pow];
    }
}
