// 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 (
	"image"
	"path/filepath"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"

	"github.com/lxn/win"
)

// Icon is a bitmap that supports transparency and combining multiple
// variants of an image in different resolutions.
type Icon struct {
	filePath  string
	index     int
	res       *uint16
	dpi2hIcon map[int]win.HICON
	size96dpi Size
	isStock   bool
	hasIndex  bool
}

type ExtractableIcon interface {
	FilePath_() string
	Index_() int
	Size_() int
}

func IconFrom(src interface{}, dpi int) (*Icon, error) {
	if src == nil {
		return nil, nil
	}

	img, err := ImageFrom(src)
	if err != nil {
		return nil, err
	}

	return iconCache.Icon(img, dpi)
}

func IconApplication() *Icon {
	return stockIcon(win.IDI_APPLICATION)
}

func IconError() *Icon {
	return stockIcon(win.IDI_ERROR)
}

func IconQuestion() *Icon {
	return stockIcon(win.IDI_QUESTION)
}

func IconWarning() *Icon {
	return stockIcon(win.IDI_WARNING)
}

func IconInformation() *Icon {
	return stockIcon(win.IDI_INFORMATION)
}

func IconWinLogo() *Icon {
	return stockIcon(win.IDI_WINLOGO)
}

func IconShield() *Icon {
	return stockIcon(win.IDI_SHIELD)
}

func stockIcon(id uintptr) *Icon {
	return &Icon{res: win.MAKEINTRESOURCE(id), size96dpi: defaultIconSize(), isStock: true}
}

// NewIconFromFile returns a new Icon, using the specified icon image file and default size.
func NewIconFromFile(filePath string) (*Icon, error) {
	return NewIconFromFileWithSize(filePath, Size{})
}

// NewIconFromFileWithSize returns a new Icon, using the specified icon image file and size.
func NewIconFromFileWithSize(filePath string, size Size) (*Icon, error) {
	if size.Width == 0 || size.Height == 0 {
		size = defaultIconSize()
	}

	return checkNewIcon(&Icon{filePath: filePath, size96dpi: size})
}

// NewIconFromResource returns a new Icon of default size, using the specified icon resource.
func NewIconFromResource(name string) (*Icon, error) {
	return NewIconFromResourceWithSize(name, Size{})
}

// NewIconFromResourceWithSize returns a new Icon of size size, using the specified icon resource.
func NewIconFromResourceWithSize(name string, size Size) (*Icon, error) {
	return newIconFromResource(syscall.StringToUTF16Ptr(name), size)
}

// NewIconFromResourceId returns a new Icon of default size, using the specified icon resource.
func NewIconFromResourceId(id int) (*Icon, error) {
	return NewIconFromResourceIdWithSize(id, Size{})
}

// NewIconFromResourceIdWithSize returns a new Icon of size size, using the specified icon resource.
func NewIconFromResourceIdWithSize(id int, size Size) (*Icon, error) {
	return newIconFromResource(win.MAKEINTRESOURCE(uintptr(id)), size)
}

func newIconFromResource(res *uint16, size Size) (*Icon, error) {
	if size.Width == 0 || size.Height == 0 {
		size = defaultIconSize()
	}

	return checkNewIcon(&Icon{res: res, size96dpi: size})
}

// NewIconFromSysDLL returns a new Icon, as identified by index of
// size 16x16 from the system DLL identified by dllBaseName.
func NewIconFromSysDLL(dllBaseName string, index int) (*Icon, error) {
	return NewIconFromSysDLLWithSize(dllBaseName, index, 16)
}

// NewIconFromSysDLLWithSize returns a new Icon, as identified by
// index of the desired size from the system DLL identified by dllBaseName.
func NewIconFromSysDLLWithSize(dllBaseName string, index, size int) (*Icon, error) {
	system32, err := windows.GetSystemDirectory()
	if err != nil {
		return nil, err
	}

	return checkNewIcon(&Icon{filePath: filepath.Join(system32, dllBaseName+".dll"), index: index, hasIndex: true, size96dpi: Size{size, size}})
}

// NewIconExtractedFromFile returns a new Icon, as identified by index of size 16x16 from filePath.
func NewIconExtractedFromFile(filePath string, index, _ int) (*Icon, error) {
	return checkNewIcon(&Icon{filePath: filePath, index: index, hasIndex: true, size96dpi: Size{16, 16}})
}

// NewIconExtractedFromFileWithSize returns a new Icon, as identified by index of the desired size from filePath.
func NewIconExtractedFromFileWithSize(filePath string, index, size int) (*Icon, error) {
	return checkNewIcon(&Icon{filePath: filePath, index: index, hasIndex: true, size96dpi: Size{size, size}})
}

// NewIconFromImage returns a new Icon at 96dpi, using the specified image.Image as source.
//
// Deprecated: Newer applications should use NewIconFromImageForDPI.
func NewIconFromImage(im image.Image) (ic *Icon, err error) {
	return NewIconFromImageForDPI(im, 96)
}

// NewIconFromImageForDPI returns a new Icon at given DPI, using the specified image.Image as source.
func NewIconFromImageForDPI(im image.Image, dpi int) (ic *Icon, err error) {
	hIcon, err := createAlphaCursorOrIconFromImage(im, image.Pt(0, 0), true)
	if err != nil {
		return nil, err
	}
	b := im.Bounds()
	return newIconFromHICONAndSize(hIcon, SizeTo96DPI(Size{b.Dx(), b.Dy()}, dpi), dpi), nil
}

// NewIconFromImageWithSize returns a new Icon of the given size in native pixels, using the
// specified Image as source.
func NewIconFromImageWithSize(image Image, size Size) (*Icon, error) {
	bmp, err := NewBitmapFromImageWithSize(image, size)
	if err != nil {
		return nil, err
	}

	return NewIconFromBitmap(bmp)
}

func newIconFromImageForDPI(image Image, dpi int) (*Icon, error) {
	size96dpi := image.Size()
	size := SizeFrom96DPI(size96dpi, dpi)

	bmp, err := NewBitmapFromImageWithSize(image, size)
	if err != nil {
		return nil, err
	}

	hIcon, err := createAlphaCursorOrIconFromBitmap(bmp, Point{}, true)
	if err != nil {
		return nil, err
	}

	return &Icon{dpi2hIcon: map[int]win.HICON{dpi: hIcon}, size96dpi: size96dpi}, nil
}

// NewIconFromBitmap returns a new Icon, using the specified Bitmap as source.
func NewIconFromBitmap(bmp *Bitmap) (ic *Icon, err error) {
	hIcon, err := createAlphaCursorOrIconFromBitmap(bmp, Point{}, true)
	if err != nil {
		return nil, err
	}
	return newIconFromHICONAndSize(hIcon, bmp.Size(), bmp.dpi), nil
}

// NewIconFromHICON returns a new Icon at 96dpi, using the specified win.HICON as source.
//
// Deprecated: Newer applications should use NewIconFromHICONForDPI.
func NewIconFromHICON(hIcon win.HICON) (ic *Icon, err error) {
	return NewIconFromHICONForDPI(hIcon, 96)
}

// NewIconFromHICONForDPI returns a new Icon at given DPI, using the specified win.HICON as source.
func NewIconFromHICONForDPI(hIcon win.HICON, dpi int) (ic *Icon, err error) {
	s, err := sizeFromHICON(hIcon)
	if err != nil {
		return nil, err
	}

	return newIconFromHICONAndSize(hIcon, SizeTo96DPI(s, dpi), dpi), nil
}

func newIconFromHICONAndSize(hIcon win.HICON, size Size, dpi int) *Icon {
	return &Icon{dpi2hIcon: map[int]win.HICON{dpi: hIcon}, size96dpi: size}
}

func checkNewIcon(icon *Icon) (*Icon, error) {
	if _, err := icon.handleForDPIWithError(96); err != nil {
		return nil, err
	}

	return icon, nil
}

func (i *Icon) handleForDPI(dpi int) win.HICON {
	hIcon, _ := i.handleForDPIWithError(dpi)
	return hIcon
}

func (i *Icon) handleForDPIWithError(dpi int) (win.HICON, error) {
	if i.dpi2hIcon == nil {
		i.dpi2hIcon = make(map[int]win.HICON)
	} else if handle, ok := i.dpi2hIcon[dpi]; ok {
		return handle, nil
	}

	var hInst win.HINSTANCE
	var name *uint16
	if i.filePath != "" {
		absFilePath, err := filepath.Abs(i.filePath)
		if err != nil {
			return 0, err
		}

		name = syscall.StringToUTF16Ptr(absFilePath)
	} else {
		if !i.isStock {
			if hInst = win.GetModuleHandle(nil); hInst == 0 {
				return 0, lastError("GetModuleHandle")
			}
		}

		name = i.res
	}

	var size Size
	if i.size96dpi.Width == 0 || i.size96dpi.Height == 0 {
		size = SizeFrom96DPI(defaultIconSize(), dpi)
	} else {
		size = SizeFrom96DPI(i.size96dpi, dpi)
	}

	var hIcon win.HICON

	if i.hasIndex {
		win.SHDefExtractIcon(
			name,
			int32(i.index),
			0,
			nil,
			&hIcon,
			win.MAKELONG(0, uint16(size.Width)))
		if hIcon == 0 {
			return 0, newError("SHDefExtractIcon")
		}
	} else {
		hr := win.HICON(win.LoadIconWithScaleDown(
			hInst,
			name,
			int32(size.Width),
			int32(size.Height),
			&hIcon))
		if hr < 0 || hIcon == 0 {
			return 0, lastError("LoadIconWithScaleDown")
		}
	}

	i.dpi2hIcon[dpi] = hIcon

	return hIcon, nil
}

// Dispose releases the operating system resources associated with the Icon.
func (i *Icon) Dispose() {
	if i.isStock || len(i.dpi2hIcon) == 0 {
		return
	}

	for dpi, hIcon := range i.dpi2hIcon {
		win.DestroyIcon(hIcon)
		delete(i.dpi2hIcon, dpi)
	}
}

func (i *Icon) draw(hdc win.HDC, location Point) error {
	dpi := dpiForHDC(hdc)
	size := SizeFrom96DPI(i.size96dpi, dpi)

	return i.drawStretched(hdc, Rectangle{location.X, location.Y, size.Width, size.Height})
}

func (i *Icon) drawStretched(hdc win.HDC, bounds Rectangle) error {
	dpi := int(float64(bounds.Width) / float64(i.size96dpi.Width) * 96.0)

	hIcon := i.handleForDPI(dpi)
	if hIcon == 0 {
		var dpiAvailMax int
		for dpiAvail, handle := range i.dpi2hIcon {
			if dpiAvail > dpiAvailMax {
				hIcon = handle
				dpiAvailMax = dpiAvail
			}
			if dpiAvail > dpi {
				break
			}
		}
	}

	if !win.DrawIconEx(hdc, int32(bounds.X), int32(bounds.Y), hIcon, int32(bounds.Width), int32(bounds.Height), 0, 0, win.DI_NORMAL) {
		return lastError("DrawIconEx")
	}

	return nil
}

// Size returns icon size in 1/96" units.
func (i *Icon) Size() Size {
	return i.size96dpi
}

// create an Alpha Icon or Cursor from an Image
// http://support.microsoft.com/kb/318876
func createAlphaCursorOrIconFromImage(im image.Image, hotspot image.Point, fIcon bool) (win.HICON, error) {
	bmp, err := NewBitmapFromImage(im)
	if err != nil {
		return 0, err
	}
	defer bmp.Dispose()

	return createAlphaCursorOrIconFromBitmap(bmp, Point{hotspot.X, hotspot.Y}, fIcon)
}

// createAlphaCursorOrIconFromBitmap creates a cursor/icon from a bitmap. hotspot coordinates are in native pixels.
func createAlphaCursorOrIconFromBitmap(bmp *Bitmap, hotspot Point, fIcon bool) (win.HICON, error) {
	// Create an empty mask bitmap.
	hMonoBitmap := win.CreateBitmap(int32(bmp.size.Width), int32(bmp.size.Height), 1, 1, nil)
	if hMonoBitmap == 0 {
		return 0, newError("CreateBitmap failed")
	}
	defer win.DeleteObject(win.HGDIOBJ(hMonoBitmap))

	var ii win.ICONINFO
	if fIcon {
		ii.FIcon = win.TRUE
	}
	ii.XHotspot = uint32(hotspot.X)
	ii.YHotspot = uint32(hotspot.Y)
	ii.HbmMask = hMonoBitmap
	ii.HbmColor = bmp.hBmp

	// Create the alpha cursor with the alpha DIB section.
	hIconOrCursor := win.CreateIconIndirect(&ii)

	return hIconOrCursor, nil
}

// sizeFromHICON returns icon size in native pixels.
func sizeFromHICON(hIcon win.HICON) (Size, error) {
	var ii win.ICONINFO
	var bi win.BITMAPINFO

	if !win.GetIconInfo(hIcon, &ii) {
		return Size{}, lastError("GetIconInfo")
	}
	defer win.DeleteObject(win.HGDIOBJ(ii.HbmMask))

	var hBmp win.HBITMAP
	if ii.HbmColor != 0 {
		hBmp = ii.HbmColor

		defer win.DeleteObject(win.HGDIOBJ(ii.HbmColor))
	} else {
		hBmp = ii.HbmMask
	}

	if 0 == win.GetObject(win.HGDIOBJ(hBmp), unsafe.Sizeof(bi), unsafe.Pointer(&bi)) {
		return Size{}, newError("GetObject")
	}

	return Size{int(bi.BmiHeader.BiWidth), int(bi.BmiHeader.BiHeight)}, nil
}

// defaultIconSize returns default small icon size in 1/92" units.
func defaultIconSize() Size {
	return Size{int(win.GetSystemMetricsForDpi(win.SM_CXSMICON, 96)), int(win.GetSystemMetricsForDpi(win.SM_CYSMICON, 96))}
}