// +build windows

package main

import (
	"fmt"
	"regexp"
	"strings"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
)

// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
	pid  int
	ppid int
	exe  string
}

// getImagePid returns the
func getImagePid(imageName string) ([]int, error) {
	processes, err := processes()
	if err != nil {
		return nil, err
	}
	var pids []int
	for i := 0; i < len(processes); i++ {
		if strings.ToLower(processes[i].exe) == imageName {
			pids = append(pids, processes[i].pid)
		}
	}
	return pids, nil
}

// getWindowTitle returns the title of a window linked to a process name
func getWindowTitle(imageName, windowTitleRegex string) (string, error) {
	processPid, err := getImagePid(imageName)
	if err != nil {
		return "", nil
	}

	// is a spotify process running?
	// no: returns an empty string
	if len(processPid) == 0 {
		return "", nil
	}

	// returns the first window of the first pid
	_, windowTitle, err := GetWindowTitle(processPid[0], windowTitleRegex)
	if err != nil {
		return "", nil
	}
	return windowTitle, nil
}

func newWindowsProcess(e *windows.ProcessEntry32) *WindowsProcess {
	// Find when the string ends for decoding
	end := 0
	for {
		if e.ExeFile[end] == 0 {
			break
		}
		end++
	}

	return &WindowsProcess{
		pid:  int(e.ProcessID),
		ppid: int(e.ParentProcessID),
		exe:  syscall.UTF16ToString(e.ExeFile[:end]),
	}
}

// Processes returns a snapshot of all the processes
// Taken and adapted from https://github.com/mitchellh/go-ps
func processes() ([]WindowsProcess, error) {
	// get process table snapshot
	handle, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
	if err != nil {
		return nil, syscall.GetLastError()
	}
	defer func() {
		_ = windows.CloseHandle(handle)
	}()

	// get process infor by looping through the snapshot
	var entry windows.ProcessEntry32
	entry.Size = uint32(unsafe.Sizeof(entry))
	err = windows.Process32First(handle, &entry)
	if err != nil {
		return nil, fmt.Errorf("error retrieving process info")
	}

	results := make([]WindowsProcess, 0, 50)
	for {
		results = append(results, *newWindowsProcess(&entry))
		err := windows.Process32Next(handle, &entry)
		if err != nil {
			if err == syscall.ERROR_NO_MORE_FILES {
				break
			}
			return nil, fmt.Errorf("Fail to syscall Process32Next: %v", err)
		}
	}

	return results, nil
}

// win32 specific code

// win32 dll load and function definitions
var (
	user32                       = syscall.NewLazyDLL("user32.dll")
	procEnumWindows              = user32.NewProc("EnumWindows")
	procGetWindowTextW           = user32.NewProc("GetWindowTextW")
	procGetWindowThreadProcessID = user32.NewProc("GetWindowThreadProcessId")
)

// EnumWindows call EnumWindows from user32 and returns all active windows
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-enumwindows
func EnumWindows(enumFunc, lparam uintptr) (err error) {
	r1, _, e1 := syscall.Syscall(procEnumWindows.Addr(), 2, enumFunc, lparam, 0)
	if r1 == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

// GetWindowText returns the title and text of a window from a window handle
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
func GetWindowText(hwnd syscall.Handle, str *uint16, maxCount int32) (length int32, err error) {
	r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hwnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
	length = int32(r0)
	if length == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

// GetWindowTitle searches for a window attached to the pid
func GetWindowTitle(pid int, windowTitleRegex string) (syscall.Handle, string, error) {
	var hwnd syscall.Handle
	var title string
	compiledRegex, err := regexp.Compile(windowTitleRegex)
	if err != nil {
		return 0, "", fmt.Errorf("Error while compiling the regex '%s'", windowTitleRegex)
	}
	// callback fro EnumWindows
	cb := syscall.NewCallback(func(h syscall.Handle, p uintptr) uintptr {
		var prcsID int = 0
		// get pid
		_, _, _ = procGetWindowThreadProcessID.Call(uintptr(h), uintptr(unsafe.Pointer(&prcsID)))
		// check if pid matches spotify pid
		if prcsID == pid {
			b := make([]uint16, 200)
			_, err := GetWindowText(h, &b[0], int32(len(b)))
			if err != nil {
				// ignore the error
				return 1 // continue enumeration
			}
			title = syscall.UTF16ToString(b)
			if compiledRegex.MatchString(title) {
				// will cause EnumWindows to return 0 (error)
				// but we don't want to enumerate all windows since we got what we want
				hwnd = h
				return 0
			}
		}

		return 1 // continue enumeration
	})
	// Enumerates all top-level windows on the screen
	// The error is not checked because if EnumWindows is stopped bofere enumerating all windows
	// it returns 0(error occurred) instead of 1(success)
	// In our case, title will equal "" or the title of the window anyway
	_ = EnumWindows(cb, 0)
	return hwnd, title, nil
}
