// Copyright 2019 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 (
	"sync"
	"unsafe"

	"github.com/lxn/win"
)

// The global window group manager instance.
var wgm windowGroupManager

// windowGroupManager manages window groups for each thread with one or
// more windows.
type windowGroupManager struct {
	mutex  sync.RWMutex
	groups map[uint32]*WindowGroup
}

// Group returns a window group for the given thread ID, if one exists.
// If a group does not already exist it returns nil.
func (m *windowGroupManager) Group(threadID uint32) *WindowGroup {
	m.mutex.RLock()
	defer m.mutex.RUnlock()
	if m.groups == nil {
		return nil
	}
	return m.groups[threadID]
}

// CreateGroup returns a window group for the given thread ID. If one does
// not already exist, it will be created.
//
// The group will have its counter incremented as a result of this call.
// It is the caller's responsibility to call Done when finished with the
// group.
func (m *windowGroupManager) CreateGroup(threadID uint32) *WindowGroup {
	// Fast path with read lock
	m.mutex.RLock()
	if m.groups != nil {
		if group := m.groups[threadID]; group != nil {
			m.mutex.RUnlock()
			group.Add(1)
			return group
		}
	}
	m.mutex.RUnlock()

	// Slow path with write lock
	m.mutex.Lock()
	if m.groups == nil {
		m.groups = make(map[uint32]*WindowGroup)
	} else {
		if group := m.groups[threadID]; group != nil {
			// Another caller raced with our lock and beat us
			m.mutex.Unlock()
			group.Add(1)
			return group
		}
	}

	group := newWindowGroup(threadID, m.removeGroup)
	group.Add(1)
	m.groups[threadID] = group
	m.mutex.Unlock()

	return group
}

// removeGroup is called by window groups to remove themselves from
// the manager.
func (m *windowGroupManager) removeGroup(threadID uint32) {
	m.mutex.Lock()
	delete(m.groups, threadID)
	m.mutex.Unlock()
}

// WindowGroup holds data common to windows that share a thread.
//
// Each WindowGroup keeps track of the number of references to
// the group. When the number of references reaches zero, the
// group is disposed of.
type WindowGroup struct {
	refs            int // Tracks the number of windows that rely on this group
	ignored         int // Tracks the number of refs created by the group itself
	threadID        uint32
	completion      func(uint32) // Used to tell the window group manager to remove this group
	removed         bool         // Has this group been removed from its manager? (used for race detection)
	toolTip         *ToolTip
	activeForm      Form
	oleInit         bool
	accPropServices *win.IAccPropServices

	syncMutex           sync.Mutex
	syncFuncs           []func()                   // Functions queued to run on the group's thread
	layoutResultsByForm map[Form]*formLayoutResult // Layout computations queued for application on the group's thread
}

// newWindowGroup returns a new window group for the given thread ID.
//
// The completion function will be called when the group is disposed of.
func newWindowGroup(threadID uint32, completion func(uint32)) *WindowGroup {
	hr := win.OleInitialize()

	return &WindowGroup{
		threadID:            threadID,
		completion:          completion,
		oleInit:             hr == win.S_OK || hr == win.S_FALSE,
		layoutResultsByForm: make(map[Form]*formLayoutResult),
	}
}

// ThreadID identifies the thread that the group is affiliated with.
func (g *WindowGroup) ThreadID() uint32 {
	return g.threadID
}

// Refs returns the current number of references to the group.
func (g *WindowGroup) Refs() int {
	return g.refs
}

// AccessibilityServices returns an instance of CLSID_AccPropServices class.
func (g *WindowGroup) accessibilityServices() *win.IAccPropServices {
	if g.accPropServices != nil {
		return g.accPropServices
	}

	var accPropServices *win.IAccPropServices
	hr := win.CoCreateInstance(&win.CLSID_AccPropServices, nil, win.CLSCTX_ALL, &win.IID_IAccPropServices, (*unsafe.Pointer)(unsafe.Pointer(&accPropServices)))
	if win.FAILED(hr) {
		return nil
	}

	g.accPropServices = accPropServices
	return accPropServices
}

// accPropIds is a static list of accessibility properties user (may) set for a window
// and we should clear when the window is disposed.
var accPropIds = []win.MSAAPROPID{
	win.PROPID_ACC_DEFAULTACTION,
	win.PROPID_ACC_DESCRIPTION,
	win.PROPID_ACC_HELP,
	win.PROPID_ACC_KEYBOARDSHORTCUT,
	win.PROPID_ACC_NAME,
	win.PROPID_ACC_ROLE,
	win.PROPID_ACC_ROLEMAP,
	win.PROPID_ACC_STATE,
	win.PROPID_ACC_STATEMAP,
	win.PROPID_ACC_VALUEMAP,
}

// accClearHwndProps clears all window properties for Dynamic Annotation to release resources.
func (g *WindowGroup) accClearHwndProps(hwnd win.HWND) {
	if g.accPropServices != nil {
		g.accPropServices.ClearHwndProps(hwnd, win.OBJID_CLIENT, win.CHILDID_SELF, accPropIds)
	}
}

// Add changes the group's reference counter by delta, which may be negative.
//
// If the reference counter becomes zero the group will be disposed of.
//
// If the reference counter goes negative Add will panic.
func (g *WindowGroup) Add(delta int) {
	if g.removed {
		panic("walk: add() called on a WindowGroup that has been removed from its manager")
	}

	g.refs += delta
	if g.refs < 0 {
		panic("walk: negative WindowGroup refs counter")
	}
	if g.refs-g.ignored == 0 {
		g.dispose()
	}
}

// Done decrements the group's reference counter by one.
func (g *WindowGroup) Done() {
	g.Add(-1)
}

// Synchronize adds f to the group's function queue, to be executed
// by the message loop running on the the group's thread.
//
// Synchronize can be called from any thread.
func (g *WindowGroup) Synchronize(f func()) {
	g.syncMutex.Lock()
	defer g.syncMutex.Unlock()
	g.syncFuncs = append(g.syncFuncs, f)
}

// synchronizeLayout causes the given layout computations to be applied
// later by the message loop running on the group's thread.
//
// Any previously queued layout computations for the affected form that
// have not yet been applied will be replaced.
//
// synchronizeLayout can be called from any thread.
func (g *WindowGroup) synchronizeLayout(result *formLayoutResult) {
	g.syncMutex.Lock()
	g.layoutResultsByForm[result.form] = result
	g.syncMutex.Unlock()
}

// RunSynchronized runs all of the function calls queued by Synchronize
// and applies any layout changes queued by synchronizeLayout.
//
// RunSynchronized must be called by the group's thread.
func (g *WindowGroup) RunSynchronized() {
	// Clear the list of callbacks first to avoid deadlock
	// if a callback itself calls Synchronize()...
	g.syncMutex.Lock()
	funcs := g.syncFuncs
	var results []*formLayoutResult
	for _, result := range g.layoutResultsByForm {
		results = append(results, result)
		delete(g.layoutResultsByForm, result.form)
	}
	g.syncFuncs = nil
	g.syncMutex.Unlock()

	for _, result := range results {
		applyLayoutResults(result.results, result.stopwatch)
	}
	for _, f := range funcs {
		f()
	}
}

// ToolTip returns the tool tip control for the group, if one exists.
func (g *WindowGroup) ToolTip() *ToolTip {
	return g.toolTip
}

// CreateToolTip returns a tool tip control for the group.
//
// If a control has not already been prepared for the group one will be
// created.
func (g *WindowGroup) CreateToolTip() (*ToolTip, error) {
	if g.toolTip != nil {
		return g.toolTip, nil
	}

	tt, err := NewToolTip() // This must not call group.ToolTip()
	if err != nil {
		return nil, err
	}
	g.toolTip = tt

	// At this point the ToolTip has already added a reference for itself
	// to the group as part of the ToolTip's InitWindow process. We don't
	// want it to count toward the group's liveness, however, because it
	// would keep the group from cleaning up after itself.
	//
	// To solve this problem we also keep track of the number of
	// references that each group should ignore. The ignored references
	// are subtracted from the total number of references when evaluating
	// liveness. The expectation is that ignored references will be
	// removed as part of the group's disposal process.
	g.ignore(1)

	return tt, nil
}

// ActiveForm returns the currently active form for the group. If no
// form is active it returns nil.
func (g *WindowGroup) ActiveForm() Form {
	return g.activeForm
}

// SetActiveForm updates the currently active form for the group.
func (g *WindowGroup) SetActiveForm(form Form) {
	g.activeForm = form
}

// ignore changes the number of references that the group will ignore.
//
// ignore is used internally by WindowGroup to keep track of the number
// of references created by the group itself. When finished with a group,
// call Done() instead.
func (g *WindowGroup) ignore(delta int) {
	if g.removed {
		panic("walk: ignore() called on a WindowGroup that has been removed from its manager")
	}

	g.ignored += delta
	if g.ignored < 0 {
		panic("walk: negative WindowGroup ignored counter")
	}
	if g.refs-g.ignored == 0 {
		g.dispose()
	}
}

// dispose releases any resources consumed by the group.
func (g *WindowGroup) dispose() {
	if g.accPropServices != nil {
		g.accPropServices.Release()
		g.accPropServices = nil
	}

	if g.oleInit {
		win.OleUninitialize()
		g.oleInit = false
	}

	if g.toolTip != nil {
		g.toolTip.Dispose()
		g.toolTip = nil
	}
	g.removed = true // race detection only
	g.completion(g.threadID)
}