package main

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"log"
	"os/exec"
	"regexp"
	"strings"
)

const (
	// TODO: drop these in favor of strings.Fields()
	nonSpaceToken = "[^\t\n\f\r ]+" // equivalent to perl's \S
	spaceToken    = "[\t\n\f\r ]+"  // equivalent to perl's \s
)

// packageProvides returns a map from package name to a string slice of
// provided packages, for all of the specified packages (or all of the
// installed ones, if none are specified).
func packageProvides(ignoreMissing bool, pkgSet map[string]bool) map[string]map[string]bool {
	var pkgs []string
	for p := range pkgSet {
		pkgs = append(pkgs, p)
	}

	lines, err := runCommand(append([]string{
		"dpkg-query",
		"-Wf", "${binary:Package} ${Package};${Status};${Provides}\n",
		"--"},
		pkgs...))
	if err != nil {
		log.Fatal(err)
	}

	// sample input:
	// libparted-fs-resize0:amd64 libparted-fs-resize0;install ok installed;
	// xserver-xorg-core xserver-xorg-core;deinstall ok config-files;xorg-input-abi-24, xorg-video-abi-25, xserver-xorg-video-modesetting
	// ^(\S+).*;.*\s(\S+);(.*)$
	pkgStatusProvides := regexp.MustCompile("^(" + nonSpaceToken + ").*;.*" + spaceToken + "(" + nonSpaceToken + ");(.*)$")

	pkgProvides := make(map[string]map[string]bool)
	for _, line := range lines {
		m := pkgStatusProvides.FindStringSubmatch(line)
		if len(m) != 4 {
			log.Fatalf("Unexpected output from dpkg-query: %q\n", line)
			continue
		}
		pkg := string(m[1])
		status := string(m[2])
		providedPkgs := strings.Split(strings.Replace(string(m[3]), " ", "", -1), ",")

		if status == "installed" {
			provides := make(map[string]bool)
			for _, pr := range providedPkgs {
				provides[pr] = true
			}
			pkgProvides[pkg] = provides
		} else if len(pkgs) > 0 {
			fmt.Printf("Skipping %s because it's not installed", pkg)
		}

	}

	return pkgProvides
}

// packageFiles returns a map from deb (binary) package name to string slice of
// package files, and the total number of files.
func packageFiles(pkgs []string) (map[string][]string, int64, error) {
	lines, err := runCommand(append([]string{"dpkg", "-L"}, pkgs...))
	if err != nil {
		log.Fatal(err)
	}

	pkg2files := make(map[string][]string)
	i := 0
	var totalFiles int64
	diverted := regexp.MustCompile("^(?:locally diverted|diverted by " + nonSpaceToken + ") to: (/.+)$")
	for _, line := range lines {
		if line == "" {
			i++
			continue
		}
		pkg := pkgs[i]
		file := line

		if m := diverted.FindStringSubmatch(line); len(m) == 2 {
			file = string(m[1])
		}
		if len(file) == 0 || file[0] != '/' {
			continue
		}

		pkg2files[pkg] = append(pkg2files[pkg], file)
		totalFiles += 1
	}

	return pkg2files, totalFiles, nil
}

func runCommand(args []string) ([]string, error) {
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = "/" // to not be affected if the current directory is removed
	out, err := cmd.CombinedOutput()
	lines := strings.Split(string(out), "\n")
	if err != exec.ErrDot && err != nil {
		return lines, fmt.Errorf("%q failed: %w", strings.Join(args, " "), err)
	}

	lines, err = readLines(bytes.NewReader(out))
	if err != nil {
		return lines, fmt.Errorf("failed to read the output of %q: %w", args, err)
	}

	return lines, nil
}

func readLines(r io.Reader) ([]string, error) {
	var lines []string
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}
	return lines, nil
}
