/*
 * Copyright (C) 2017 ~ 2018 Deepin Technology Co., Ltd.
 *
 * Author:     jouyouyun <jouyouwen717@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package dock

import (
	"bytes"
	"encoding/base64"
	"errors"
	"image"
	"image/png"
	"math"
	"strings"
	"time"

	"github.com/nfnt/resize"

	x "github.com/linuxdeepin/go-x11-client"
	"github.com/linuxdeepin/go-x11-client/util/wm/ewmh"
	"github.com/linuxdeepin/go-x11-client/util/wm/icccm"
)

func atomsContains(slice []x.Atom, atom x.Atom) bool {
	for _, a := range slice {
		if a == atom {
			return true
		}
	}
	return false
}

func getWmClass(win x.Window) (*icccm.WMClass, error) {
	wmClass, err := icccm.GetWMClass(globalXConn, win).Reply(globalXConn)
	if err != nil {
		return nil, err
	}
	return &wmClass, nil
}

func getAtomName(atom x.Atom) (string, error) {
	return globalXConn.GetAtomName(atom)
}

func getAtom(name string) (x.Atom, error) {
	return globalXConn.GetAtom(name)
}

func maximizeWindow(win x.Window) error {
	return ewmh.RequestChangeWMState(globalXConn, win, ewmh.WMStateAdd, atomNetWmStateMaximizedVert, atomNetWmStateMaximizedHorz, 2).Check(globalXConn)
}

func minimizeWindow(win x.Window) error {
	logger.Debug("minimizeWindow", win)
	return icccm.RequestChangeWMState(globalXConn, win, icccm.StateIconic).Check(globalXConn)
}

func makeWindowAbove(win x.Window) error {
	return ewmh.RequestChangeWMState(globalXConn, win, ewmh.WMStateAdd, atomNetWmStateAbove,
		0, 2).Check(globalXConn)
}

func moveWindow(win x.Window) error {
	// TODO:
	//return ewmh.WmMoveresize(xu, win, ewmh.MoveKeyboard)
	//ewmh.RequestWMMoveResize(win, )
	return nil
}

func closeWindow(win x.Window, ts x.Timestamp) error {
	return ewmh.RequestCloseWindow(globalXConn, win, ts, 2).Check(globalXConn)
}

type windowFrameExtents struct {
	Left, Right, Top, Bottom uint
}

func getWindowFrameExtents(xConn *x.Conn, win x.Window) (*windowFrameExtents, error) {
	reply, err := x.GetProperty(xConn, false, win, atomNetFrameExtents, x.AtomCardinal,
		0, 4).Reply(xConn)
	if err != nil || reply.Format == 0 {
		// try _GTK_FRAME_EXTENTS
		reply, err = x.GetProperty(xConn, false, win, atomGtkFrameExtents, x.AtomCardinal,
			0, 4).Reply(xConn)
		if err != nil {
			return nil, err
		}
	}

	return getFrameExtentsFromReply(reply)
}

func getCardinalsFromReply(r *x.GetPropertyReply) ([]uint32, error) {
	if r.Format != 32 {
		return nil, errors.New("bad reply")
	}
	count := len(r.Value) / 4
	ret := make([]uint32, count)
	rdr := x.NewReaderFromData(r.Value)
	for i := 0; i < count; i++ {
		ret[i] = uint32(rdr.Read4b())
	}
	return ret, nil
}

func getFrameExtentsFromReply(reply *x.GetPropertyReply) (*windowFrameExtents, error) {
	list, err := getCardinalsFromReply(reply)
	if err != nil {
		return nil, err
	}

	if len(list) != 4 {
		return nil, errors.New("length of list is not 4")
	}
	return &windowFrameExtents{
		Left:   uint(list[0]),
		Right:  uint(list[1]),
		Top:    uint(list[2]),
		Bottom: uint(list[3]),
	}, nil
}

func getMotifWmHints(xConn *x.Conn, win x.Window) (*MotifWmHints, error) {
	reply, err := x.GetProperty(xConn, false, win, atomMotifWmHints,
		atomMotifWmHints, 0, 5).Reply(xConn)
	if err != nil {
		return nil, err
	}
	return getMotifWmHintsFromReply(reply)
}

func getMotifWmHintsFromReply(reply *x.GetPropertyReply) (*MotifWmHints, error) {
	list, err := getCardinalsFromReply(reply)
	if err != nil {
		return nil, err
	}
	if len(list) != 5 {
		return nil, errors.New("length of list is not 5")
	}
	return &MotifWmHints{
		Flags:       list[0],
		Functions:   list[1],
		Decorations: list[2],
		InputMode:   int32(list[3]),
		Status:      list[4],
	}, nil
}

type MotifWmHints struct {
	Flags       uint32
	Functions   uint32
	Decorations uint32
	InputMode   int32
	Status      uint32
}

const (
	MotifHintFunctions = 1 << iota
	MotifHintDecorations
	MotifHintInputMode
	MotifHintStatus
)

const (
	MotifFunctionAll = 1 << iota
	MotifFunctionResize
	MotifFunctionMove
	MotifFunctionMinimize
	MotifFunctionMaximize
	MotifFunctionClose
	MotifFunctionNone = 0
)

func (h *MotifWmHints) allowedClose() bool {
	// 允许关闭的条件：
	// 1. 不设置 Functions 字段，即 h.Flags 没有设置 MotifHintFunctions 标志位；
	// 2. 或者设置了 Functions 字段并且 h.Functions 设置了 MotifFunctionAll 标志位；
	// 3. 或者设置了 Functions 字段并且 h.Functions 设置了 MotifFunctionClose 标志位。
	// 相关定义在 motif-2.3.8/lib/Xm/MwmUtil.h 。
	return h.Flags&MotifHintFunctions == 0 ||
		h.Functions&MotifFunctionAll != 0 ||
		h.Functions&MotifFunctionClose != 0
}

func getWindowGeometry(xConn *x.Conn, win x.Window) (*Rect, error) {
	rect, err := getGeometry(xConn, win)
	if err != nil {
		return nil, err
	}

	root := xConn.GetDefaultScreen().Root
	coord, err := x.TranslateCoordinates(xConn, win, root, 0, 0).Reply(xConn)
	if err != nil {
		return nil, err
	}
	rect.X = int32(coord.DstX)
	rect.Y = int32(coord.DstY)

	dWin, err := getDecorativeWindow(xConn, win)
	if err != nil {
		return nil, err
	}

	dRect, err := getGeometry(xConn, dWin)
	if err != nil {
		return nil, err
	}

	if rect.X == dRect.X && rect.Y == dRect.Y {
		// 无标题栏的窗口，比如 deepin-editor, dconf-editor
		frameExtents, _ := getWindowFrameExtents(xConn, win)
		if frameExtents != nil {
			X := rect.X + int32(frameExtents.Left)
			y := rect.Y + int32(frameExtents.Top)
			w := rect.Width - uint32(frameExtents.Left+frameExtents.Right)
			h := rect.Height - uint32(frameExtents.Top+frameExtents.Bottom)
			return &Rect{X, y, w, h}, nil
		}
		return rect, nil
	}
	// else 普通的有标题栏的窗口，比如 xev, 返回装饰窗口的位置和大小。
	return dRect, nil
}

func getDecorativeWindow(conn *x.Conn, win x.Window) (x.Window, error) {
	count := 0
	for {
		count++
		reply, err := x.QueryTree(conn, win).Reply(conn)
		if err != nil {
			return 0, err
		}

		if reply.Root == reply.Parent {
			return win, nil
		}
		if count > 10 {
			return 0, errors.New("getDecorateWindow: exceeded the loop iteration limit")
		}
		win = reply.Parent
	}
}

func getGeometry(xConn *x.Conn, win x.Window) (*Rect, error) {
	geo, err := x.GetGeometry(xConn, x.Drawable(win)).Reply(xConn)
	if err != nil {
		return nil, err
	}
	return &Rect{
		X:      int32(geo.X),
		Y:      int32(geo.Y),
		Width:  uint32(geo.Width),
		Height: uint32(geo.Height),
	}, nil
}

func getWmName(win x.Window) string {
	// get _NET_WM_NAME
	name, err := ewmh.GetWMName(globalXConn, win).Reply(globalXConn)
	if err != nil || name == "" {
		// get WM_NAME
		nameTp, _ := icccm.GetWMName(globalXConn, win).Reply(globalXConn)
		name, _ = nameTp.GetStr()
	}

	return strings.Replace(name, "\x00", "", -1)
}

func getWmPid(win x.Window) uint {
	pid, _ := ewmh.GetWMPid(globalXConn, win).Reply(globalXConn)
	return uint(pid)
}

// WM_CLIENT_LEADER
func getWmClientLeader(win x.Window) (x.Window, error) {
	reply, err := x.GetProperty(globalXConn, false, win, atomWmClientLeader, atomUTF8String,
		0, 1).Reply(globalXConn)
	if err != nil {
		return 0, err
	}
	leader, err := getWindowFromReply(reply)
	if err != nil {
		return 0, err
	}
	return leader, nil
}

// WM_TRANSIENT_FOR
func getWmTransientFor(win x.Window) (x.Window, error) {
	return icccm.GetWMTransientFor(globalXConn, win).Reply(globalXConn)
}

// _NET_WM_WINDOW_OPACITY
func getWmWindowOpacity(win x.Window) (uint, error) {
	reply, err := x.GetProperty(globalXConn, false, win, atomNetWmWindowOpacity, atomUTF8String,
		0, 1).Reply(globalXConn)
	if err != nil {
		return 0, err
	}

	opacity, err := getCardinalFromReply(reply)
	if err != nil {
		return 0, err
	}

	return uint(opacity), nil
}

func getWmCommand(win x.Window) ([]string, error) {
	reply, err := x.GetProperty(globalXConn, false, win, atomWmCommand, atomUTF8String,
		0, lengthMax).Reply(globalXConn)
	if err != nil {
		return nil, err
	}
	return getUTF8StrsFromReply(reply)
}

func getWindowGtkApplicationId(win x.Window) string {
	gtkAppId, _ := getWindowPropertyString(win, atomGtkApplicationId)
	return gtkAppId
}

func getWindowFlatpakAppID(win x.Window) string {
	id, _ := getWindowPropertyString(win, atomFlatpakAppId)
	return id
}

func getAndroidUengineId(win x.Window) int32 {
	atom := atomAndroidUengineId

	reply, err := x.GetProperty(globalXConn, false, win,
		atom, atomInteger, 0, lengthMax).Reply(globalXConn)

	if err != nil || reply.ValueLen == 0 {
		return -1
	}

	return int32(x.Get32(reply.Value))
}

func getAndroidUengineName(win x.Window) string {
	reply, err := x.GetProperty(globalXConn, false, win,
		atomAndroidUengineName, atomString, 0, lengthMax).Reply(globalXConn)
	if err != nil {
		return ""
	}

	name := string(reply.Value)
	return name
}

const lengthMax = 0xffff

func getWmWindowRole(win x.Window) string {
	role, _ := getWindowPropertyString(win, atomWmWindowRole)
	return role
}

func getWindowPropertyString(win x.Window, atom x.Atom) (string, error) {
	reply, err := x.GetProperty(globalXConn, false, win, atom, atomUTF8String,
		0, lengthMax).Reply(globalXConn)
	if err != nil {
		return "", err
	}
	return getUTF8StrFromReply(reply)
}

func getCardinalFromReply(r *x.GetPropertyReply) (uint32, error) {
	if r.Format != 32 || len(r.Value) != 4 {
		return 0, errors.New("bad reply")
	}
	return uint32(x.Get32(r.Value)), nil
}

func getWindowFromReply(r *x.GetPropertyReply) (x.Window, error) {
	if r.Format != 32 || len(r.Value) != 4 {
		return 0, errors.New("bad reply")
	}
	return x.Window(x.Get32(r.Value)), nil
}

func getUTF8StrFromReply(reply *x.GetPropertyReply) (string, error) {
	if reply.Format != 8 {
		return "", errors.New("bad reply")
	}

	return string(reply.Value), nil
}

func getUTF8StrsFromReply(reply *x.GetPropertyReply) ([]string, error) {
	if reply.Format != 8 {
		return nil, errors.New("bad reply")
	}

	data := reply.Value
	var strs []string
	sstart := 0
	for i, c := range data {
		if c == 0 {
			strs = append(strs, string(data[sstart:i]))
			sstart = i + 1
		}
	}
	if sstart < len(data) {
		strs = append(strs, string(data[sstart:]))
	}
	return strs, nil
}

const bestIconSize = 48

func getIconFromWindow(win x.Window) string {
	img, err := findIconEwmh(win)
	if err != nil {
		logger.Warning(err)
		// try icccm
		img, err = findIconIcccm(win)
		if err != nil {
			logger.Warning(err)
			// get icon failed
			return ""
		}
	}

	img = resize.Thumbnail(bestIconSize, bestIconSize, img, resize.NearestNeighbor)

	// encode image to png, then to base64 string
	var buf bytes.Buffer
	if err := png.Encode(&buf, img); err != nil {
		logger.Warning(err)
		return ""
	}
	return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
}

func findIconEwmh(win x.Window) (image.Image, error) {
	icon, err := getBestEwmhIcon(win)
	if err != nil {
		return nil, err
	}
	return NewNRGBAImageFromEwmhIcon(icon), nil
}

// findIconIcccm helps FindIcon by trying to return an icccm-style icon.
func findIconIcccm(wid x.Window) (image.Image, error) {
	// TODO
	return nil, errors.New("todo")
	//hints, err := icccm.WmHintsGet(xu, wid)
	//if err != nil {
	//	return nil, err
	//}
	//
	//// Only continue if the WM_HINTS flags say an icon is specified and
	//// if at least one of icon pixmap or icon mask is non-zero.
	//if hints.Flags&icccm.HintIconPixmap == 0 ||
	//	(hints.IconPixmap == 0 && hints.IconMask == 0) {
	//
	//	return nil, errors.New("No icon found in WM_HINTS.")
	//}
	//
	//return xgraphics.NewIcccmIcon(xu, hints.IconPixmap, hints.IconMask)
}

func getBestEwmhIcon(win x.Window) (*ewmh.WMIcon, error) {
	icons, err := ewmh.GetWMIcon(globalXConn, win).Reply(globalXConn)
	if err != nil {
		return nil, err
	}

	best := findBestEwmhIcon(bestIconSize, bestIconSize, icons)
	if best == nil {
		return nil, errors.New("ewmh icon not found")
	}
	return best, nil
}

// findBestEwmhIcon takes width/height dimensions and a slice of *ewmh.WmIcon
// and finds the best matching icon of the bunch. We always prefer bigger.
// If no icons are bigger than the preferred dimensions, use the biggest
// available. Otherwise, use the smallest icon that is greater than or equal
// to the preferred dimensions. The preferred dimensions is essentially
// what you'll likely scale the resulting icon to.
// If width and height are 0, then the largest icon found will be returned.
func findBestEwmhIcon(width, height int, icons []ewmh.WMIcon) *ewmh.WMIcon {
	// nada nada limonada
	if len(icons) == 0 {
		return nil
	}

	parea := width * height // preferred size
	best := -1

	// If zero area, set it to the largest possible.
	if parea == 0 {
		parea = math.MaxInt32
	}

	var bestArea, iconArea int

	for i, icon := range icons {
		// the first valid icon we've seen; use it!
		if best == -1 {
			best = i
			continue
		}

		// load areas for comparison
		bestArea = int(icons[best].Width * icons[best].Height)
		iconArea = int(icon.Width * icon.Height)

		// We don't always want to accept bigger icons if our best is
		// already bigger. But we always want something bigger if our best
		// is insufficient.
		if (iconArea >= parea && iconArea <= bestArea) ||
			(bestArea < parea && iconArea > bestArea) {
			best = i
		}
	}

	if best > -1 {
		return &icons[best]
	}
	return nil
}

func NewNRGBAImageFromEwmhIcon(icon *ewmh.WMIcon) *image.NRGBA {
	img := image.NewNRGBA(image.Rect(0, 0, int(icon.Width), int(icon.Height)))
	// icon.Data []uint8 BGRA
	// img.Pix []uint8 RGBA

	for i := 0; i < len(icon.Data); i += 4 {
		b := icon.Data[i]
		g := icon.Data[i+1]
		r := icon.Data[i+2]
		a := icon.Data[i+3]

		img.Pix[i] = r
		img.Pix[i+1] = g
		img.Pix[i+2] = b
		img.Pix[i+3] = a
	}
	return img
}

func getWindowUserTime(win x.Window) (uint, error) {
	timestamp, err := ewmh.GetWMUserTime(globalXConn, win).Reply(globalXConn)
	if err != nil {
		userTimeWindow, err := ewmh.GetWMUserTimeWindow(globalXConn, win).Reply(globalXConn)
		if err != nil {
			return 0, err
		}

		timestamp, err = ewmh.GetWMUserTime(globalXConn, userTimeWindow).Reply(globalXConn)
		if err != nil {
			return 0, err
		}
	}
	return uint(timestamp), nil
}

func changeCurrentWorkspaceToWindowWorkspace(win x.Window) error {
	winWorkspace, err := ewmh.GetWMDesktop(globalXConn, win).Reply(globalXConn)
	if err != nil {
		return err
	}

	currentWorkspace, err := ewmh.GetCurrentDesktop(globalXConn).Reply(globalXConn)
	if err != nil {
		return err
	}

	if currentWorkspace == winWorkspace {
		logger.Debugf("No need to change workspace, the current desktop is already %v", currentWorkspace)
		return nil
	}
	logger.Debug("Change workspace")

	winUserTime, err := getWindowUserTime(win)
	logger.Debug("window user time:", winUserTime)
	if err != nil {
		// only warning not return
		logger.Warning("getWindowUserTime failed:", err)
	}
	_ = ewmh.RequestChangeCurrentDesktop(globalXConn, winWorkspace,
		x.Timestamp(winUserTime)).Check(globalXConn)

	return nil
}

func activateWindow(win x.Window) error {
	logger.Debug("activateWindow", win)
	err := changeCurrentWorkspaceToWindowWorkspace(win)
	if err != nil {
		return err
	}
	err = ewmh.RequestChangeActiveWindow(globalXConn, win,
		2, 0, 0).Check(globalXConn)
	if err != nil {
		return err
	}

	time.AfterFunc(50*time.Millisecond, func() {
		err := ewmh.RequestRestackWindow(globalXConn, win, 0,
			x.StackModeAbove).Check(globalXConn)
		if err != nil {
			logger.Warning(err)
		}
	})

	return nil
}

func isHiddenPre(win x.Window) bool {
	state, _ := ewmh.GetWMState(globalXConn, win).Reply(globalXConn)
	return atomsContains(state, atomNetWmStateHidden)
}

// works for new deepin wm.
func isWindowOnCurrentWorkspace(win x.Window) (bool, error) {
	winWorkspace, err := ewmh.GetWMDesktop(globalXConn, win).Reply(globalXConn)
	if err != nil {
		return false, err
	}

	currentWorkspace, err := ewmh.GetCurrentDesktop(globalXConn).Reply(globalXConn)
	if err != nil {
		return false, err
	}

	return winWorkspace == currentWorkspace, nil
}

func onCurrentWorkspacePre(win x.Window) bool {
	isOnCurrentWorkspace, err := isWindowOnCurrentWorkspace(win)
	if err != nil {
		logger.Warning(err)
		// 也许是窗口跳过窗口管理器了，如 dde-control-center
		return true
	}
	return isOnCurrentWorkspace
}

func isGoodWindow(win x.Window) bool {
	_, err := x.GetGeometry(globalXConn, x.Drawable(win)).Reply(globalXConn)
	return err == nil
}

func killClient(win x.Window) error {
	return x.KillClientChecked(globalXConn, uint32(win)).Check(globalXConn)
}
