package util

import (
	"archive/zip"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/user"
	"path/filepath"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"
	"unicode"

	"github.com/blang/semver"
	runewidth "github.com/mattn/go-runewidth"
)

var (
	// These variables should be set by the linker when compiling

	// Version is the version number or commit hash
	Version = "0.0.0-unknown"
	// SemVersion is the Semantic version
	SemVersion semver.Version
	// CommitHash is the commit this version was built on
	CommitHash = "Unknown"
	// CompileDate is the date this binary was compiled on
	CompileDate = "Unknown"
	// Debug logging
	Debug = "OFF"
	// FakeCursor is used to disable the terminal cursor and have micro
	// draw its own (enabled for windows consoles where the cursor is slow)
	FakeCursor = false

	// Stdout is a buffer that is written to stdout when micro closes
	Stdout *bytes.Buffer
)

func init() {
	var err error
	SemVersion, err = semver.Make(Version)
	if err != nil {
		fmt.Println("Invalid version: ", Version, err)
	}

	_, wt := os.LookupEnv("WT_SESSION")
	if runtime.GOOS == "windows" && !wt {
		FakeCursor = true
	}
	Stdout = new(bytes.Buffer)
}

// SliceEnd returns a byte slice where the index is a rune index
// Slices off the start of the slice
func SliceEnd(slc []byte, index int) []byte {
	len := len(slc)
	i := 0
	totalSize := 0
	for totalSize < len {
		if i >= index {
			return slc[totalSize:]
		}

		_, _, size := DecodeCharacter(slc[totalSize:])
		totalSize += size
		i++
	}

	return slc[totalSize:]
}

// SliceEndStr is the same as SliceEnd but for strings
func SliceEndStr(str string, index int) string {
	len := len(str)
	i := 0
	totalSize := 0
	for totalSize < len {
		if i >= index {
			return str[totalSize:]
		}

		_, _, size := DecodeCharacterInString(str[totalSize:])
		totalSize += size
		i++
	}

	return str[totalSize:]
}

// SliceStart returns a byte slice where the index is a rune index
// Slices off the end of the slice
func SliceStart(slc []byte, index int) []byte {
	len := len(slc)
	i := 0
	totalSize := 0
	for totalSize < len {
		if i >= index {
			return slc[:totalSize]
		}

		_, _, size := DecodeCharacter(slc[totalSize:])
		totalSize += size
		i++
	}

	return slc[:totalSize]
}

// SliceStartStr is the same as SliceStart but for strings
func SliceStartStr(str string, index int) string {
	len := len(str)
	i := 0
	totalSize := 0
	for totalSize < len {
		if i >= index {
			return str[:totalSize]
		}

		_, _, size := DecodeCharacterInString(str[totalSize:])
		totalSize += size
		i++
	}

	return str[:totalSize]
}

// SliceVisualEnd will take a byte slice and slice off the start
// up to a given visual index. If the index is in the middle of a
// rune the number of visual columns into the rune will be returned
// It will also return the char pos of the first character of the slice
func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int, int) {
	width := 0
	i := 0
	for len(b) > 0 {
		r, _, size := DecodeCharacter(b)

		w := 0
		switch r {
		case '\t':
			ts := tabsize - (width % tabsize)
			w = ts
		default:
			w = runewidth.RuneWidth(r)
		}
		if width+w > n {
			return b, n - width, i
		}
		width += w
		b = b[size:]
		i++
	}
	return b, n - width, i
}

// Abs is a simple absolute value function for ints
func Abs(n int) int {
	if n < 0 {
		return -n
	}
	return n
}

// StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
// with a given tabsize
func StringWidth(b []byte, n, tabsize int) int {
	if n <= 0 {
		return 0
	}
	i := 0
	width := 0
	for len(b) > 0 {
		r, _, size := DecodeCharacter(b)
		b = b[size:]

		switch r {
		case '\t':
			ts := tabsize - (width % tabsize)
			width += ts
		default:
			width += runewidth.RuneWidth(r)
		}

		i++

		if i == n {
			return width
		}
	}
	return width
}

// Min takes the min of two ints
func Min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

// Max takes the max of two ints
func Max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// FSize gets the size of a file
func FSize(f *os.File) int64 {
	fi, _ := f.Stat()
	return fi.Size()
}

// IsWordChar returns whether or not the string is a 'word character'
// Word characters are defined as numbers, letters, or '_'
func IsWordChar(r rune) bool {
	return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
}

// Spaces returns a string with n spaces
func Spaces(n int) string {
	return strings.Repeat(" ", n)
}

// IsSpaces checks if a given string is only spaces
func IsSpaces(str []byte) bool {
	for _, c := range str {
		if c != ' ' {
			return false
		}
	}

	return true
}

// IsSpacesOrTabs checks if a given string contains only spaces and tabs
func IsSpacesOrTabs(str []byte) bool {
	for _, c := range str {
		if c != ' ' && c != '\t' {
			return false
		}
	}

	return true
}

// IsWhitespace returns true if the given rune is a space, tab, or newline
func IsWhitespace(c rune) bool {
	return unicode.IsSpace(c)
}

// IsBytesWhitespace returns true if the given bytes are all whitespace
func IsBytesWhitespace(b []byte) bool {
	for _, c := range b {
		if !IsWhitespace(rune(c)) {
			return false
		}
	}
	return true
}

// RunePos returns the rune index of a given byte index
// Make sure the byte index is not between code points
func RunePos(b []byte, i int) int {
	return CharacterCount(b[:i])
}

// MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) {
	if len(path) > 0 {
		rel, err := filepath.Rel(base, path)
		if err != nil {
			return path, err
		}
		return rel, nil
	}
	return path, nil
}

// ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
// home directory. Does nothing if the path does not start with '~'.
func ReplaceHome(path string) (string, error) {
	if !strings.HasPrefix(path, "~") {
		return path, nil
	}

	var userData *user.User
	var err error

	homeString := strings.Split(path, "/")[0]
	if homeString == "~" {
		userData, err = user.Current()
		if err != nil {
			return "", errors.New("Could not find user: " + err.Error())
		}
	} else {
		userData, err = user.Lookup(homeString[1:])
		if err != nil {
			return "", errors.New("Could not find user: " + err.Error())
		}
	}

	home := userData.HomeDir

	return strings.Replace(path, homeString, home, 1), nil
}

// GetPathAndCursorPosition returns a filename without everything following a `:`
// This is used for opening files like util.go:10:5 to specify a line and column
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
func GetPathAndCursorPosition(path string) (string, []string) {
	re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
	match := re.FindStringSubmatch(path)
	// no lines/columns were specified in the path, return just the path with no cursor location
	if len(match) == 0 {
		return path, nil
	} else if match[len(match)-1] != "" {
		// if the last capture group match isn't empty then both line and column were provided
		return match[1], match[2:]
	}
	// if it was empty, then only a line was provided, so default to column 0
	return match[1], []string{match[2], "0"}
}

// GetModTime returns the last modification time for a given file
func GetModTime(path string) (time.Time, error) {
	info, err := os.Stat(path)
	if err != nil {
		return time.Now(), err
	}
	return info.ModTime(), nil
}

// EscapePath replaces every path separator in a given path with a %
func EscapePath(path string) string {
	path = filepath.ToSlash(path)
	if runtime.GOOS == "windows" {
		// ':' is not valid in a path name on Windows but is ok on Unix
		path = strings.ReplaceAll(path, ":", "%")
	}
	return strings.ReplaceAll(path, "/", "%")
}

// GetLeadingWhitespace returns the leading whitespace of the given byte array
func GetLeadingWhitespace(b []byte) []byte {
	ws := []byte{}
	for len(b) > 0 {
		r, _, size := DecodeCharacter(b)
		if r == ' ' || r == '\t' {
			ws = append(ws, byte(r))
		} else {
			break
		}

		b = b[size:]
	}
	return ws
}

// IntOpt turns a float64 setting to an int
func IntOpt(opt interface{}) int {
	return int(opt.(float64))
}

// GetCharPosInLine gets the char position of a visual x y
// coordinate (this is necessary because tabs are 1 char but
// 4 visual spaces)
func GetCharPosInLine(b []byte, visualPos int, tabsize int) int {
	// Scan rune by rune until we exceed the visual width that we are
	// looking for. Then we can return the character position we have found
	i := 0     // char pos
	width := 0 // string visual width
	for len(b) > 0 {
		r, _, size := DecodeCharacter(b)
		b = b[size:]

		switch r {
		case '\t':
			ts := tabsize - (width % tabsize)
			width += ts
		default:
			width += runewidth.RuneWidth(r)
		}

		if width >= visualPos {
			if width == visualPos {
				i++
			}
			break
		}
		i++
	}

	return i
}

// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
// as 'true' and 'false' respectively
func ParseBool(str string) (bool, error) {
	if str == "on" {
		return true, nil
	}
	if str == "off" {
		return false, nil
	}
	return strconv.ParseBool(str)
}

// Clamp clamps a value between min and max
func Clamp(val, min, max int) int {
	if val < min {
		val = min
	} else if val > max {
		val = max
	}
	return val
}

// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
func IsNonAlphaNumeric(c rune) bool {
	return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
}

// IsAutocomplete returns whether a character should begin an autocompletion.
func IsAutocomplete(c rune) bool {
	return c == '.' || !IsNonAlphaNumeric(c)
}

// ParseSpecial replaces escaped ts with '\t'.
func ParseSpecial(s string) string {
	return strings.ReplaceAll(s, "\\t", "\t")
}

// String converts a byte array to a string (for lua plugins)
func String(s []byte) string {
	return string(s)
}

// Unzip unzips a file to given folder
func Unzip(src, dest string) error {
	r, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer r.Close()

	os.MkdirAll(dest, 0755)

	// Closure to address file descriptors issue with all the deferred .Close() methods
	extractAndWriteFile := func(f *zip.File) error {
		rc, err := f.Open()
		if err != nil {
			return err
		}
		defer rc.Close()

		path := filepath.Join(dest, f.Name)

		// Check for ZipSlip (Directory traversal)
		if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
			return fmt.Errorf("illegal file path: %s", path)
		}

		if f.FileInfo().IsDir() {
			os.MkdirAll(path, f.Mode())
		} else {
			os.MkdirAll(filepath.Dir(path), f.Mode())
			f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
			if err != nil {
				return err
			}
			defer f.Close()

			_, err = io.Copy(f, rc)
			if err != nil {
				return err
			}
		}
		return nil
	}

	for _, f := range r.File {
		err := extractAndWriteFile(f)
		if err != nil {
			return err
		}
	}

	return nil
}
