// Copyright 2011 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package walk

import (
	"strings"
	"syscall"
	"time"
	"unsafe"

	"github.com/lxn/win"
)

type DateEdit struct {
	WidgetBase
	dateChangedPublisher EventPublisher
	format               string
}

func newDateEdit(parent Container, style uint32) (*DateEdit, error) {
	de := new(DateEdit)

	if err := InitWidget(
		de,
		parent,
		"SysDateTimePick32",
		win.WS_TABSTOP|win.WS_VISIBLE|win.DTS_SHORTDATEFORMAT|style,
		0); err != nil {
		return nil, err
	}

	if style&win.DTS_SHOWNONE != 0 {
		de.setSystemTime(nil)
	}

	de.GraphicsEffects().Add(InteractionEffect)
	de.GraphicsEffects().Add(FocusEffect)

	de.MustRegisterProperty("Date", NewProperty(
		func() interface{} {
			return de.Date()
		},
		func(v interface{}) error {
			return de.SetDate(assertTimeOr(v, time.Time{}))
		},
		de.dateChangedPublisher.Event()))

	return de, nil
}

func NewDateEdit(parent Container) (*DateEdit, error) {
	return newDateEdit(parent, 0)
}

func NewDateEditWithNoneOption(parent Container) (*DateEdit, error) {
	return newDateEdit(parent, win.DTS_SHOWNONE)
}

func (de *DateEdit) systemTimeToTime(st *win.SYSTEMTIME) time.Time {
	if st == nil || !de.hasStyleBits(win.DTS_SHOWNONE) && st.WYear == 1601 && st.WMonth == 1 && st.WDay == 1 {
		return time.Time{}
	}

	var hour, minute, second int
	if de.timeOfDayDisplayed() {
		hour = int(st.WHour)
		minute = int(st.WMinute)
		second = int(st.WSecond)
	}

	return time.Date(int(st.WYear), time.Month(st.WMonth), int(st.WDay), hour, minute, second, 0, time.Local)
}

func (de *DateEdit) timeToSystemTime(t time.Time) *win.SYSTEMTIME {
	if t.Year() < 1601 {
		if de.hasStyleBits(win.DTS_SHOWNONE) {
			return nil
		} else {
			return &win.SYSTEMTIME{
				WYear:  uint16(1601),
				WMonth: uint16(1),
				WDay:   uint16(1),
			}
		}
	}

	st := &win.SYSTEMTIME{
		WYear:  uint16(t.Year()),
		WMonth: uint16(t.Month()),
		WDay:   uint16(t.Day()),
	}

	if de.timeOfDayDisplayed() {
		st.WHour = uint16(t.Hour())
		st.WMinute = uint16(t.Minute())
		st.WSecond = uint16(t.Second())
	}

	return st
}

func (de *DateEdit) systemTime() (*win.SYSTEMTIME, error) {
	var st win.SYSTEMTIME

	switch de.SendMessage(win.DTM_GETSYSTEMTIME, 0, uintptr(unsafe.Pointer(&st))) {
	case win.GDT_VALID:
		return &st, nil

	case win.GDT_NONE:
		return nil, nil
	}

	return nil, newError("SendMessage(DTM_GETSYSTEMTIME)")
}

func (de *DateEdit) setSystemTime(st *win.SYSTEMTIME) error {
	var wParam uintptr

	if st != nil {
		wParam = win.GDT_VALID
	} else {
		// Ensure today's date is displayed.
		de.setSystemTime(de.timeToSystemTime(time.Now()))

		wParam = win.GDT_NONE
	}

	if 0 == de.SendMessage(win.DTM_SETSYSTEMTIME, wParam, uintptr(unsafe.Pointer(st))) {
		return newError("SendMessage(DTM_SETSYSTEMTIME)")
	}

	return nil
}

func (de *DateEdit) timeOfDayDisplayed() bool {
	return strings.ContainsAny(de.format, "Hhms")
}

func (de *DateEdit) Format() string {
	return de.format
}

func (de *DateEdit) SetFormat(format string) error {
	lp := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(format)))

	if 0 == de.SendMessage(win.DTM_SETFORMAT, 0, lp) {
		return newError("DTM_SETFORMAT failed")
	}

	de.format = format

	return nil
}

func (de *DateEdit) Range() (min, max time.Time) {
	var st [2]win.SYSTEMTIME

	ret := de.SendMessage(win.DTM_GETRANGE, 0, uintptr(unsafe.Pointer(&st[0])))

	if ret&win.GDTR_MIN > 0 {
		min = de.systemTimeToTime(&st[0])
	}

	if ret&win.GDTR_MAX > 0 {
		max = de.systemTimeToTime(&st[1])
	}

	return
}

func (de *DateEdit) SetRange(min, max time.Time) error {
	if !min.IsZero() && !max.IsZero() {
		if min.Year() > max.Year() ||
			min.Year() == max.Year() && min.Month() > max.Month() ||
			min.Year() == max.Year() && min.Month() == max.Month() && min.Day() > max.Day() {
			return newError("invalid range")
		}
	}

	var st [2]win.SYSTEMTIME
	var wParam uintptr

	if !min.IsZero() {
		wParam |= win.GDTR_MIN
		st[0] = *de.timeToSystemTime(min)
	}

	if !max.IsZero() {
		wParam |= win.GDTR_MAX
		st[1] = *de.timeToSystemTime(max)
	}

	if 0 == de.SendMessage(win.DTM_SETRANGE, wParam, uintptr(unsafe.Pointer(&st[0]))) {
		return newError("SendMessage(DTM_SETRANGE)")
	}

	return nil
}

func (de *DateEdit) Date() time.Time {
	st, err := de.systemTime()
	if err != nil || st == nil {
		return time.Time{}
	}

	return de.systemTimeToTime(st)
}

func (de *DateEdit) SetDate(date time.Time) error {
	stNew := de.timeToSystemTime(date)
	stOld, err := de.systemTime()
	if err != nil {
		return err
	} else if stNew == stOld || stNew != nil && stOld != nil && *stNew == *stOld {
		return nil
	}

	if err := de.setSystemTime(stNew); err != nil {
		return err
	}

	de.dateChangedPublisher.Publish()

	return nil
}

func (de *DateEdit) DateChanged() *Event {
	return de.dateChangedPublisher.Event()
}

func (de *DateEdit) WndProc(hwnd win.HWND, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case win.WM_NOTIFY:
		switch uint32(((*win.NMHDR)(unsafe.Pointer(lParam))).Code) {
		case win.DTN_DATETIMECHANGE:
			de.dateChangedPublisher.Publish()
		}
	}

	return de.WidgetBase.WndProc(hwnd, msg, wParam, lParam)
}

func (*DateEdit) NeedsWmSize() bool {
	return true
}

func (de *DateEdit) CreateLayoutItem(ctx *LayoutContext) LayoutItem {
	return &dateEditLayoutItem{
		idealSize: de.dialogBaseUnitsToPixels(Size{80, 12}),
	}
}

type dateEditLayoutItem struct {
	LayoutItemBase
	idealSize Size // in native pixels
}

func (*dateEditLayoutItem) LayoutFlags() LayoutFlags {
	return GrowableHorz
}

func (li *dateEditLayoutItem) IdealSize() Size {
	return li.idealSize
}

func (li *dateEditLayoutItem) MinSize() Size {
	return li.idealSize
}