// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package dtfmt

import (
	"errors"
	"fmt"
	"io"
	"strings"
	"sync"
	"time"
	"unicode/utf8"
)

// Formatter will format time values into strings, based on pattern used to
// create the Formatter.
type Formatter struct {
	prog   prog
	sz     int
	config ctxConfig
}

var ctxPool = &sync.Pool{
	New: func() interface{} { return &ctx{} },
}

func newCtx() *ctx {
	return ctxPool.Get().(*ctx)
}

func newCtxWithSize(sz int) *ctx {
	ctx := newCtx()
	if ctx.buf == nil || cap(ctx.buf) < sz {
		ctx.buf = make([]byte, 0, sz)
	}
	return ctx
}

func releaseCtx(c *ctx) {
	ctxPool.Put(c)
}

// NewFormatter creates a new time formatter based on provided pattern.
// If pattern is invalid an error is returned.
func NewFormatter(pattern string) (*Formatter, error) {
	b := newBuilder()

	err := parsePatternTo(b, pattern)
	if err != nil {
		return nil, err
	}

	b.optimize()

	cfg, err := b.createConfig()
	if err != nil {
		return nil, err
	}

	prog, err := b.compile()
	if err != nil {
		return nil, err
	}

	sz := b.estimateSize()
	f := &Formatter{
		prog:   prog,
		sz:     sz,
		config: cfg,
	}
	return f, nil
}

// EstimateSize estimates the required buffer size required to hold
// the formatted time string. Estimated size gives no exact guarantees.
// Estimated size might still be too low or too big.
func (f *Formatter) EstimateSize() int {
	return f.sz
}

func (f *Formatter) appendTo(ctx *ctx, b []byte, t time.Time) ([]byte, error) {
	ctx.initTime(&f.config, t)
	return f.prog.eval(b, ctx, t)
}

// AppendTo appends the formatted time value to the given byte buffer.
func (f *Formatter) AppendTo(b []byte, t time.Time) ([]byte, error) {
	ctx := newCtx()
	defer releaseCtx(ctx)
	return f.appendTo(ctx, b, t)
}

// Write writes the formatted time value to the given writer. Returns
// number of bytes written or error if formatter or writer fails.
func (f *Formatter) Write(w io.Writer, t time.Time) (int, error) {
	var err error

	ctx := newCtxWithSize(f.sz)
	defer releaseCtx(ctx)

	ctx.buf, err = f.appendTo(ctx, ctx.buf[:0], t)
	if err != nil {
		return 0, err
	}
	return w.Write(ctx.buf)
}

// Format formats the given time value into a new string.
func (f *Formatter) Format(t time.Time) (string, error) {
	var err error

	ctx := newCtxWithSize(f.sz)
	defer releaseCtx(ctx)

	ctx.buf, err = f.appendTo(ctx, ctx.buf[:0], t)
	if err != nil {
		return "", err
	}
	return string(ctx.buf), nil
}

func parsePatternTo(b *builder, pattern string) error {
	for i := 0; i < len(pattern); {
		tok, tokText, err := parseToken(pattern, &i)
		if err != nil {
			return err
		}

		tokLen := len(tokText)
		switch tok {
		case 'x': // weekyear (year)
			if tokLen == 2 {
				b.twoDigitWeekYear()
			} else {
				b.weekyear(tokLen, 4)
			}

		case 'y', 'Y': // year and year of era (year) == 'y'
			if tokLen == 2 {
				b.twoDigitYear()
			} else {
				b.year(tokLen, 4)
			}

		case 'w': // week of weekyear (num)
			b.weekOfWeekyear(tokLen)

		case 'e': // day of week (num)
			b.dayOfWeek(tokLen)

		case 'E': // day of week (text)
			if tokLen >= 4 {
				b.dayOfWeekText()
			} else {
				b.dayOfWeekShortText()
			}

		case 'D': // day of year (number)
			b.dayOfYear(tokLen)

		case 'M': // month of year (month)
			if tokLen >= 3 {
				if tokLen >= 4 {
					b.monthOfYearText()
				} else {
					b.monthOfYearShortText()
				}
			} else {
				b.monthOfYear(tokLen)
			}

		case 'd': //day of month (number)
			b.dayOfMonth(tokLen)

		case 'a': // half of day (text) 'AM/PM'
			b.halfdayOfDayText()

		case 'K': // hour of half day (number) (0 - 11)
			b.hourOfHalfday(tokLen)

		case 'h': // clock hour of half day (number) (1 - 12)
			b.clockhourOfHalfday(tokLen)

		case 'H': // hour of day (number) (0 - 23)
			b.hourOfDay(tokLen)

		case 'k': // clock hour of half day (number) (1 - 24)
			b.clockhourOfDay(tokLen)

		case 'm': // minute of hour
			b.minuteOfHour(tokLen)

		case 's': // second of minute
			b.secondOfMinute(tokLen)

		case 'S': // fraction of second
			b.millisOfSecond(tokLen)

		case 'z': // timezone offset
			b.timeZoneOffsetText()

		case '\'': // literal
			if tokLen == 1 {
				b.appendRune(rune(tokText[0]))
			} else {
				b.appendLiteral(tokText)
			}

		default:
			return fmt.Errorf("unsupport format '%c'", tok)

		}
	}

	return nil
}

func parseToken(pattern string, i *int) (rune, string, error) {
	start := *i
	idx := start
	length := len(pattern)

	r, w := utf8.DecodeRuneInString(pattern[idx:])
	idx += w
	if ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') {
		// Scan a run of the same character, which indicates a time pattern.

		for idx < length {
			peek, w := utf8.DecodeRuneInString(pattern[idx:])
			if peek != r {
				break
			}

			idx += w
		}

		*i = idx
		return r, pattern[start:idx], nil
	}

	if r != '\'' { // single character, no escaped string
		*i = idx
		return '\'', pattern[start:idx], nil
	}

	start = idx // skip ' character
	iEnd := strings.IndexRune(pattern[start:], '\'')
	if iEnd < 0 {
		return r, "", errors.New("missing closing '")
	}

	if iEnd == 0 {
		// escape single quote literal
		*i = idx + 1
		return r, pattern[start : idx+1], nil
	}

	iEnd += start

	*i = iEnd + 1 // point after '
	if len(pattern) > iEnd+1 && pattern[iEnd+1] == '\'' {
		return r, pattern[start : iEnd+1], nil
	}

	return r, pattern[start:iEnd], nil
}
