// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package gtime

import (
	"bytes"
	"regexp"
	"strconv"
	"strings"

	"github.com/gogf/gf/v2/text/gregex"
)

var (
	// Refer: http://php.net/manual/en/function.date.php
	formats = map[byte]string{
		'd': "02",                        // Day: Day of the month, 2 digits with leading zeros. Eg: 01 to 31.
		'D': "Mon",                       // Day: A textual representation of a day, three letters. Eg: Mon through Sun.
		'w': "Monday",                    // Day: Numeric representation of the day of the week. Eg: 0 (for Sunday) through 6 (for Saturday).
		'N': "Monday",                    // Day: ISO-8601 numeric representation of the day of the week. Eg: 1 (for Monday) through 7 (for Sunday).
		'j': "=j=02",                     // Day: Day of the month without leading zeros. Eg: 1 to 31.
		'S': "02",                        // Day: English ordinal suffix for the day of the month, 2 characters. Eg: st, nd, rd or th. Works well with j.
		'l': "Monday",                    // Day: A full textual representation of the day of the week. Eg: Sunday through Saturday.
		'z': "",                          // Day: The day of the year (starting from 0). Eg: 0 through 365.
		'W': "",                          // Week: ISO-8601 week number of year, weeks starting on Monday. Eg: 42 (the 42nd week in the year).
		'F': "January",                   // Month: A full textual representation of a month, such as January or March. Eg: January through December.
		'm': "01",                        // Month: Numeric representation of a month, with leading zeros. Eg: 01 through 12.
		'M': "Jan",                       // Month: A short textual representation of a month, three letters. Eg: Jan through Dec.
		'n': "1",                         // Month: Numeric representation of a month, without leading zeros. Eg: 1 through 12.
		't': "",                          // Month: Number of days in the given month. Eg: 28 through 31.
		'Y': "2006",                      // Year: A full numeric representation of a year, 4 digits. Eg: 1999 or 2003.
		'y': "06",                        // Year: A two-digit representation of a year. Eg: 99 or 03.
		'a': "pm",                        // Time: Lowercase Ante meridiem and Post meridiem. Eg: am or pm.
		'A': "PM",                        // Time: Uppercase Ante meridiem and Post meridiem. Eg: AM or PM.
		'g': "3",                         // Time: 12-hour format of an hour without leading zeros. Eg: 1 through 12.
		'G': "=G=15",                     // Time: 24-hour format of an hour without leading zeros. Eg: 0 through 23.
		'h': "03",                        // Time: 12-hour format of an hour with leading zeros. Eg: 01 through 12.
		'H': "15",                        // Time: 24-hour format of an hour with leading zeros. Eg: 00 through 23.
		'i': "04",                        // Time: Minutes with leading zeros. Eg: 00 to 59.
		's': "05",                        // Time: Seconds with leading zeros. Eg: 00 through 59.
		'u': "=u=.000",                   // Time: Milliseconds. Eg: 234, 678.
		'U': "",                          // Time: Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT).
		'O': "-0700",                     // Zone: Difference to Greenwich time (GMT) in hours. Eg: +0200.
		'P': "-07:00",                    // Zone: Difference to Greenwich time (GMT) with colon between hours and minutes. Eg: +02:00.
		'T': "MST",                       // Zone: Timezone abbreviation. Eg: UTC, EST, MDT ...
		'c': "2006-01-02T15:04:05-07:00", // Format: ISO 8601 date. Eg: 2004-02-12T15:19:21+00:00.
		'r': "Mon, 02 Jan 06 15:04 MST",  // Format: RFC 2822 formatted date. Eg: Thu, 21 Dec 2000 16:01:07 +0200.
	}

	// Week to number mapping.
	weekMap = map[string]string{
		"Sunday":    "0",
		"Monday":    "1",
		"Tuesday":   "2",
		"Wednesday": "3",
		"Thursday":  "4",
		"Friday":    "5",
		"Saturday":  "6",
	}

	// Day count of each month which is not in leap year.
	dayOfMonth = []int{0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}
)

// Format formats and returns the formatted result with custom `format`.
// Refer method Layout, if you want to follow stdlib layout.
func (t *Time) Format(format string) string {
	if t == nil {
		return ""
	}
	runes := []rune(format)
	buffer := bytes.NewBuffer(nil)
	for i := 0; i < len(runes); {
		switch runes[i] {
		case '\\':
			if i < len(runes)-1 {
				buffer.WriteRune(runes[i+1])
				i += 2
				continue
			} else {
				return buffer.String()
			}
		case 'W':
			buffer.WriteString(strconv.Itoa(t.WeeksOfYear()))
		case 'z':
			buffer.WriteString(strconv.Itoa(t.DayOfYear()))
		case 't':
			buffer.WriteString(strconv.Itoa(t.DaysInMonth()))
		case 'U':
			buffer.WriteString(strconv.FormatInt(t.Unix(), 10))
		default:
			if runes[i] > 255 {
				buffer.WriteRune(runes[i])
				break
			}
			if f, ok := formats[byte(runes[i])]; ok {
				result := t.Time.Format(f)
				// Particular chars should be handled here.
				switch runes[i] {
				case 'j':
					for _, s := range []string{"=j=0", "=j="} {
						result = strings.ReplaceAll(result, s, "")
					}
					buffer.WriteString(result)
				case 'G':
					for _, s := range []string{"=G=0", "=G="} {
						result = strings.ReplaceAll(result, s, "")
					}
					buffer.WriteString(result)
				case 'u':
					buffer.WriteString(strings.ReplaceAll(result, "=u=.", ""))
				case 'w':
					buffer.WriteString(weekMap[result])
				case 'N':
					buffer.WriteString(strings.ReplaceAll(weekMap[result], "0", "7"))
				case 'S':
					buffer.WriteString(formatMonthDaySuffixMap(result))
				default:
					buffer.WriteString(result)
				}
			} else {
				buffer.WriteRune(runes[i])
			}
		}
		i++
	}
	return buffer.String()
}

// FormatNew formats and returns a new Time object with given custom `format`.
func (t *Time) FormatNew(format string) *Time {
	if t == nil {
		return nil
	}
	return NewFromStr(t.Format(format))
}

// FormatTo formats `t` with given custom `format`.
func (t *Time) FormatTo(format string) *Time {
	if t == nil {
		return nil
	}
	t.Time = NewFromStr(t.Format(format)).Time
	return t
}

// Layout formats the time with stdlib layout and returns the formatted result.
func (t *Time) Layout(layout string) string {
	if t == nil {
		return ""
	}
	return t.Time.Format(layout)
}

// LayoutNew formats the time with stdlib layout and returns the new Time object.
func (t *Time) LayoutNew(layout string) *Time {
	if t == nil {
		return nil
	}
	newTime, err := StrToTimeLayout(t.Layout(layout), layout)
	if err != nil {
		panic(err)
	}
	return newTime
}

// LayoutTo formats `t` with stdlib layout.
func (t *Time) LayoutTo(layout string) *Time {
	if t == nil {
		return nil
	}
	newTime, err := StrToTimeLayout(t.Layout(layout), layout)
	if err != nil {
		panic(err)
	}
	t.Time = newTime.Time
	return t
}

// IsLeapYear checks whether the time is leap year.
func (t *Time) IsLeapYear() bool {
	year := t.Year()
	if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
		return true
	}
	return false
}

// DayOfYear checks and returns the position of the day for the year.
func (t *Time) DayOfYear() int {
	var (
		day   = t.Day()
		month = t.Month()
	)
	if t.IsLeapYear() {
		if month > 2 {
			return dayOfMonth[month-1] + day
		}
		return dayOfMonth[month-1] + day - 1
	}
	return dayOfMonth[month-1] + day - 1
}

// DaysInMonth returns the day count of current month.
func (t *Time) DaysInMonth() int {
	switch t.Month() {
	case 1, 3, 5, 7, 8, 10, 12:
		return 31
	case 4, 6, 9, 11:
		return 30
	}
	if t.IsLeapYear() {
		return 29
	}
	return 28
}

// WeeksOfYear returns the point of current week for the year.
func (t *Time) WeeksOfYear() int {
	_, week := t.ISOWeek()
	return week
}

// formatToStdLayout converts custom format to stdlib layout.
func formatToStdLayout(format string) string {
	b := bytes.NewBuffer(nil)
	for i := 0; i < len(format); {
		switch format[i] {
		case '\\':
			if i < len(format)-1 {
				b.WriteByte(format[i+1])
				i += 2
				continue
			} else {
				return b.String()
			}

		default:
			if f, ok := formats[format[i]]; ok {
				// Handle particular chars.
				switch format[i] {
				case 'j':
					b.WriteString("2")
				case 'G':
					b.WriteString("15")
				case 'u':
					if i > 0 && format[i-1] == '.' {
						b.WriteString("000")
					} else {
						b.WriteString(".000")
					}

				default:
					b.WriteString(f)
				}
			} else {
				b.WriteByte(format[i])
			}
			i++
		}
	}
	return b.String()
}

// formatToRegexPattern converts the custom format to its corresponding regular expression.
func formatToRegexPattern(format string) string {
	s := regexp.QuoteMeta(formatToStdLayout(format))
	s, _ = gregex.ReplaceString(`[0-9]`, `[0-9]`, s)
	s, _ = gregex.ReplaceString(`[A-Za-z]`, `[A-Za-z]`, s)
	s, _ = gregex.ReplaceString(`\s+`, `\s+`, s)
	return s
}

// formatMonthDaySuffixMap returns the short english word for current day.
func formatMonthDaySuffixMap(day string) string {
	switch day {
	case "01", "21", "31":
		return "st"
	case "02", "22":
		return "nd"
	case "03", "23":
		return "rd"
	default:
		return "th"
	}
}
