// Copyright 2010 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 (
	"fmt"
	"path/filepath"
	"syscall"
	"unsafe"
)

import (
	"github.com/lxn/win"
)

type FileDialog struct {
	Title          string
	FilePath       string
	FilePaths      []string
	InitialDirPath string
	Filter         string
	FilterIndex    int
	Flags          uint32
	ShowReadOnlyCB bool
}

func (dlg *FileDialog) show(owner Form, fun func(ofn *win.OPENFILENAME) bool, flags uint32) (accepted bool, err error) {
	ofn := new(win.OPENFILENAME)

	ofn.LStructSize = uint32(unsafe.Sizeof(*ofn))
	if owner != nil {
		ofn.HwndOwner = owner.Handle()
	}

	filter := make([]uint16, len(dlg.Filter)+2)
	copy(filter, syscall.StringToUTF16(dlg.Filter))
	// Replace '|' with the expected '\0'.
	for i, c := range filter {
		if byte(c) == '|' {
			filter[i] = uint16(0)
		}
	}
	ofn.LpstrFilter = &filter[0]
	ofn.NFilterIndex = uint32(dlg.FilterIndex)

	ofn.LpstrInitialDir = syscall.StringToUTF16Ptr(dlg.InitialDirPath)
	ofn.LpstrTitle = syscall.StringToUTF16Ptr(dlg.Title)
	ofn.Flags = win.OFN_FILEMUSTEXIST | flags | dlg.Flags

	if !dlg.ShowReadOnlyCB {
		ofn.Flags |= win.OFN_HIDEREADONLY
	}

	var fileBuf []uint16
	if flags&win.OFN_ALLOWMULTISELECT > 0 {
		fileBuf = make([]uint16, 65536)
	} else {
		fileBuf = make([]uint16, 1024)
		copy(fileBuf, syscall.StringToUTF16(dlg.FilePath))
	}
	ofn.LpstrFile = &fileBuf[0]
	ofn.NMaxFile = uint32(len(fileBuf))

	if !fun(ofn) {
		errno := win.CommDlgExtendedError()
		if errno != 0 {
			err = newError(fmt.Sprintf("Error %d", errno))
		}
		return
	}

	dlg.FilterIndex = int(ofn.NFilterIndex)

	if flags&win.OFN_ALLOWMULTISELECT > 0 {
		split := func() [][]uint16 {
			var parts [][]uint16

			from := 0
			for i, c := range fileBuf {
				if c == 0 {
					if i == from {
						return parts
					}

					parts = append(parts, fileBuf[from:i])
					from = i + 1
				}
			}

			return parts
		}

		parts := split()

		if len(parts) == 1 {
			dlg.FilePaths = []string{syscall.UTF16ToString(parts[0])}
		} else {
			dirPath := syscall.UTF16ToString(parts[0])
			dlg.FilePaths = make([]string, len(parts)-1)

			for i, fp := range parts[1:] {
				dlg.FilePaths[i] = filepath.Join(dirPath, syscall.UTF16ToString(fp))
			}
		}
	} else {
		dlg.FilePath = syscall.UTF16ToString(fileBuf)
	}

	accepted = true

	return
}

func (dlg *FileDialog) ShowOpen(owner Form) (accepted bool, err error) {
	return dlg.show(owner, win.GetOpenFileName, 0)
}

func (dlg *FileDialog) ShowOpenMultiple(owner Form) (accepted bool, err error) {
	return dlg.show(owner, win.GetOpenFileName, win.OFN_ALLOWMULTISELECT|win.OFN_EXPLORER)
}

func (dlg *FileDialog) ShowSave(owner Form) (accepted bool, err error) {
	return dlg.show(owner, win.GetSaveFileName, 0)
}

func pathFromPIDL(pidl uintptr) (string, error) {
	var path [win.MAX_PATH]uint16
	if !win.SHGetPathFromIDList(pidl, &path[0]) {
		return "", newError("SHGetPathFromIDList failed")
	}

	return syscall.UTF16ToString(path[:]), nil
}

// We use this callback to disable the OK button in case of "invalid" selections.
func browseFolderCallback(hwnd win.HWND, msg uint32, lp, wp uintptr) uintptr {
	const BFFM_SELCHANGED = 2
	if msg == BFFM_SELCHANGED {
		_, err := pathFromPIDL(lp)
		var enabled uintptr
		if err == nil {
			enabled = 1
		}

		const BFFM_ENABLEOK = win.WM_USER + 101

		win.SendMessage(hwnd, BFFM_ENABLEOK, 0, enabled)
	}

	return 0
}

var browseFolderCallbackPtr uintptr

func init() {
	AppendToWalkInit(func() {
		browseFolderCallbackPtr = syscall.NewCallback(browseFolderCallback)
	})
}

func (dlg *FileDialog) ShowBrowseFolder(owner Form) (accepted bool, err error) {
	// Calling OleInitialize (or similar) is required for BIF_NEWDIALOGSTYLE.
	if hr := win.OleInitialize(); hr != win.S_OK && hr != win.S_FALSE {
		return false, newError(fmt.Sprint("OleInitialize Error: ", hr))
	}
	defer win.OleUninitialize()

	var ownerHwnd win.HWND
	if owner != nil {
		ownerHwnd = owner.Handle()
	}

	// We need to put the initial path into a buffer of at least MAX_LENGTH
	// length, or we may get random crashes.
	var buf [win.MAX_PATH]uint16
	copy(buf[:], syscall.StringToUTF16(dlg.InitialDirPath))

	const BIF_NEWDIALOGSTYLE = 0x00000040

	bi := win.BROWSEINFO{
		HwndOwner: ownerHwnd,
		LpszTitle: syscall.StringToUTF16Ptr(dlg.Title),
		UlFlags:   BIF_NEWDIALOGSTYLE,
		Lpfn:      browseFolderCallbackPtr,
	}

	win.SHParseDisplayName(&buf[0], 0, &bi.PidlRoot, 0, nil)

	pidl := win.SHBrowseForFolder(&bi)
	if pidl == 0 {
		return false, nil
	}
	defer win.CoTaskMemFree(pidl)

	dlg.FilePath, err = pathFromPIDL(pidl)
	accepted = dlg.FilePath != ""
	return
}
