package strftime

import (
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"
)

// These are all of the standard, POSIX compliant specifications.
// Extensions should be in extensions.go
var (
	fullWeekDayName             = StdlibFormat("Monday")
	abbrvWeekDayName            = StdlibFormat("Mon")
	fullMonthName               = StdlibFormat("January")
	abbrvMonthName              = StdlibFormat("Jan")
	centuryDecimal              = AppendFunc(appendCentury)
	timeAndDate                 = StdlibFormat("Mon Jan _2 15:04:05 2006")
	mdy                         = StdlibFormat("01/02/06")
	dayOfMonthZeroPad           = StdlibFormat("02")
	dayOfMonthSpacePad          = StdlibFormat("_2")
	ymd                         = StdlibFormat("2006-01-02")
	twentyFourHourClockZeroPad  = StdlibFormat("15")
	twelveHourClockZeroPad      = StdlibFormat("03")
	dayOfYear                   = AppendFunc(appendDayOfYear)
	twentyFourHourClockSpacePad = &hourPadded{twelveHour: false, pad: ' '}
	twelveHourClockSpacePad     = &hourPadded{twelveHour: true, pad: ' '}
	minutesZeroPad              = StdlibFormat("04")
	monthNumberZeroPad          = StdlibFormat("01")
	newline                     = Verbatim("\n")
	ampm                        = StdlibFormat("PM")
	hm                          = StdlibFormat("15:04")
	imsp                        = hmsWAMPM{}
	secondsNumberZeroPad        = StdlibFormat("05")
	hms                         = StdlibFormat("15:04:05")
	tab                         = Verbatim("\t")
	weekNumberSundayOrigin      = weeknumberOffset(true) // week number of the year, Sunday first
	weekdayMondayOrigin         = weekday(1)
	// monday as the first day, and 01 as the first value
	weekNumberMondayOriginOneOrigin = AppendFunc(appendWeekNumber)
	eby                             = StdlibFormat("_2-Jan-2006")
	weekyear                        = AppendFunc(appendWeekYear)          // week year, with century
	weekyearNoCentury               = AppendFunc(appendWeekYearNoCentury) // week year, without century
	// monday as the first day, and 00 as the first value
	weekNumberMondayOrigin = weeknumberOffset(false) // week number of the year, Monday first
	weekdaySundayOrigin    = weekday(0)
	natReprTime            = StdlibFormat("15:04:05") // national representation of the time XXX is this correct?
	natReprDate            = StdlibFormat("01/02/06") // national representation of the date XXX is this correct?
	year                   = StdlibFormat("2006")     // year with century
	yearNoCentury          = StdlibFormat("06")       // year w/o century
	timezone               = StdlibFormat("MST")      // time zone name
	timezoneOffset         = StdlibFormat("-0700")    // time zone ofset from UTC
	percent                = Verbatim("%")
)

// Appender is the interface that must be fulfilled by components that
// implement the translation of specifications to actual time value.
//
// The Append method takes the accumulated byte buffer, and the time to
// use to generate the textual representation. The resulting byte
// sequence must be returned by this method, normally by using the
// append() builtin function.
type Appender interface {
	Append([]byte, time.Time) []byte
}

// AppendFunc is an utility type to allow users to create a
// function-only version of an Appender
type AppendFunc func([]byte, time.Time) []byte

func (af AppendFunc) Append(b []byte, t time.Time) []byte {
	return af(b, t)
}

type appenderList []Appender

type dumper interface {
	dump(io.Writer)
}

func (l appenderList) dump(out io.Writer) {
	var buf bytes.Buffer
	ll := len(l)
	for i, a := range l {
		if dumper, ok := a.(dumper); ok {
			dumper.dump(&buf)
		} else {
			fmt.Fprintf(&buf, "%#v", a)
		}

		if i < ll-1 {
			fmt.Fprintf(&buf, ",\n")
		}
	}
	if _, err := buf.WriteTo(out); err != nil {
		panic(err)
	}
}

// does the time.Format thing
type stdlibFormat struct {
	s string
}

// StdlibFormat returns an Appender that simply goes through `time.Format()`
// For example, if you know you want to display the abbreviated month name for %b,
// you can create a StdlibFormat with the pattern `Jan` and register that
// for specification `b`:
//
// a  := StdlibFormat(`Jan`)
// ss := NewSpecificationSet()
// ss.Set('b', a) // does %b -> abbreviated month name
func StdlibFormat(s string) Appender {
	return &stdlibFormat{s: s}
}

func (v stdlibFormat) Append(b []byte, t time.Time) []byte {
	return t.AppendFormat(b, v.s)
}

func (v stdlibFormat) str() string {
	return v.s
}

func (v stdlibFormat) canCombine() bool {
	return true
}

func (v stdlibFormat) combine(w combiner) Appender {
	return StdlibFormat(v.s + w.str())
}

func (v stdlibFormat) dump(out io.Writer) {
	fmt.Fprintf(out, "stdlib: %s", v.s)
}

type verbatimw struct {
	s string
}

// Verbatim returns an Appender suitable for generating static text.
// For static text, this method is slightly favorable than creating
// your own appender, as adjacent verbatim blocks will be combined
// at compile time to produce more efficient Appenders
func Verbatim(s string) Appender {
	return &verbatimw{s: s}
}

func (v verbatimw) Append(b []byte, _ time.Time) []byte {
	return append(b, v.s...)
}

func (v verbatimw) canCombine() bool {
	return canCombine(v.s)
}

func (v verbatimw) combine(w combiner) Appender {
	if _, ok := w.(*stdlibFormat); ok {
		return StdlibFormat(v.s + w.str())
	}
	return Verbatim(v.s + w.str())
}

func (v verbatimw) str() string {
	return v.s
}

func (v verbatimw) dump(out io.Writer) {
	fmt.Fprintf(out, "verbatim: %s", v.s)
}

// unpadded wraps another Appender and strips the padding from the value it
// produces. It implements the glibc '-' and Windows '#' flags, which suppress
// the leading zeros or spaces on numeric fields (e.g. %-d -> "1" instead of
// "01"). Non-numeric fields are emitted unchanged, matching libc behavior.
type unpadded struct {
	inner Appender
}

func (v unpadded) Append(b []byte, t time.Time) []byte {
	var buf [64]byte
	s := v.inner.Append(buf[:0], t)

	// drop leading spaces (space-padded fields such as %e, %k, %l)
	i := 0
	for i < len(s) && s[i] == ' ' {
		i++
	}
	s = s[i:]

	// keep a leading sign, if any (e.g. the "+"/"-" of %z)
	if len(s) > 0 && (s[0] == '+' || s[0] == '-') {
		b = append(b, s[0])
		s = s[1:]
	}

	// drop leading zeros, but always keep at least one digit
	j := 0
	for j < len(s)-1 && s[j] == '0' {
		j++
	}
	return append(b, s[j:]...)
}

func (v unpadded) dump(out io.Writer) {
	fmt.Fprintf(out, "unpadded[")
	if d, ok := v.inner.(dumper); ok {
		d.dump(out)
	} else {
		fmt.Fprintf(out, "%#v", v.inner)
	}
	fmt.Fprintf(out, "]")
}

// These words below, as well as any decimal character
var combineExclusion = []string{
	"Mon",
	"Monday",
	"Jan",
	"January",
	"MST",
	"PM",
	"pm",
}

func canCombine(s string) bool {
	if strings.ContainsAny(s, "0123456789") {
		return false
	}
	for _, word := range combineExclusion {
		if strings.Contains(s, word) {
			return false
		}
	}
	return true
}

type combiner interface {
	canCombine() bool
	combine(combiner) Appender
	str() string
}

// this is container for the compiler to keep track of appenders,
// and combine them as we parse and compile the pattern
type combiningAppend struct {
	list           appenderList
	prev           Appender
	prevCanCombine bool
}

func (ca *combiningAppend) Append(w Appender) {
	if ca.prevCanCombine {
		prev, prevOK := ca.prev.(combiner)
		if wc, ok := w.(combiner); ok && prevOK && wc.canCombine() {
			ca.prev = prev.combine(wc)
			ca.list[len(ca.list)-1] = ca.prev
			return
		}
	}

	ca.list = append(ca.list, w)
	ca.prev = w
	ca.prevCanCombine = false
	if comb, ok := w.(combiner); ok {
		if comb.canCombine() {
			ca.prevCanCombine = true
		}
	}
}

func appendCentury(b []byte, t time.Time) []byte {
	n := t.Year() / 100
	if n < 10 {
		b = append(b, '0')
	}
	return append(b, strconv.Itoa(n)...)
}

type weekday int

func (v weekday) Append(b []byte, t time.Time) []byte {
	n := int(t.Weekday())
	if n < int(v) {
		n += 7
	}
	return append(b, byte(n+48))
}

type weeknumberOffset bool

func (v weeknumberOffset) Append(b []byte, t time.Time) []byte {
	offset := int(t.Weekday())
	if v {
		offset = 6 - offset
	} else if offset != 0 {
		offset = 7 - offset
	}
	n := (t.YearDay() + offset) / 7
	if n < 10 {
		b = append(b, '0')
	}
	return append(b, strconv.Itoa(n)...)
}

func appendWeekYear(b []byte, t time.Time) []byte {
	year, _ := t.ISOWeek()

	// Handle negative years (BCE)
	if year < 0 {
		b = append(b, '-')
		year = -year
	}
	// Ensure 4-digit formatting
	if year < 1000 {
		if year < 10 {
			b = append(b, '0', '0', '0')
		} else if year < 100 {
			b = append(b, '0', '0')
		} else {
			b = append(b, '0')
		}
	}
	return append(b, strconv.Itoa(year)...)
}

func appendWeekYearNoCentury(b []byte, t time.Time) []byte {
	year, _ := t.ISOWeek()
	if year < 0 {
		b = append(b, '-')
		year = -year
	}
	yearNoCentury := year % 100
	if yearNoCentury < 10 {
		b = append(b, '0')
	}
	return append(b, strconv.Itoa(yearNoCentury)...)
}

func appendWeekNumber(b []byte, t time.Time) []byte {
	_, n := t.ISOWeek()
	if n < 10 {
		b = append(b, '0')
	}
	return append(b, strconv.Itoa(n)...)
}

func appendDayOfYear(b []byte, t time.Time) []byte {
	n := t.YearDay()
	if n < 10 {
		b = append(b, '0', '0')
	} else if n < 100 {
		b = append(b, '0')
	}
	return append(b, strconv.Itoa(n)...)
}

type hourPadded struct {
	pad        byte
	twelveHour bool
}

func (v hourPadded) Append(b []byte, t time.Time) []byte {
	h := t.Hour()
	if v.twelveHour && h > 12 {
		h = h - 12
	}
	if v.twelveHour && h == 0 {
		h = 12
	}

	if h < 10 {
		b = append(b, v.pad)
		b = append(b, byte(h+48))
	} else {
		b = unrollTwoDigits(b, h)
	}
	return b
}

func unrollTwoDigits(b []byte, v int) []byte {
	b = append(b, byte((v/10)+48))
	b = append(b, byte((v%10)+48))
	return b
}

type hmsWAMPM struct{}

func (v hmsWAMPM) Append(b []byte, t time.Time) []byte {
	h := t.Hour()
	var am bool

	if h == 0 {
		b = append(b, '1')
		b = append(b, '2')
		am = true
	} else {
		switch {
		case h == 12:
			// no op
		case h > 12:
			h = h - 12
		default:
			am = true
		}
		b = unrollTwoDigits(b, h)
	}
	b = append(b, ':')
	b = unrollTwoDigits(b, t.Minute())
	b = append(b, ':')
	b = unrollTwoDigits(b, t.Second())

	b = append(b, ' ')
	if am {
		b = append(b, 'A')
	} else {
		b = append(b, 'P')
	}
	b = append(b, 'M')

	return b
}
