package main

import (
	"errors"
	"fmt"
	"image"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"time"

	"github.com/godbus/dbus/v5"
)

const (
	portalBusName       = "org.freedesktop.portal.Desktop"
	portalObjectPath    = dbus.ObjectPath("/org/freedesktop/portal/desktop")
	screenCastInterface = "org.freedesktop.portal.ScreenCast"
	requestInterface    = "org.freedesktop.portal.Request"
	sessionInterface    = "org.freedesktop.portal.Session"
)

type portalStream struct {
	NodeID     uint32
	Properties map[string]dbus.Variant
}

func openPortalScreenCastSession() (*screenCaptureSession, error) {
	if _, err := exec.LookPath("gst-launch-1.0"); err != nil {
		return nil, errors.New("chybí gst-launch-1.0 pro přenos GNOME ScreenCast")
	}

	conn, err := dbus.ConnectSessionBus()
	if err != nil {
		return nil, fmt.Errorf("připojení k desktop portálu: %w", err)
	}
	closeConnection := true
	defer func() {
		if closeConnection {
			conn.Close()
		}
	}()

	portal := conn.Object(portalBusName, portalObjectPath)
	token := fmt.Sprintf("fckeuspy%d", time.Now().UnixNano())
	createResult, err := portalRequest(conn, portal, screenCastInterface+".CreateSession", map[string]dbus.Variant{
		"handle_token":         dbus.MakeVariant(token + "create"),
		"session_handle_token": dbus.MakeVariant(token + "session"),
	})
	if err != nil {
		return nil, fmt.Errorf("vytvoření ScreenCast relace: %w", err)
	}
	sessionPath, err := portalObjectPathResult(createResult, "session_handle")
	if err != nil {
		return nil, err
	}
	sessionOpen := true
	defer func() {
		if sessionOpen {
			_ = portal.Call(sessionInterface+".Close", 0, sessionPath).Err
		}
	}()

	if _, err := portalRequest(conn, portal, screenCastInterface+".SelectSources", sessionPath, map[string]dbus.Variant{
		"handle_token": dbus.MakeVariant(token + "select"),
		"types":        dbus.MakeVariant(uint32(3)), // monitor or window
		"multiple":     dbus.MakeVariant(false),
		"cursor_mode":  dbus.MakeVariant(uint32(1)), // hidden
	}); err != nil {
		return nil, fmt.Errorf("výběr okna nebo monitoru: %w", err)
	}

	startResult, err := portalRequest(conn, portal, screenCastInterface+".Start", sessionPath, "", map[string]dbus.Variant{
		"handle_token": dbus.MakeVariant(token + "start"),
	})
	if err != nil {
		return nil, fmt.Errorf("spuštění ScreenCast relace: %w", err)
	}
	streams, err := portalStreams(startResult)
	if err != nil {
		return nil, err
	}
	if len(streams) != 1 {
		return nil, fmt.Errorf("ScreenCast vrátil %d streamů, očekáván je jeden", len(streams))
	}

	var remote dbus.UnixFD
	if err := portal.Call(screenCastInterface+".OpenPipeWireRemote", 0, sessionPath, map[string]dbus.Variant{}).Store(&remote); err != nil {
		return nil, fmt.Errorf("otevření PipeWire streamu: %w", err)
	}
	frames, err := openPipeWireFrameStream(remote, streams[0].NodeID)
	if err != nil {
		return nil, err
	}
	closeConnection = false
	sessionOpen = false
	return &screenCaptureSession{
		capture: frames.Capture,
		close: func() {
			frames.Close()
			_ = portal.Call(sessionInterface+".Close", 0, sessionPath).Err
			conn.Close()
		},
	}, nil
}

func portalRequest(conn *dbus.Conn, portal dbus.BusObject, method string, args ...any) (map[string]dbus.Variant, error) {
	signals := make(chan *dbus.Signal, 1)
	conn.Signal(signals)
	defer conn.RemoveSignal(signals)
	if err := conn.AddMatchSignal(
		dbus.WithMatchInterface(requestInterface),
		dbus.WithMatchMember("Response"),
	); err != nil {
		return nil, err
	}
	defer conn.RemoveMatchSignal(
		dbus.WithMatchInterface(requestInterface),
		dbus.WithMatchMember("Response"),
	)

	var requestPath dbus.ObjectPath
	if err := portal.Call(method, 0, args...).Store(&requestPath); err != nil {
		return nil, err
	}

	timeout := time.NewTimer(2 * time.Minute)
	defer timeout.Stop()
	select {
	case <-timeout.C:
		return nil, errors.New("desktop portál neodpověděl včas")
	default:
	}

	for {
		select {
		case signal := <-signals:
			if signal.Path != requestPath {
				continue
			}
			if len(signal.Body) != 2 {
				return nil, errors.New("neplatná odpověď desktop portálu")
			}
			response, ok := signal.Body[0].(uint32)
			if !ok {
				return nil, errors.New("neplatný stav odpovědi desktop portálu")
			}
			if response != 0 {
				return nil, errors.New("výběr byl zrušen")
			}
			result, ok := signal.Body[1].(map[string]dbus.Variant)
			if !ok {
				return nil, errors.New("neplatná data odpovědi desktop portálu")
			}
			return result, nil
		case <-timeout.C:
			return nil, errors.New("desktop portál neodpověděl včas")
		}
	}
}

func portalObjectPathResult(result map[string]dbus.Variant, key string) (dbus.ObjectPath, error) {
	value, ok := result[key]
	if !ok {
		return "", fmt.Errorf("desktop portál nevrátil %s", key)
	}
	var path dbus.ObjectPath
	if err := value.Store(&path); err != nil {
		return "", fmt.Errorf("neplatné %s: %w", key, err)
	}
	return path, nil
}

func portalStreams(result map[string]dbus.Variant) ([]portalStream, error) {
	value, ok := result["streams"]
	if !ok {
		return nil, errors.New("desktop portál nevrátil žádný stream")
	}
	var streams []portalStream
	if err := value.Store(&streams); err != nil {
		return nil, fmt.Errorf("neplatná data streamu desktop portálu: %w", err)
	}
	return streams, nil
}

type pipeWireFrameStream struct {
	dir     string
	command *exec.Cmd
}

func openPipeWireFrameStream(remote dbus.UnixFD, nodeID uint32) (*pipeWireFrameStream, error) {
	dir, err := os.MkdirTemp("", "fckeuspy-pipewire-*")
	if err != nil {
		return nil, err
	}
	remoteFile := os.NewFile(uintptr(remote), "pipewire-remote")
	command := exec.Command(
		"gst-launch-1.0", "-q",
		"pipewiresrc", "fd=3", fmt.Sprintf("path=%d", nodeID),
		"!", "videoconvert",
		"!", "videorate",
		"!", "video/x-raw,framerate=5/1",
		"!", "pngenc", "compression-level=1",
		"!", "multifilesink", "location="+filepath.Join(dir, "frame-%05d.png"), "max-files=2",
	)
	command.ExtraFiles = []*os.File{remoteFile}
	if err := command.Start(); err != nil {
		remoteFile.Close()
		os.Remove(dir)
		return nil, fmt.Errorf("spuštění PipeWire čtečky: %w", err)
	}
	remoteFile.Close()
	return &pipeWireFrameStream{dir: dir, command: command}, nil
}

func (s *pipeWireFrameStream) Capture() (image.Image, error) {
	entries, err := os.ReadDir(s.dir)
	if err != nil {
		return nil, err
	}
	if len(entries) == 0 {
		return nil, errors.New("PipeWire zatím neposkytl žádný snímek")
	}
	sort.Slice(entries, func(i, j int) bool {
		left, leftErr := entries[i].Info()
		right, rightErr := entries[j].Info()
		if leftErr != nil || rightErr != nil {
			return entries[i].Name() > entries[j].Name()
		}
		return left.ModTime().After(right.ModTime())
	})
	var lastErr error
	for _, entry := range entries {
		img, decodeErr := decodeImageFile(filepath.Join(s.dir, entry.Name()))
		if decodeErr == nil {
			return img, nil
		}
		lastErr = decodeErr
	}
	if lastErr != nil {
		return nil, lastErr
	}
	return nil, errors.New("PipeWire zatím neposkytl žádný snímek")
}

func (s *pipeWireFrameStream) Close() {
	if s.command.Process != nil {
		_ = s.command.Process.Kill()
	}
	_, _ = s.command.Process.Wait()
	entries, err := os.ReadDir(s.dir)
	if err == nil {
		for _, entry := range entries {
			_ = os.Remove(filepath.Join(s.dir, entry.Name()))
		}
	}
	_ = os.Remove(s.dir)
}
