// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package main

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

type command struct {
	Path string   `json:"path"`
	Args []string `json:"args"`
	// Updates and additions have the form:
	// `NAME=VALUE`
	// Removals have the form:
	// `NAME=`.
	EnvUpdates []string `json:"env_updates,omitempty"`
}

func newProcessCommand() *command {
	// This is a workaround for the fact that ld.so does not support
	// passing in the executable name when ld.so is invoked as
	// an executable (crbug/1003841).
	path := os.Getenv("LD_ARGV0")
	if path == "" {
		path = os.Args[0]
	}
	return &command{
		Path: path,
		Args: os.Args[1:],
	}
}

func mergeEnvValues(values []string, updates []string) []string {
	envMap := map[string]string{}
	for _, entry := range values {
		equalPos := strings.IndexRune(entry, '=')
		envMap[entry[:equalPos]] = entry[equalPos+1:]
	}
	for _, update := range updates {
		equalPos := strings.IndexRune(update, '=')
		key := update[:equalPos]
		value := update[equalPos+1:]
		if value == "" {
			delete(envMap, key)
		} else {
			envMap[key] = value
		}
	}
	env := []string{}
	for key, value := range envMap {
		env = append(env, key+"="+value)
	}
	return env
}

func runCmd(env env, cmd *command, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
	execCmd := exec.Command(cmd.Path, cmd.Args...)
	execCmd.Env = mergeEnvValues(env.environ(), cmd.EnvUpdates)
	execCmd.Dir = env.getwd()
	execCmd.Stdin = stdin
	execCmd.Stdout = stdout
	execCmd.Stderr = stderr
	return execCmd.Run()
}

func resolveAgainstPathEnv(env env, cmd string) (string, error) {
	path, _ := env.getenv("PATH")
	for _, path := range strings.Split(path, ":") {
		resolvedPath := filepath.Join(path, cmd)
		if _, err := os.Lstat(resolvedPath); err == nil {
			return resolvedPath, nil
		}
	}
	return "", fmt.Errorf("Couldn't find cmd %q in path", cmd)
}

func getAbsCmdPath(env env, cmd *command) string {
	path := cmd.Path
	if !filepath.IsAbs(path) {
		path = filepath.Join(env.getwd(), path)
	}
	return path
}

func newCommandBuilder(env env, cfg *config, cmd *command) (*commandBuilder, error) {
	basename := filepath.Base(cmd.Path)
	var nameParts []string
	if basename == "clang-tidy" {
		nameParts = []string{basename}
	} else {
		nameParts = strings.Split(basename, "-")
	}
	target := builderTarget{}
	switch len(nameParts) {
	case 1:
		// E.g. gcc
		target = builderTarget{
			compiler: nameParts[0],
		}
	case 4:
		// E.g. armv7m-cros-eabi-gcc
		target = builderTarget{
			arch:     nameParts[0],
			vendor:   nameParts[1],
			abi:      nameParts[2],
			compiler: nameParts[3],
			target:   basename[:strings.LastIndex(basename, "-")],
		}
	case 5:
		// E.g. x86_64-cros-linux-gnu-gcc
		target = builderTarget{
			arch:     nameParts[0],
			vendor:   nameParts[1],
			sys:      nameParts[2],
			abi:      nameParts[3],
			compiler: nameParts[4],
			target:   basename[:strings.LastIndex(basename, "-")],
		}
	default:
		return nil, newErrorwithSourceLocf("unexpected compiler name pattern. Actual: %s", basename)
	}

	var compilerType compilerType
	switch {
	case strings.HasPrefix(target.compiler, "clang-tidy"):
		compilerType = clangTidyType
	case strings.HasPrefix(target.compiler, "clang"):
		compilerType = clangType
	default:
		compilerType = gccType
	}
	target.compilerType = compilerType
	absWrapperPath, err := getAbsWrapperPath(env, cmd)
	if err != nil {
		return nil, err
	}
	rootPath := filepath.Join(filepath.Dir(absWrapperPath), cfg.rootRelPath)
	return &commandBuilder{
		path:           cmd.Path,
		args:           createBuilderArgs( /*fromUser=*/ true, cmd.Args),
		env:            env,
		cfg:            cfg,
		rootPath:       rootPath,
		absWrapperPath: absWrapperPath,
		target:         target,
	}, nil
}

type commandBuilder struct {
	path           string
	target         builderTarget
	args           []builderArg
	envUpdates     []string
	env            env
	cfg            *config
	rootPath       string
	absWrapperPath string
}

type builderArg struct {
	value    string
	fromUser bool
}

type compilerType int32

const (
	gccType compilerType = iota
	clangType
	clangTidyType
)

type builderTarget struct {
	target       string
	arch         string
	vendor       string
	sys          string
	abi          string
	compiler     string
	compilerType compilerType
}

func createBuilderArgs(fromUser bool, args []string) []builderArg {
	builderArgs := make([]builderArg, len(args))
	for i, arg := range args {
		builderArgs[i] = builderArg{value: arg, fromUser: fromUser}
	}
	return builderArgs
}

func (builder *commandBuilder) clone() *commandBuilder {
	return &commandBuilder{
		path:           builder.path,
		args:           append([]builderArg{}, builder.args...),
		env:            builder.env,
		cfg:            builder.cfg,
		rootPath:       builder.rootPath,
		target:         builder.target,
		absWrapperPath: builder.absWrapperPath,
	}
}

func (builder *commandBuilder) wrapPath(path string) {
	builder.args = append([]builderArg{{value: builder.path, fromUser: false}}, builder.args...)
	builder.path = path
}

func (builder *commandBuilder) addPreUserArgs(args ...string) {
	index := 0
	for _, arg := range builder.args {
		if arg.fromUser {
			break
		}
		index++
	}
	builder.args = append(builder.args[:index], append(createBuilderArgs( /*fromUser=*/ false, args), builder.args[index:]...)...)
}

func (builder *commandBuilder) addPostUserArgs(args ...string) {
	builder.args = append(builder.args, createBuilderArgs( /*fromUser=*/ false, args)...)
}

// Allows to map and filter arguments. Filters when the callback returns an empty string.
func (builder *commandBuilder) transformArgs(transform func(arg builderArg) string) {
	// See https://github.com/golang/go/wiki/SliceTricks
	newArgs := builder.args[:0]
	for _, arg := range builder.args {
		newArg := transform(arg)
		if newArg != "" {
			newArgs = append(newArgs, builderArg{
				value:    newArg,
				fromUser: arg.fromUser,
			})
		}
	}
	builder.args = newArgs
}

func (builder *commandBuilder) updateEnv(updates ...string) {
	builder.envUpdates = append(builder.envUpdates, updates...)
}

func (builder *commandBuilder) build() *command {
	cmdArgs := make([]string, len(builder.args))
	for i, builderArg := range builder.args {
		cmdArgs[i] = builderArg.value
	}
	return &command{
		Path:       builder.path,
		Args:       cmdArgs,
		EnvUpdates: builder.envUpdates,
	}
}