package values

import (
	"fmt"
	"github.com/dolanor/caldav-go/icalendar/properties"
	"github.com/dolanor/caldav-go/utils"
	"log"
	"strings"
	"time"
)

var _ = log.Print

const DateFormatString = "20060102"
const DateTimeFormatString = "20060102T150405"
const UTCDateTimeFormatString = "20060102T150405Z"

// a representation of a date and time for iCalendar
type DateTime struct {
	t time.Time
}

type DateTimes []*DateTime

// The exception dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set
// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial
// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar
// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE"
// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set
// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE"
// properties, and then excluding any start date and times which fall within the union of start date and times
// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion
// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties
// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only
// one recurrence is considered. Duplicate instances are ignored.
//
// The "EXDATE" property can be used to exclude the value specified in "DTSTART". However, in such cases the original
// "DTSTART" date MUST still be maintained by the calendaring and scheduling system because the original "DTSTART"
// value has inherent usage dependencies by other properties such as the "RECURRENCE-ID".
type ExceptionDateTimes DateTimes

// The recurrence dates, if specified, are used in computing the recurrence set. The recurrence set is the complete set
// of recurrence instances for a calendar component. The recurrence set is generated by considering the initial
// "DTSTART" property along with the "RRULE", "RDATE", "EXDATE" and "EXRULE" properties contained within the iCalendar
// object. The "DTSTART" property defines the first instance in the recurrence set. Multiple instances of the "RRULE"
// and "EXRULE" properties can also be specified to define more sophisticated recurrence sets. The final recurrence set
// is generated by gathering all of the start date-times generated by any of the specified "RRULE" and "RDATE"
// properties, and then excluding any start date and times which fall within the union of start date and times
// generated by any specified "EXRULE" and "EXDATE" properties. This implies that start date and times within exclusion
// related properties (i.e., "EXDATE" and "EXRULE") take precedence over those specified by inclusion properties
// (i.e., "RDATE" and "RRULE"). Where duplicate instances are generated by the "RRULE" and "RDATE" properties, only
// one recurrence is considered. Duplicate instances are ignored.
type RecurrenceDateTimes DateTimes

// creates a new icalendar datetime representation
func NewDateTime(t time.Time) *DateTime {
	return &DateTime{t: t.Truncate(time.Second)}
}

// creates a new icalendar datetime array representation
func NewDateTimes(dates ...*DateTime) DateTimes {
	return DateTimes(dates)
}

// creates a new icalendar datetime array representation
func NewExceptionDateTimes(dates ...*DateTime) *ExceptionDateTimes {
	datetimes := NewDateTimes(dates...)
	return (*ExceptionDateTimes)(&datetimes)
}

// creates a new icalendar datetime array representation
func NewRecurrenceDateTimes(dates ...*DateTime) *RecurrenceDateTimes {
	datetimes := NewDateTimes(dates...)
	return (*RecurrenceDateTimes)(&datetimes)
}

// checks to see if two datetimes are equal
func (d *DateTime) Equals(test *DateTime) bool {
	return d.t.Equal(test.t)
}

// returns the native time for the datetime object
func (d *DateTime) NativeTime() time.Time {
	return d.t
}

// encodes the datetime value for the iCalendar specification
func (d *DateTime) EncodeICalValue() (string, error) {
	val := d.t.Format(DateTimeFormatString)
	loc := d.t.Location()
	if loc == time.UTC {
		val = fmt.Sprintf("%sZ", val)
	}
	return val, nil
}

// decodes the datetime value from the iCalendar specification
func (d *DateTime) DecodeICalValue(value string) error {
	layout := DateTimeFormatString
	if strings.HasSuffix(value, "Z") {
		layout = UTCDateTimeFormatString
	} else if len(value) == 8 {
		layout = DateFormatString
	}
	var err error
	d.t, err = time.ParseInLocation(layout, value, time.UTC)
	if err != nil {
		return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err)
	} else {
		return nil
	}
}

// encodes the datetime params for the iCalendar specification
func (d *DateTime) EncodeICalParams() (params properties.Params, err error) {
	loc := d.t.Location()
	if loc != time.UTC {
		params = properties.Params{properties.TimeZoneIdPropertyName: loc.String()}
	}
	return
}

// decodes the datetime params from the iCalendar specification
func (d *DateTime) DecodeICalParams(params properties.Params) error {
	layout := DateTimeFormatString
	value := d.t.Format(layout)
	if name, found := params[properties.TimeZoneIdPropertyName]; !found {
		return nil
	} else if loc, err := time.LoadLocation(name); err != nil {
		return utils.NewError(d.DecodeICalValue, "unable to parse timezone", d, err)
	} else if t, err := time.ParseInLocation(layout, value, loc); err != nil {
		return utils.NewError(d.DecodeICalValue, "unable to parse datetime value", d, err)
	} else {
		d.t = t
		return nil
	}
}

// validates the datetime value against the iCalendar specification
func (d *DateTime) ValidateICalValue() error {

	loc := d.t.Location()

	if loc == time.Local {
		msg := "DateTime location may not Local, please use UTC or explicit Location"
		return utils.NewError(d.ValidateICalValue, msg, d, nil)
	}

	if loc.String() == "" {
		msg := "DateTime location must have a valid name"
		return utils.NewError(d.ValidateICalValue, msg, d, nil)
	}

	return nil
}

// encodes the datetime value for the iCalendar specification
func (d *DateTime) String() string {
	if s, err := d.EncodeICalValue(); err != nil {
		panic(err)
	} else {
		return s
	}
}

// encodes a list of datetime values for the iCalendar specification
func (ds *DateTimes) EncodeICalValue() (string, error) {
	var csv CSV
	for i, d := range *ds {
		if s, err := d.EncodeICalValue(); err != nil {
			msg := fmt.Sprintf("unable to encode datetime at index %d", i)
			return "", utils.NewError(ds.EncodeICalValue, msg, ds, err)
		} else {
			csv = append(csv, s)
		}
	}
	return csv.EncodeICalValue()
}

// encodes a list of datetime params for the iCalendar specification
func (ds *DateTimes) EncodeICalParams() (params properties.Params, err error) {
	if len(*ds) > 0 {
		params, err = (*ds)[0].EncodeICalParams()
	}
	return
}

// decodes a list of datetime params from the iCalendar specification
func (ds *DateTimes) DecodeICalParams(params properties.Params) error {
	for i, d := range *ds {
		if err := d.DecodeICalParams(params); err != nil {
			msg := fmt.Sprintf("unable to decode datetime params for index %d", i)
			return utils.NewError(ds.DecodeICalValue, msg, ds, err)
		}
	}
	return nil
}

// encodes a list of datetime values for the iCalendar specification
func (ds *DateTimes) DecodeICalValue(value string) error {
	csv := new(CSV)
	if err := csv.DecodeICalValue(value); err != nil {
		return utils.NewError(ds.DecodeICalValue, "unable to decode datetime list as CSV", ds, err)
	}
	for i, value := range *csv {
		d := new(DateTime)
		if err := d.DecodeICalValue(value); err != nil {
			msg := fmt.Sprintf("unable to decode datetime at index %d", i)
			return utils.NewError(ds.DecodeICalValue, msg, ds, err)
		} else {
			*ds = append(*ds, d)
		}
	}
	return nil
}

// encodes exception date times property name for icalendar
func (e *ExceptionDateTimes) EncodeICalName() (properties.PropertyName, error) {
	return properties.ExceptionDateTimesPropertyName, nil
}

// encodes recurrence date times property name for icalendar
func (r *RecurrenceDateTimes) EncodeICalName() (properties.PropertyName, error) {
	return properties.RecurrenceDateTimesPropertyName, nil
}

// encodes exception date times property value for icalendar
func (e *ExceptionDateTimes) EncodeICalValue() (string, error) {
	return (*DateTimes)(e).EncodeICalValue()
}

// encodes recurrence date times property value for icalendar
func (r *RecurrenceDateTimes) EncodeICalValue() (string, error) {
	return (*DateTimes)(r).EncodeICalValue()
}

// decodes exception date times property value for icalendar
func (e *ExceptionDateTimes) DecodeICalValue(value string) error {
	return (*DateTimes)(e).DecodeICalValue(value)
}

// decodes recurrence date times property value for icalendar
func (r *RecurrenceDateTimes) DecodeICalValue(value string) error {
	return (*DateTimes)(r).DecodeICalValue(value)
}

// encodes exception date times property params for icalendar
func (e *ExceptionDateTimes) EncodeICalParams() (params properties.Params, err error) {
	return (*DateTimes)(e).EncodeICalParams()
}

// encodes recurrence date times property params for icalendar
func (r *RecurrenceDateTimes) EncodeICalParams() (params properties.Params, err error) {
	return (*DateTimes)(r).EncodeICalParams()
}

// encodes exception date times property params for icalendar
func (e *ExceptionDateTimes) DecodeICalParams(params properties.Params) error {
	return (*DateTimes)(e).DecodeICalParams(params)
}

// encodes recurrence date times property params for icalendar
func (r *RecurrenceDateTimes) DecodeICalParams(params properties.Params) error {
	return (*DateTimes)(r).DecodeICalParams(params)
}
