// This file is part of PathsHelper library.
//
// Copyright 2018-2025 Arduino AG (http://www.arduino.cc/)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// 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 <https://www.gnu.org/licenses/>.
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to license@arduino.cc.

package paths

import (
	"bytes"
	"context"
	"errors"
	"io"
	"os"
	"os/exec"
)

// Process is representation of an external process run
type Process struct {
	cmd *exec.Cmd
}

// NewProcess creates a command with the provided command line arguments
// and environment variables (that will be added to the parent os.Environ).
// The argument args[0] is the path to the executable, the remainder are the
// arguments to the command.
func NewProcess(extraEnv []string, args ...string) (*Process, error) {
	if len(args) == 0 {
		return nil, errors.New("no executable specified")
	}
	p := &Process{
		cmd: exec.Command(args[0], args[1:]...),
	}
	p.cmd.Env = append(os.Environ(), extraEnv...)
	tellCommandNotToSpawnShell(p.cmd)          // windows specific
	tellCommandToStartOnNewProcessGroup(p.cmd) // linux and macosx specific

	// This is required because some tools detects if the program is running
	// from terminal by looking at the stdin/out bindings.
	// https://github.com/arduino/arduino-cli/issues/844
	p.cmd.Stdin = nullReaderInstance
	return p, nil
}

// TellCommandNotToSpawnShell avoids that the specified Cmd display a small
// command prompt while runnning on Windows. It has no effects on other OS.
//
// Deprecated: TellCommandNotToSpawnShell is now always applied by default, there is no need to call it anymore.
func (p *Process) TellCommandNotToSpawnShell() {
	tellCommandNotToSpawnShell(p.cmd)
}

// NewProcessFromPath creates a command from the provided executable path,
// additional environment vars (in addition to the system default ones)
// and command line arguments.
func NewProcessFromPath(extraEnv []string, executable *Path, args ...string) (*Process, error) {
	processArgs := []string{executable.String()}
	processArgs = append(processArgs, args...)
	return NewProcess(extraEnv, processArgs...)
}

// RedirectStdoutTo will redirect the process' stdout to the specified
// writer. Any previous redirection will be overwritten.
func (p *Process) RedirectStdoutTo(out io.Writer) {
	p.cmd.Stdout = out
}

// RedirectStderrTo will redirect the process' stderr to the specified
// writer. Any previous redirection will be overwritten.
func (p *Process) RedirectStderrTo(err io.Writer) {
	p.cmd.Stderr = err
}

// StdinPipe returns a pipe that will be connected to the command's standard
// input when the command starts. The pipe will be closed automatically after
// Wait sees the command exit. A caller need only call Close to force the pipe
// to close sooner. For example, if the command being run will not exit until
// standard input is closed, the caller must close the pipe.
func (p *Process) StdinPipe() (io.WriteCloser, error) {
	if p.cmd.Stdin == nullReaderInstance {
		p.cmd.Stdin = nil
	}
	return p.cmd.StdinPipe()
}

// StdoutPipe returns a pipe that will be connected to the command's standard
// output when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// don't need to close the pipe themselves. It is thus incorrect to call Wait
// before all reads from the pipe have completed.
// For the same reason, it is incorrect to call Run when using StdoutPipe.
func (p *Process) StdoutPipe() (io.ReadCloser, error) {
	return p.cmd.StdoutPipe()
}

// StderrPipe returns a pipe that will be connected to the command's standard
// error when the command starts.
//
// Wait will close the pipe after seeing the command exit, so most callers
// don't need to close the pipe themselves. It is thus incorrect to call Wait
// before all reads from the pipe have completed.
// For the same reason, it is incorrect to use Run when using StderrPipe.
func (p *Process) StderrPipe() (io.ReadCloser, error) {
	return p.cmd.StderrPipe()
}

// Start will start the underliyng process.
func (p *Process) Start() error {
	return p.cmd.Start()
}

// Wait waits for the command to exit and waits for any copying to stdin or copying
// from stdout or stderr to complete.
func (p *Process) Wait() error {
	// TODO: make some helpers to retrieve exit codes out of *ExitError.
	return p.cmd.Wait()
}

// WaitWithinContext wait for the process to complete. If the given context is canceled
// before the normal process termination, the process is killed.
func (p *Process) WaitWithinContext(ctx context.Context) error {
	completed := make(chan struct{})
	defer close(completed)
	go func() {
		select {
		case <-ctx.Done():
			p.Kill()
		case <-completed:
		}
	}()
	return p.Wait()
}

// Signal sends a signal to the Process. Sending Interrupt on Windows is not implemented.
func (p *Process) Signal(sig os.Signal) error {
	return p.cmd.Process.Signal(sig)
}

// Kill causes the Process to exit immediately. Kill does not wait until the Process has
// actually exited. This only kills the Process itself, not any other processes it may
// have started.
func (p *Process) Kill() error {
	return kill(p.cmd)
}

// SetDir sets the working directory of the command. If Dir is the empty string, Run
// runs the command in the calling process's current directory.
func (p *Process) SetDir(dir string) {
	p.cmd.Dir = dir
}

// GetDir gets the working directory of the command.
func (p *Process) GetDir() string {
	return p.cmd.Dir
}

// SetDirFromPath sets the working directory of the command. If path is nil, Run
// runs the command in the calling process's current directory.
func (p *Process) SetDirFromPath(path *Path) {
	if path == nil {
		p.cmd.Dir = ""
	} else {
		p.cmd.Dir = path.String()
	}
}

// Run starts the specified command and waits for it to complete.
func (p *Process) Run() error {
	return p.cmd.Run()
}

// SetEnvironment set the environment for the running process. Each entry is of the form "key=value".
// System default environments will be wiped out.
func (p *Process) SetEnvironment(values []string) {
	p.cmd.Env = append([]string{}, values...)
}

// RunWithinContext starts the specified command and waits for it to complete. If the given context
// is canceled before the normal process termination, the process is killed.
func (p *Process) RunWithinContext(ctx context.Context) error {
	if err := p.Start(); err != nil {
		return err
	}
	return p.WaitWithinContext(ctx)
}

// RunAndCaptureOutput starts the specified command and waits for it to complete. If the given context
// is canceled before the normal process termination, the process is killed. The standard output and
// standard error of the process are captured and returned at process termination.
func (p *Process) RunAndCaptureOutput(ctx context.Context) ([]byte, []byte, error) {
	stdout := &bytes.Buffer{}
	stderr := &bytes.Buffer{}
	p.RedirectStdoutTo(stdout)
	p.RedirectStderrTo(stderr)
	err := p.RunWithinContext(ctx)
	return stdout.Bytes(), stderr.Bytes(), err
}

// RunAndCaptureCombinedOutput starts the specified command and waits for it to complete. If the given context
// is canceled before the normal process termination, the process is killed. The standard output and
// standard error of the process are captured and returned combined at process termination.
func (p *Process) RunAndCaptureCombinedOutput(ctx context.Context) ([]byte, error) {
	out := &bytes.Buffer{}
	p.RedirectStdoutTo(out)
	p.RedirectStderrTo(out)
	err := p.RunWithinContext(ctx)
	return out.Bytes(), err
}

// GetArgs returns the command arguments
func (p *Process) GetArgs() []string {
	return p.cmd.Args
}

// nullReaderInstance is an io.Reader that will always return EOF
var nullReaderInstance = &nullReader{}

type nullReader struct{}

func (r *nullReader) Read(buff []byte) (int, error) {
	return 0, io.EOF
}
