package strftime

import (
	"fmt"
	"sync"

	"github.com/lestrrat-go/strftime/internal/errors"
)

// because there is no such thing was a sync.RWLocker
type rwLocker interface {
	RLock()
	RUnlock()
	sync.Locker
}

// SpecificationSet is a container for patterns that Strftime uses.
// If you want a custom strftime, you can copy the default
// SpecificationSet and tweak it
type SpecificationSet interface {
	Lookup(byte) (Appender, error)
	Delete(byte) error
	Set(byte, Appender) error
}

type specificationSet struct {
	mutable bool
	lock    rwLocker
	store   map[byte]Appender
}

// The default specification set does not need any locking as it is never
// accessed from the outside, and is never mutated.
var defaultSpecificationSet SpecificationSet

func init() {
	defaultSpecificationSet = newImmutableSpecificationSet()
}

func newImmutableSpecificationSet() SpecificationSet {
	// Create a mutable one so that populateDefaultSpecifications work through
	// its magic, then copy the associated map
	// (NOTE: this is done this way because there used to be
	// two struct types for specification set, united under an interface.
	// it can now be removed, but we would need to change the entire
	// populateDefaultSpecifications method, and I'm currently too lazy
	// PRs welcome)
	tmp := NewSpecificationSet()

	ss := &specificationSet{
		mutable: false,
		lock:    nil, // never used, so intentionally not initialized
		store:   tmp.(*specificationSet).store,
	}

	return ss
}

// NewSpecificationSet creates a specification set with the default specifications.
func NewSpecificationSet() SpecificationSet {
	ds := &specificationSet{
		mutable: true,
		lock:    &sync.RWMutex{},
		store:   make(map[byte]Appender),
	}
	populateDefaultSpecifications(ds)

	return ds
}

var defaultSpecifications = map[byte]Appender{
	'A': fullWeekDayName,
	'a': abbrvWeekDayName,
	'B': fullMonthName,
	'b': abbrvMonthName,
	'C': centuryDecimal,
	'c': timeAndDate,
	'D': mdy,
	'd': dayOfMonthZeroPad,
	'e': dayOfMonthSpacePad,
	'F': ymd,
	'H': twentyFourHourClockZeroPad,
	'h': abbrvMonthName,
	'I': twelveHourClockZeroPad,
	'j': dayOfYear,
	'k': twentyFourHourClockSpacePad,
	'l': twelveHourClockSpacePad,
	'M': minutesZeroPad,
	'm': monthNumberZeroPad,
	'n': newline,
	'p': ampm,
	'R': hm,
	'r': imsp,
	'S': secondsNumberZeroPad,
	'T': hms,
	't': tab,
	'U': weekNumberSundayOrigin,
	'u': weekdayMondayOrigin,
	'V': weekNumberMondayOriginOneOrigin,
	'v': eby,
	'W': weekNumberMondayOrigin,
	'w': weekdaySundayOrigin,
	'X': natReprTime,
	'x': natReprDate,
	'Y': year,
	'y': yearNoCentury,
	'Z': timezone,
	'z': timezoneOffset,
	'%': percent,
}

func populateDefaultSpecifications(ds SpecificationSet) {
	for c, handler := range defaultSpecifications {
		if err := ds.Set(c, handler); err != nil {
			panic(fmt.Sprintf("failed to set default specification for %c: %s", c, err))
		}
	}
}

func (ds *specificationSet) Lookup(b byte) (Appender, error) {
	if ds.mutable {
		ds.lock.RLock()
		defer ds.lock.RLock()
	}
	v, ok := ds.store[b]
	if !ok {
		return nil, errors.Errorf(`lookup failed: '%%%c' was not found in specification set`, b)
	}
	return v, nil
}

func (ds *specificationSet) Delete(b byte) error {
	if !ds.mutable {
		return errors.New(`delete failed: this specification set is marked immutable`)
	}

	ds.lock.Lock()
	defer ds.lock.Unlock()
	delete(ds.store, b)
	return nil
}

func (ds *specificationSet) Set(b byte, a Appender) error {
	if !ds.mutable {
		return errors.New(`set failed: this specification set is marked immutable`)
	}

	ds.lock.Lock()
	defer ds.lock.Unlock()
	ds.store[b] = a
	return nil
}
