// Package utils ...
package utils

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"image"
	"image/jpeg"
	"image/png"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"regexp"
	"strings"
	"sync"
	"text/template"
	"time"

	"github.com/ysmood/gson"
)

// TestEnvs for testing.
var TestEnvs = map[string]string{
	"GODEBUG": "tracebackancestors=100",
}

// InContainer will be true if is inside container environment, such as docker.
var InContainer = FileExists("/.dockerenv") || FileExists("/.containerenv") ||
	os.Getenv("KUBERNETES_SERVICE_HOST") != ""

// Noop does nothing.
func Noop() {}

// Logger interface.
type Logger interface {
	// Same as fmt.Printf
	Println(vs ...interface{})
}

// Log type for Println.
type Log func(msg ...interface{})

// Println interface.
func (l Log) Println(msg ...interface{}) {
	l(msg...)
}

// LoggerQuiet does nothing.
var LoggerQuiet Logger = Log(func(_ ...interface{}) {})

// MultiLogger is similar to https://golang.org/pkg/io/#MultiWriter
func MultiLogger(list ...Logger) Log {
	return Log(func(msg ...interface{}) {
		for _, lg := range list {
			lg.Println(msg...)
		}
	})
}

// Panic is the same as the built-in panic.
var Panic = func(v interface{}) { panic(v) }

// E if the last arg is error, panic it.
func E(args ...interface{}) []interface{} {
	err, ok := args[len(args)-1].(error)
	if ok {
		Panic(err)
	}
	return args
}

// S Template render, the params is key-value pairs.
func S(tpl string, params ...interface{}) string {
	var out bytes.Buffer

	dict := map[string]interface{}{}
	fnDict := template.FuncMap{}

	l := len(params)
	for i := 0; i < l-1; i += 2 {
		k := params[i].(string) //nolint: forcetypeassert
		v := params[i+1]
		if reflect.TypeOf(v).Kind() == reflect.Func {
			fnDict[k] = v
		} else {
			dict[k] = v
		}
	}

	t := template.Must(template.New("").Funcs(fnDict).Parse(tpl))
	E(t.Execute(&out, dict))

	return out.String()
}

// RandString generate random string with specified string length.
func RandString(l int) string {
	b := make([]byte, l)
	_, _ = rand.Read(b)
	return hex.EncodeToString(b)
}

// Mkdir makes dir recursively.
func Mkdir(path string) error {
	return os.MkdirAll(path, 0o775)
}

// AbsolutePaths returns absolute paths of files in current working directory.
func AbsolutePaths(paths []string) []string {
	absPaths := []string{}
	for _, p := range paths {
		absPath, err := filepath.Abs(p)
		E(err)
		absPaths = append(absPaths, absPath)
	}
	return absPaths
}

// OutputFile auto creates file if not exists, it will try to detect the data type and
// auto output binary, string or json.
func OutputFile(p string, data interface{}) error {
	dir := filepath.Dir(p)
	_ = Mkdir(dir)

	var bin []byte

	switch t := data.(type) {
	case []byte:
		bin = t
	case string:
		bin = []byte(t)
	case io.Reader:
		f, _ := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o664)
		_, err := io.Copy(f, t)
		return err
	default:
		bin = MustToJSONBytes(data)
	}

	return os.WriteFile(p, bin, 0o664)
}

// ReadString reads file as string.
func ReadString(p string) (string, error) {
	bin, err := os.ReadFile(p)
	return string(bin), err
}

// All runs all actions concurrently, returns the wait function for all actions.
func All(actions ...func()) func() {
	wg := &sync.WaitGroup{}

	wg.Add(len(actions))

	runner := func(action func()) {
		defer wg.Done()
		action()
	}

	for _, action := range actions {
		go runner(action)
	}

	return wg.Wait
}

// IdleCounter is similar to sync.WaitGroup but it only resolves if no jobs for specified duration.
type IdleCounter struct {
	lock     *sync.Mutex
	job      int
	duration time.Duration
	tmr      *time.Timer
}

// NewIdleCounter ...
func NewIdleCounter(d time.Duration) *IdleCounter {
	tmr := time.NewTimer(time.Hour)
	tmr.Stop()

	return &IdleCounter{
		lock:     &sync.Mutex{},
		duration: d,
		tmr:      tmr,
	}
}

// Add ...
func (de *IdleCounter) Add() {
	de.lock.Lock()
	defer de.lock.Unlock()

	de.tmr.Stop()
	de.job++
}

// Done ...
func (de *IdleCounter) Done() {
	de.lock.Lock()
	defer de.lock.Unlock()

	de.job--
	if de.job == 0 {
		de.reset(de.duration)
	}
	if de.job < 0 {
		panic("all jobs are already done")
	}
}

// Wait ...
func (de *IdleCounter) Wait(ctx context.Context) {
	de.lock.Lock()
	if de.job == 0 {
		de.reset(de.duration)
	}
	de.lock.Unlock()

	select {
	case <-ctx.Done():
		de.tmr.Stop()
	case <-de.tmr.C:
	}
}

func (de *IdleCounter) reset(d time.Duration) {
	if !de.tmr.Stop() {
		select {
		case <-de.tmr.C:
		default:
		}
	}
	de.tmr.Reset(d)
}

var chPause = make(chan struct{})

// Pause the goroutine forever.
func Pause() {
	<-chPause
}

// Dump values for debugging.
func Dump(list ...interface{}) string {
	out := []string{}
	for _, el := range list {
		out = append(out, gson.New(el).JSON("", "  "))
	}
	return strings.Join(out, " ")
}

// MustToJSONBytes encode data to json bytes.
func MustToJSONBytes(data interface{}) []byte {
	buf := bytes.NewBuffer(nil)
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(false)
	E(enc.Encode(data))
	b := buf.Bytes()
	return b[:len(b)-1]
}

// MustToJSON encode data to json string.
func MustToJSON(data interface{}) string {
	return string(MustToJSONBytes(data))
}

// FileExists checks if file exists, only for file, not for dir.
func FileExists(path string) bool {
	info, err := os.Stat(path)
	if err != nil {
		return false
	}

	if info.IsDir() {
		return false
	}

	return true
}

var regSpace = regexp.MustCompile(`\s`)

// Exec command.
func Exec(line string, rest ...string) string {
	return ExecLine(true, line, rest...)
}

var execLogger = log.New(os.Stdout, "[exec] ", 0)

// ExecLine of command.
func ExecLine(std bool, line string, rest ...string) string {
	args := rest
	if line != "" {
		args = append(regSpace.Split(line, -1), rest...)
	}

	execLogger.Println(FormatCLIArgs(args))

	buf := bytes.NewBuffer(nil)

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stderr = buf
	cmd.Stdout = buf

	if std {
		cmd.Stdin = os.Stdin
		cmd.Stderr = io.MultiWriter(buf, os.Stderr)
		cmd.Stdout = io.MultiWriter(buf, os.Stdout)
	}

	if err := cmd.Run(); err != nil {
		if std {
			panic(err)
		}
		panic(fmt.Sprintf("%v\n%v", err, buf.String()))
	}

	return buf.String()
}

// UseNode installs Node.js and set the bin path to PATH env var.
func UseNode(std bool) {
	binPath := strings.TrimSpace(ExecLine(std, "go run github.com/ysmood/use-node@latest -p v20"))
	E(os.Setenv("PATH", binPath+string(os.PathListSeparator)+os.Getenv("PATH")))
}

// FormatCLIArgs into one line string.
func FormatCLIArgs(args []string) string {
	list := []string{}
	for _, arg := range args {
		if regSpace.MatchString(arg) {
			list = append(list, fmt.Sprintf("%#v", arg))
		} else {
			list = append(list, arg)
		}
	}
	return strings.Join(list, " ")
}

// EscapeGoString not using encoding like base64 or gzip because of they will
// make git diff every large for small change.
func EscapeGoString(s string) string {
	return "`" + strings.ReplaceAll(s, "`", "` + \"`\" + `") + "`"
}

// CropImage by the specified box, quality is only for jpeg bin.
func CropImage(bin []byte, quality, x, y, width, height int) ([]byte, error) {
	img, typ, err := image.Decode(bytes.NewBuffer(bin))
	if err != nil {
		return nil, err
	}

	cropped := bytes.NewBuffer(nil)

	img = img.(interface { //nolint: forcetypeassert
		SubImage(r image.Rectangle) image.Image
	}).SubImage(image.Rect(
		x, y, x+width, y+height,
	))

	switch typ {
	case "png":
		err = png.Encode(cropped, img)
	case "jpeg":
		if quality == 0 {
			quality = 80
		}

		err = jpeg.Encode(cropped, img, &jpeg.Options{Quality: quality})
	}

	return cropped.Bytes(), err
}
