// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2014-2016 Canonical Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as
 * published by the Free Software Foundation.
 *
 * 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 wrappers

import (
	"bufio"
	"bytes"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/logger"
	"github.com/snapcore/snapd/osutil"
	"github.com/snapcore/snapd/snap"
)

// valid simple prefixes
var validDesktopFilePrefixes = []string{
	// headers
	"[Desktop Entry]",
	"[Desktop Action ",
	// https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s05.html
	"Type=",
	"Version=",
	"Name=",
	"GenericName=",
	"NoDisplay=",
	"Comment=",
	"Icon=",
	"Hidden=",
	"OnlyShowIn=",
	"NotShowIn=",
	"Exec=",
	// Note that we do not support TryExec, it does not make sense
	// in the snap context
	"Terminal=",
	"Actions=",
	"MimeType=",
	"Categories=",
	"Keywords=",
	"StartupNotify=",
	"StartupWMClass=",
}

// name desktop file keys are localized as key[LOCALE]=:
//   lang_COUNTRY@MODIFIER
//   lang_COUNTRY
//   lang@MODIFIER
//   lang
var validLocalizedDesktopFilePrefixes = []string{
	"Name",
	"GenericName",
	"Comment",
	"Keywords",
}

func isValidDesktopFilePrefix(line string) bool {
	for _, prefix := range validDesktopFilePrefixes {
		if strings.HasPrefix(line, prefix) {
			return true
		}
	}

	return false
}

func trimLang(s string) string {
	const langChars = "@_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"

	if s == "" || s[0] != '[' {
		return s
	}
	t := strings.TrimLeft(s[1:], langChars)
	if t != "" && t[0] == ']' {
		return t[1:]
	}
	return s
}

func isValidLocalizedDesktopFilePrefix(line string) bool {
	for _, prefix := range validLocalizedDesktopFilePrefixes {
		s := strings.TrimPrefix(line, prefix)
		if s == line {
			continue
		}
		if strings.HasPrefix(trimLang(s), "=") {
			return true
		}
	}
	return false
}

// rewriteExecLine rewrites a "Exec=" line to use the wrapper path for snap application.
func rewriteExecLine(s *snap.Info, desktopFile, line string) (string, error) {
	cmd := strings.SplitN(line, "=", 2)[1]
	for _, app := range s.Apps {
		env := fmt.Sprintf("env BAMF_DESKTOP_FILE_HINT=%s ", desktopFile)
		wrapper := app.WrapperPath()
		validCmd := filepath.Base(wrapper)
		// check the prefix to allow %flag style args
		// this is ok because desktop files are not run through sh
		// so we don't have to worry about the arguments too much
		if cmd == validCmd {
			return "Exec=" + env + wrapper, nil
		} else if strings.HasPrefix(cmd, validCmd+" ") {
			return fmt.Sprintf("Exec=%s%s%s", env, wrapper, line[len("Exec=")+len(validCmd):]), nil
		}
	}

	return "", fmt.Errorf("invalid exec command: %q", cmd)
}

func sanitizeDesktopFile(s *snap.Info, desktopFile string, rawcontent []byte) []byte {
	newContent := []string{}

	scanner := bufio.NewScanner(bytes.NewReader(rawcontent))
	for scanner.Scan() {
		line := scanner.Text()

		// whitespace/comments are just copied
		if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
			newContent = append(newContent, line)
			continue
		}

		// ignore everything we have not whitelisted
		if !isValidDesktopFilePrefix(line) && !isValidLocalizedDesktopFilePrefix(line) {
			continue
		}
		// rewrite exec lines to an absolute path for the binary
		if strings.HasPrefix(line, "Exec=") {
			var err error
			line, err = rewriteExecLine(s, desktopFile, line)
			if err != nil {
				// something went wrong, ignore the line
				continue
			}
		}

		// do variable substitution
		line = strings.Replace(line, "${SNAP}", s.MountDir(), -1)
		newContent = append(newContent, line)
	}

	return []byte(strings.Join(newContent, "\n"))
}

func updateDesktopDatabase(desktopFiles []string) error {
	if len(desktopFiles) == 0 {
		return nil
	}

	if _, err := exec.LookPath("update-desktop-database"); err == nil {
		if output, err := exec.Command("update-desktop-database", dirs.SnapDesktopFilesDir).CombinedOutput(); err != nil {
			return fmt.Errorf("cannot update-desktop-database %q: %s", output, err)
		}
		logger.Debugf("update-desktop-database successful")
	}
	return nil
}

// AddSnapDesktopFiles puts in place the desktop files for the applications from the snap.
func AddSnapDesktopFiles(s *snap.Info) error {
	if err := os.MkdirAll(dirs.SnapDesktopFilesDir, 0755); err != nil {
		return err
	}

	baseDir := s.MountDir()

	desktopFiles, err := filepath.Glob(filepath.Join(baseDir, "meta", "gui", "*.desktop"))
	if err != nil {
		return fmt.Errorf("cannot get desktop files for %v: %s", baseDir, err)
	}

	for _, df := range desktopFiles {
		content, err := ioutil.ReadFile(df)
		if err != nil {
			return err
		}

		installedDesktopFileName := filepath.Join(dirs.SnapDesktopFilesDir, fmt.Sprintf("%s_%s", s.Name(), filepath.Base(df)))
		content = sanitizeDesktopFile(s, installedDesktopFileName, content)
		if err := osutil.AtomicWriteFile(installedDesktopFileName, []byte(content), 0755, 0); err != nil {
			return err
		}
	}

	// updates mime info etc
	if err := updateDesktopDatabase(desktopFiles); err != nil {
		return err
	}

	return nil
}

// RemoveSnapDesktopFiles removes the added desktop files for the applications in the snap.
func RemoveSnapDesktopFiles(s *snap.Info) error {
	glob := filepath.Join(dirs.SnapDesktopFilesDir, s.Name()+"_*.desktop")
	activeDesktopFiles, err := filepath.Glob(glob)
	if err != nil {
		return fmt.Errorf("cannot get desktop files for %v: %s", glob, err)
	}
	for _, f := range activeDesktopFiles {
		os.Remove(f)
	}

	// updates mime info etc
	if err := updateDesktopDatabase(activeDesktopFiles); err != nil {
		return err
	}

	return nil
}
