package staros

import (
	"bytes"
	"context"
	"errors"
	"io"
	"os"
	"os/exec"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"
)

//StarCmd Is Here

type StarCmd struct {
	CMD     *exec.Cmd
	outfile io.ReadCloser
	infile  io.WriteCloser
	errfile io.ReadCloser
	running int32
	//Store AlL of the Standed Outputs
	stdout []byte
	//Store All of the Standed Errors
	errout       []byte
	runerr       error
	exitcode     int
	stdoutBuf    *bytes.Buffer
	stderrBuf    *bytes.Buffer
	stdoutpoint  int
	stderrpoint  int
	lock         sync.Mutex
	prewrite     []string
	prewritetime time.Duration
	stopctxfunc  context.CancelFunc
	stopctx      context.Context
}

func Command(command string, args ...string) (*StarCmd, error) {
	var err error
	shell := new(StarCmd)
	shell.running = 0
	shell.prewritetime = time.Millisecond * 200
	shell.stdoutBuf = bytes.NewBuffer(make([]byte, 0))
	shell.stderrBuf = bytes.NewBuffer(make([]byte, 0))
	shell.stopctx, shell.stopctxfunc = context.WithCancel(context.Background())
	cmd := exec.Command(command, args...)
	shell.CMD = cmd
	shell.infile, err = shell.CMD.StdinPipe()
	if err != nil {
		return shell, err
	}
	shell.errfile, err = shell.CMD.StderrPipe()
	if err != nil {
		return shell, err
	}
	shell.outfile, err = shell.CMD.StdoutPipe()
	if err != nil {
		return shell, err
	}
	shell.runerr = nil
	shell.exitcode = -999
	return shell, nil
}
func CommandContext(ctx context.Context, command string, args ...string) (*StarCmd, error) {
	var err error
	shell := new(StarCmd)
	shell.running = 0
	shell.stdoutBuf = bytes.NewBuffer(make([]byte, 0))
	shell.stderrBuf = bytes.NewBuffer(make([]byte, 0))
	shell.prewritetime = time.Millisecond * 200
	shell.stopctx, shell.stopctxfunc = context.WithCancel(context.Background())
	cmd := exec.CommandContext(ctx, command, args...)
	shell.CMD = cmd
	shell.infile, err = shell.CMD.StdinPipe()
	if err != nil {
		return shell, err
	}
	shell.errfile, err = shell.CMD.StderrPipe()
	if err != nil {
		return shell, err
	}
	shell.outfile, err = shell.CMD.StdoutPipe()
	if err != nil {
		return shell, err
	}
	shell.runerr = nil
	shell.exitcode = -999
	return shell, nil
}

func (starcli *StarCmd) queryStdout(ctx context.Context) {
	for starcli.IsRunning() && starcli.CMD != nil {
		select {
		case <-ctx.Done():
			return
		default:
		}
		out := make([]byte, 65535)
		n, err := starcli.outfile.Read(out)
		if n != 0 {
			starcli.lock.Lock()
			starcli.stdoutBuf.Write(out[:n])
			starcli.lock.Unlock()
			for _, v := range out[:n] {
				starcli.stdout = append(starcli.stdout, v)
			}
		}
		if err != nil {
			if err == io.EOF {
				break
			} else {
				if !strings.Contains(err.Error(), "file already closed") {
					starcli.runerr = err
				}
				return
			}
		}
	}
}

func (starcli *StarCmd) queryStderr(ctx context.Context) {
	for starcli.IsRunning() && starcli.CMD != nil {
		select {
		case <-ctx.Done():
			return
		default:
		}
		out := make([]byte, 65535)
		n, err := starcli.errfile.Read(out)
		if n != 0 {
			starcli.lock.Lock()
			starcli.stderrBuf.Write(out[:n])
			starcli.lock.Unlock()
			for _, v := range out[:n] {
				starcli.errout = append(starcli.errout, v)
			}
		}
		if err != nil {
			if err == io.EOF {
				break
			} else {
				if !strings.Contains(err.Error(), "file already closed") {
					starcli.runerr = err
				}
				return
			}
		}
	}
	return
}
func (starcli *StarCmd) NowLineOutput() (string, error) {
	starcli.lock.Lock()
	buf, _ := starcli.stdoutBuf.ReadBytes('\n')
	buferr, _ := starcli.stderrBuf.ReadBytes(byte('\n'))
	starcli.lock.Unlock()
	if len(buferr) != 0 {
		return string(buf), errors.New(string(buferr))
	}
	return string(buf), nil
}

func (starcli *StarCmd) NowLineStdOut() string {
	starcli.lock.Lock()
	defer starcli.lock.Unlock()
	buf, _ := starcli.stdoutBuf.ReadBytes('\n')
	return string(buf)
}

func (starcli *StarCmd) NowLineStdErr() error {
	starcli.lock.Lock()
	defer starcli.lock.Unlock()
	buferr, _ := starcli.stderrBuf.ReadBytes(byte('\n'))
	if len(buferr) != 0 {
		return errors.New(string(buferr))
	}
	return nil
}

func (starcli *StarCmd) NowAllOutput() (string, error) {
	var outstr string
	starcli.lock.Lock()
	buf := make([]byte, starcli.stdoutBuf.Len())
	n, _ := starcli.stdoutBuf.Read(buf)
	starcli.lock.Unlock()
	if n != 0 {
		outstr = string(buf[:n])
	}
	if starcli.runerr != nil {
		return outstr, starcli.runerr
	}
	starcli.lock.Lock()
	buf = make([]byte, starcli.stderrBuf.Len())
	n, _ = starcli.stderrBuf.Read(buf)
	starcli.lock.Unlock()
	if n != 0 {
		return outstr, errors.New(string(buf[:n]))
	}
	return outstr, nil
}

func (starcli *StarCmd) NowStdOut() string {
	var outstr string
	starcli.lock.Lock()
	buf := make([]byte, starcli.stdoutBuf.Len())
	n, _ := starcli.stdoutBuf.Read(buf)
	starcli.lock.Unlock()
	if n != 0 {
		outstr = string(buf[:n])
	}
	return outstr
}

func (starcli *StarCmd) NowStdErr() error {
	starcli.lock.Lock()
	buf := make([]byte, starcli.stderrBuf.Len())
	n, _ := starcli.stderrBuf.Read(buf)
	starcli.lock.Unlock()
	if n != 0 {
		return errors.New(string(buf[:n]))
	}
	return nil
}

func (starcli *StarCmd) AllOutPut() (string, error) {
	err := starcli.runerr
	if err == nil && len(starcli.errout) != 0 {
		err = errors.New(string(starcli.errout))
	}
	return string(starcli.stdout), err
}

func (starcli *StarCmd) AllStdOut() string {
	return string(starcli.stdout)
}

func (starcli *StarCmd) AllStdErr() error {
	err := starcli.runerr
	if err == nil && len(starcli.errout) != 0 {
		err = errors.New(string(starcli.errout))
	}
	return err
}

func (starcli *StarCmd) setRunning(alive bool) {
	if alive {
		val := atomic.LoadInt32(&starcli.running)
		if val == 0 {
			atomic.AddInt32(&starcli.running, 1)
		} else {
			atomic.AddInt32(&starcli.running, 1-val)
		}
		return
	}
	val := atomic.LoadInt32(&starcli.running)
	if val == 1 {
		atomic.AddInt32(&starcli.running, -1)
	} else {
		atomic.AddInt32(&starcli.running, -val)
	}
}
func (starcli *StarCmd) Start() error {
	if err := starcli.CMD.Start(); err != nil {
		return err
	}
	starcli.setRunning(true)
	go func() {
		err := starcli.CMD.Wait()
		if err != nil {
			starcli.runerr = err
		}
		starcli.stopctxfunc()
		starcli.setRunning(false)
		if starcli.CMD.ProcessState != nil {
			starcli.exitcode = starcli.CMD.ProcessState.Sys().(syscall.WaitStatus).ExitStatus()
		}
	}()
	go starcli.queryStdout(starcli.stopctx)
	go starcli.queryStderr(starcli.stopctx)
	go func(ctx context.Context) {
		if len(starcli.prewrite) != 0 {
			for _, v := range starcli.prewrite {
				select {
				case <-ctx.Done():
					return
				default:
					break
				}
				starcli.WriteCmd(v)
				time.Sleep(starcli.prewritetime)
			}
		}
	}(starcli.stopctx)
	return nil
}

func (starcli *StarCmd) IsRunning() bool {
	return 0 != atomic.LoadInt32(&starcli.running)
}

func (starcli *StarCmd) Stoped() <-chan struct{} {
	return starcli.stopctx.Done()
}

func (starcli *StarCmd) Exec(cmd string, wait int) (string, error) {
	starcli.infile.Write([]byte(cmd + "\n"))
	time.Sleep(time.Millisecond * time.Duration(wait))
	return starcli.NowAllOutput()
}

func (starcli *StarCmd) WriteCmd(cmdstr string) {
	starcli.infile.Write([]byte(cmdstr + "\n"))
}

func (starcli *StarCmd) PreWrite(cmd ...string) {
	for _, v := range cmd {
		starcli.prewrite = append(starcli.prewrite, v)
	}
}

func (starcli *StarCmd) PreWriteInterval(dt time.Duration) {
	starcli.prewritetime = dt
}

func (starcli *StarCmd) ExitCode() int {
	return starcli.exitcode
}

func (starcli *StarCmd) Kill() error {
	err := starcli.CMD.Process.Kill()
	if err != nil {
		return err
	}
	starcli.setRunning(false)
	return nil
}

func (starcli *StarCmd) GetPid() int {
	return starcli.CMD.Process.Pid
}

func (starcli *StarCmd) Signal(sig os.Signal) error {
	return starcli.CMD.Process.Signal(sig)
}
