// SPDX-FileCopyrightText: 2018 - 2022 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

package audio

import (
	"fmt"
	"strconv"
	"strings"
	"sync"

	dbus "github.com/godbus/dbus"
	"github.com/linuxdeepin/go-lib/dbusutil"
	"github.com/linuxdeepin/go-lib/procfs"
	"github.com/linuxdeepin/go-lib/pulse"
)

const (
	PropAppIconName      = "application.icon_name"      // 应用程序图标名称属性
	PropAppName          = "application.name"           // 应用程序名称属性
	PropAppProcessID     = "application.process.id"     // 应用程序进程ID属性
	PropAppProcessBinary = "application.process.binary" // 应用程序进程二进制文件属性
)

// SinkInput 音频输入流结构体
// 代表一个正在播放音频的应用程序流
type SinkInput struct {
	audio             *Audio // 音频管理实例
	service           *dbusutil.Service // DBus服务
	PropsMu           sync.RWMutex // 属性读写锁
	index             uint32 // 输入流索引
	correctIconCalled bool // 是否已调用图标修正
	correctedIcon     string // 修正后的图标
	visible           bool // 是否在UI中可见
	cVolume           pulse.CVolume // 脉冲音频音量对象
	channelMap        pulse.ChannelMap // 声道映射
	// Name process name
	Name           string // 应用程序名称
	Icon           string // 应用程序图标
	Mute           bool // 是否静音
	Volume         float64 // 当前音量
	Balance        float64 // 左右声道平衡值
	SupportBalance bool // 是否支持左右声道调整
	Fade           float64 // 前后声道平衡值
	SupportFade    bool // 是否支持前后声道调整
	SinkIndex      uint32 // 输出设备索引
}

// newSinkInput 创建新的音频输入流实例
// sinkInputInfo: 脉冲音频输入流信息
// audio: 音频管理实例
func newSinkInput(sinkInputInfo *pulse.SinkInput, audio *Audio) *SinkInput {
	if sinkInputInfo == nil {
		return nil
	}
	sinkInput := &SinkInput{
		audio:   audio,
		service: audio.service,
		index:   sinkInputInfo.Index,
		visible: getSinkInputVisible(sinkInputInfo),
	}
	sinkInput.update(sinkInputInfo)
	return sinkInput
}

// getPropSinkIndex 获取输出设备索引属性
// 返回当前输出设备索引
func (s *SinkInput) getPropSinkIndex() uint32 {
	s.PropsMu.RLock()
	v := s.SinkIndex
	s.PropsMu.RUnlock()
	return v
}

// getSinkInputVisible 判断输入流是否在UI中可见
// sinkInputInfo: 脉冲音频输入流信息
// 根据应用程序名称和媒体角色判断是否显示
func getSinkInputVisible(sinkInputInfo *pulse.SinkInput) bool {
	appName := sinkInputInfo.PropList[pulse.PA_PROP_APPLICATION_NAME]
	switch appName {
	case "com.deepin.SoundEffect", "deepin-notifications":
		return false
	}

	switch sinkInputInfo.PropList[pulse.PA_PROP_MEDIA_ROLE] {
	case "video", "music", "game":
		return true
	case "animation", "production", "phone":
		//TODO: what's the meaning of this type? Should we filter this SinkInput?
		return true
	case "event", "a11y", "test", "filter":
		return false
	default:
		return true
	}
}

// SetVolume 设置输入流音量
// value: 音量值 (0.0 - 1.0)
// isPlay: 是否播放声音反馈
func (s *SinkInput) SetVolume(value float64, isPlay bool) *dbus.Error {
	logger.Infof("dbus call SetVolume with value %f and isPlay %t, the sink input name is %s",
		value, isPlay, s.Name)

	if !isVolumeValid(value) {
		err := fmt.Errorf("invalid volume value: %v", value)
		logger.Warning(err)
		return dbusutil.ToError(err)
	}

	if value == 0 {
		value = 0.001
		s.SetMute(true)
	} else if s.Mute {
		s.SetMute(false)
	}
	s.PropsMu.RLock()
	cv := s.cVolume.SetAvg(value)
	s.PropsMu.RUnlock()
	s.audio.context().SetSinkInputVolume(s.index, cv)

	if isPlay {
		playFeedback()
	}
	return nil
}

// SetBalance 设置输入流左右声道平衡
// value: 声道平衡值 (-1.0 到 1.0)
// isPlay: 是否播放声音反馈
func (s *SinkInput) SetBalance(value float64, isPlay bool) *dbus.Error {
	logger.Infof("dbus call SetBalance with value %f and isPlay %t, the sink input name is %s",
		value, isPlay, s.Name)

	if value < -1.00 || value > 1.00 {
		err := fmt.Errorf("invalid volume value: %v", value)
		logger.Warning(err)
		return dbusutil.ToError(err)
	}

	s.PropsMu.RLock()
	cv := s.cVolume.SetBalance(s.channelMap, value)
	s.PropsMu.RUnlock()
	s.audio.context().SetSinkInputVolume(s.index, cv)

	if isPlay {
		playFeedback()
	}
	return nil
}

// SetFade 设置输入流前后声道平衡
// value: 声道平衡值 (-1.0 到 1.0)
func (s *SinkInput) SetFade(value float64) *dbus.Error {
	logger.Infof("dbus call SetFade with value %f, the sink input name is %s", value, s.Name)

	if value < -1.00 || value > 1.00 {
		err := fmt.Errorf("invalid volume value: %v", value)
		logger.Warning(err)
		return dbusutil.ToError(err)
	}

	s.PropsMu.RLock()
	cv := s.cVolume.SetFade(s.channelMap, value)
	s.PropsMu.RUnlock()
	s.audio.context().SetSinkInputVolume(s.index, cv)

	playFeedback()
	return nil
}

// SetMute 设置输入流静音状态
// value: 是否静音
func (s *SinkInput) SetMute(value bool) *dbus.Error {
	logger.Infof("dbus call SetMute with value %t, the sink input name is %s", value, s.Name)

	s.audio.context().SetSinkInputMute(s.index, value)
	if !value {
		playFeedback()
	}
	return nil
}

// getPath 获取DBus对象路径
// 返回输入流的DBus对象路径
func (s *SinkInput) getPath() dbus.ObjectPath {
	return dbus.ObjectPath(dbusPath + "/SinkInput" + strconv.Itoa(int(s.index)))
}

// GetInterfaceName 获取DBus接口名称
// 返回输入流的DBus接口名称
func (*SinkInput) GetInterfaceName() string {
	return dbusInterface + ".SinkInput"
}

// getProcessParentCmdline 获取进程父进程的命令行参数
// pidStr: 进程ID字符串
// 返回父进程的命令行参数列表
func getProcessParentCmdline(pidStr string) ([]string, error) {
	pid, err := strconv.ParseInt(pidStr, 10, 32)
	if err != nil {
		return nil, err
	}

	p := procfs.Process(pid)
	status, err := p.Status()
	if err != nil {
		return nil, err
	}
	ppid, err := status.PPid()
	if err != nil {
		return nil, err
	}
	pp := procfs.Process(ppid)
	return pp.Cmdline()
}

// isProcessParentFirefox 判断进程父进程是否为Firefox
// pidStr: 进程ID字符串
// 返回是否为Firefox父进程
func isProcessParentFirefox(pidStr string) (bool, error) {
	cmdline, err := getProcessParentCmdline(pidStr)
	if err != nil {
		return false, err
	}
	if len(cmdline) > 0 && strings.Contains(cmdline[0], "firefox") {
		return true, nil
	}
	return false, nil
}

// isProcessParentSMPlayer 判断进程父进程是否为SMPlayer
// pidStr: 进程ID字符串
// 返回是否为SMPlayer父进程
func isProcessParentSMPlayer(pidStr string) (bool, error) {
	cmdline, err := getProcessParentCmdline(pidStr)
	if err != nil {
		return false, err
	}
	if len(cmdline) > 0 && strings.Contains(cmdline[0], "smplayer") {
		return true, nil
	}
	return false, nil
}

// correct icon
func (s *SinkInput) correctIcon(sinkInputInfo *pulse.SinkInput) (string, error) {
	if s.correctIconCalled {
		return s.correctedIcon, nil
	}
	s.correctIconCalled = true

	processBin := sinkInputInfo.PropList[PropAppProcessBinary]
	appPid := sinkInputInfo.PropList[PropAppProcessID]
	appName := sinkInputInfo.PropList[PropAppName]

	var icon string
	switch processBin {
	case "firefox":
		icon = "firefox"
	case "plugin-container":
		// may be flash player embed in firefox
		is, err := isProcessParentFirefox(appPid)
		if err != nil {
			logger.Warning(err)
			break
		}
		if is {
			icon = "firefox"
		}
	case "mpv":
		if appName == "SMPlayer" {
			icon = "smplayer"
		}

	case "mplayer":
		is, err := isProcessParentSMPlayer(appPid)
		if err != nil {
			logger.Warning(err)
			break
		}
		if is {
			icon = "smplayer"
		}

	case "wine-preloader":
		if appName == "foobar2000.exe" {
			icon = "apps.org.foobar2000"
		}

	case "python2.7":
		if appName == "foobnix" {
			icon = "foobnix"
		}
	case "python3.5":
		if appName == "com.github.geigi.cozy" {
			icon = "com.github.geigi.cozy"
		}

	case "cocomusic":
		icon = "cocomusic"
	case "Museeks":
		icon = "museeks"
	case "cumulonimbus":
		icon = "cumulonimbus"
	case "yarock":
		icon = "application-x-yarock"
	case "mixnode":
		icon = "mixnode"
	case "headset":
		icon = "headset"
	case "electron-xiami":
		icon = "electron_xiami"
	}

	s.correctedIcon = icon
	if icon != "" {
		logger.Debugf("correct icon of sink-input #%d to %q", sinkInputInfo.Index, icon)
	}
	return icon, nil
}

func (s *SinkInput) update(sinkInputInfo *pulse.SinkInput) {
	s.PropsMu.Lock()
	defer s.PropsMu.Unlock()

	if !s.visible {
		s.SinkIndex = sinkInputInfo.Sink
		return
	}

	s.cVolume = sinkInputInfo.Volume
	s.channelMap = sinkInputInfo.ChannelMap
	s.setPropSinkIndex(sinkInputInfo.Sink)
	name := sinkInputInfo.PropList[PropAppName]
	s.setPropName(name)
	icon := sinkInputInfo.PropList[PropAppIconName]
	correctedIcon, err := s.correctIcon(sinkInputInfo)
	if err != nil {
		logger.Warning(err)
	}
	if correctedIcon != "" {
		icon = correctedIcon
	}
	if icon == "" {
		// Using default media player icon
		icon = "media-player"
	}
	s.setPropIcon(icon)

	s.setPropVolume(sinkInputInfo.Volume.Avg())
	s.setPropMute(sinkInputInfo.Mute)

	s.setPropSupportFade(false)
	s.setPropFade(sinkInputInfo.Volume.Fade(sinkInputInfo.ChannelMap))
	s.setPropSupportBalance(true)
	s.setPropBalance(sinkInputInfo.Volume.Balance(sinkInputInfo.ChannelMap))
}
