// Copyright (c) 2018, Google Inc.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

// godeps prints out dependencies of a package in either CMake or Make depfile
// format, for incremental rebuilds.
//
// The depfile format is preferred. It works correctly when new files are added.
// However, CMake only supports depfiles for custom commands with Ninja and
// starting CMake 3.7. For other configurations, we also support CMake's format,
// but CMake must be rerun when file lists change.
package main

import (
	"flag"
	"fmt"
	"go/build"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

var (
	format  = flag.String("format", "cmake", "The format to output to, either 'cmake' or 'depfile'")
	mainPkg = flag.String("pkg", "", "The package to print dependencies for")
	target  = flag.String("target", "", "The name of the output file")
	out     = flag.String("out", "", "The path to write the output to. If unset, this is stdout")
)

func cMakeQuote(in string) string {
	// See https://cmake.org/cmake/help/v3.0/manual/cmake-language.7.html#quoted-argument
	var b strings.Builder
	b.Grow(len(in))
	// Iterate over in as bytes.
	for i := 0; i < len(in); i++ {
		switch c := in[i]; c {
		case '\\', '"':
			b.WriteByte('\\')
			b.WriteByte(c)
		case '\t':
			b.WriteString("\\t")
		case '\r':
			b.WriteString("\\r")
		case '\n':
			b.WriteString("\\n")
		default:
			b.WriteByte(in[i])
		}
	}
	return b.String()
}

func writeCMake(outFile *os.File, files []string) error {
	for i, file := range files {
		if i != 0 {
			if _, err := outFile.WriteString(";"); err != nil {
				return err
			}
		}
		if _, err := outFile.WriteString(cMakeQuote(file)); err != nil {
			return err
		}
	}
	return nil
}

func makeQuote(in string) string {
	// See https://www.gnu.org/software/make/manual/make.html#Rule-Syntax
	var b strings.Builder
	b.Grow(len(in))
	// Iterate over in as bytes.
	for i := 0; i < len(in); i++ {
		switch c := in[i]; c {
		case '$':
			b.WriteString("$$")
		case '#', '\\', ' ':
			b.WriteByte('\\')
			b.WriteByte(c)
		default:
			b.WriteByte(c)
		}
	}
	return b.String()
}

func writeDepfile(outFile *os.File, files []string) error {
	if _, err := fmt.Fprintf(outFile, "%s:", makeQuote(*target)); err != nil {
		return err
	}
	for _, file := range files {
		if _, err := fmt.Fprintf(outFile, " %s", makeQuote(file)); err != nil {
			return err
		}
	}
	_, err := outFile.WriteString("\n")
	return err
}

func appendPrefixed(list, newFiles []string, prefix string) []string {
	for _, file := range newFiles {
		list = append(list, filepath.Join(prefix, file))
	}
	return list
}

func main() {
	flag.Parse()

	if len(*mainPkg) == 0 {
		fmt.Fprintf(os.Stderr, "-pkg argument is required.\n")
		os.Exit(1)
	}

	var isDepfile bool
	switch *format {
	case "depfile":
		isDepfile = true
	case "cmake":
		isDepfile = false
	default:
		fmt.Fprintf(os.Stderr, "Unknown format: %q\n", *format)
		os.Exit(1)
	}

	if isDepfile && len(*target) == 0 {
		fmt.Fprintf(os.Stderr, "-target argument is required for depfile.\n")
		os.Exit(1)
	}

	done := make(map[string]struct{})
	var files []string
	var recurse func(pkgName string) error
	recurse = func(pkgName string) error {
		pkg, err := build.Default.Import(pkgName, ".", 0)
		if err != nil {
			return err
		}

		// Skip standard packages.
		if pkg.Goroot {
			return nil
		}

		// Skip already-visited packages.
		if _, ok := done[pkg.Dir]; ok {
			return nil
		}
		done[pkg.Dir] = struct{}{}

		files = appendPrefixed(files, pkg.GoFiles, pkg.Dir)
		files = appendPrefixed(files, pkg.CgoFiles, pkg.Dir)
		// Include ignored Go files. A subsequent change may cause them
		// to no longer be ignored.
		files = appendPrefixed(files, pkg.IgnoredGoFiles, pkg.Dir)

		// Recurse into imports.
		for _, importName := range pkg.Imports {
			if err := recurse(importName); err != nil {
				return err
			}
		}
		return nil
	}
	if err := recurse(*mainPkg); err != nil {
		fmt.Fprintf(os.Stderr, "Error getting dependencies: %s\n", err)
		os.Exit(1)
	}

	sort.Strings(files)

	outFile := os.Stdout
	if len(*out) != 0 {
		var err error
		outFile, err = os.Create(*out)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error writing output: %s\n", err)
			os.Exit(1)
		}
		defer outFile.Close()
	}

	var err error
	if isDepfile {
		err = writeDepfile(outFile, files)
	} else {
		err = writeCMake(outFile, files)
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error writing output: %s\n", err)
		os.Exit(1)
	}
}
