package symlink

import (
	"bytes"
	"errors"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/sys/windows"
)

func toShort(path string) (string, error) {
	p, err := windows.UTF16FromString(path)
	if err != nil {
		return "", err
	}
	b := p // GetShortPathName says we can reuse buffer
	n, err := windows.GetShortPathName(&p[0], &b[0], uint32(len(b)))
	if err != nil {
		return "", err
	}
	if n > uint32(len(b)) {
		b = make([]uint16, n)
		if _, err = windows.GetShortPathName(&p[0], &b[0], uint32(len(b))); err != nil {
			return "", err
		}
	}
	return windows.UTF16ToString(b), nil
}

func toLong(path string) (string, error) {
	p, err := windows.UTF16FromString(path)
	if err != nil {
		return "", err
	}
	b := p // GetLongPathName says we can reuse buffer
	n, err := windows.GetLongPathName(&p[0], &b[0], uint32(len(b)))
	if err != nil {
		return "", err
	}
	if n > uint32(len(b)) {
		b = make([]uint16, n)
		n, err = windows.GetLongPathName(&p[0], &b[0], uint32(len(b)))
		if err != nil {
			return "", err
		}
	}
	b = b[:n]
	return windows.UTF16ToString(b), nil
}

func evalSymlinks(path string) (string, error) {
	path, err := walkSymlinks(path)
	if err != nil {
		return "", err
	}

	p, err := toShort(path)
	if err != nil {
		return "", err
	}
	p, err = toLong(p)
	if err != nil {
		return "", err
	}
	// windows.GetLongPathName does not change the case of the drive letter,
	// but the result of EvalSymlinks must be unique, so we have
	// EvalSymlinks(`c:\a`) == EvalSymlinks(`C:\a`).
	// Make drive letter upper case.
	if len(p) >= 2 && p[1] == ':' && 'a' <= p[0] && p[0] <= 'z' {
		p = string(p[0]+'A'-'a') + p[1:]
	} else if len(p) >= 6 && p[5] == ':' && 'a' <= p[4] && p[4] <= 'z' {
		p = p[:3] + string(p[4]+'A'-'a') + p[5:]
	}
	return filepath.Clean(p), nil
}

const (
	utf8RuneSelf   = 0x80
	longPathPrefix = `\\?\`
)

func walkSymlinks(path string) (string, error) {
	const maxIter = 255
	originalPath := path
	// consume path by taking each frontmost path element,
	// expanding it if it's a symlink, and appending it to b
	var b bytes.Buffer
	for n := 0; path != ""; n++ {
		if n > maxIter {
			return "", errors.New("EvalSymlinks: too many links in " + originalPath)
		}

		// A path beginning with `\\?\` represents the root, so automatically
		// skip that part and begin processing the next segment.
		if strings.HasPrefix(path, longPathPrefix) {
			b.WriteString(longPathPrefix)
			path = path[4:]
			continue
		}

		// find next path component, p
		var i = -1
		for j, c := range path {
			if c < utf8RuneSelf && os.IsPathSeparator(uint8(c)) {
				i = j
				break
			}
		}
		var p string
		if i == -1 {
			p, path = path, ""
		} else {
			p, path = path[:i], path[i+1:]
		}

		if p == "" {
			if b.Len() == 0 {
				// must be absolute path
				b.WriteRune(filepath.Separator)
			}
			continue
		}

		// If this is the first segment after the long path prefix, accept the
		// current segment as a volume root or UNC share and move on to the next.
		if b.String() == longPathPrefix {
			b.WriteString(p)
			b.WriteRune(filepath.Separator)
			continue
		}

		fi, err := os.Lstat(b.String() + p)
		if err != nil {
			return "", err
		}
		if fi.Mode()&os.ModeSymlink == 0 {
			b.WriteString(p)
			if path != "" || (b.Len() == 2 && len(p) == 2 && p[1] == ':') {
				b.WriteRune(filepath.Separator)
			}
			continue
		}

		// it's a symlink, put it at the front of path
		dest, err := os.Readlink(b.String() + p)
		if err != nil {
			return "", err
		}
		if isAbs(dest) {
			b.Reset()
		}
		path = dest + string(filepath.Separator) + path
	}
	return filepath.Clean(b.String()), nil
}

func isDriveOrRoot(p string) bool {
	if p == string(filepath.Separator) {
		return true
	}

	length := len(p)
	if length >= 2 {
		if p[length-1] == ':' && (('a' <= p[length-2] && p[length-2] <= 'z') || ('A' <= p[length-2] && p[length-2] <= 'Z')) {
			return true
		}
	}
	return false
}

// isAbs is a platform-specific wrapper for filepath.IsAbs. On Windows,
// golang filepath.IsAbs does not consider a path \windows\system32 as absolute
// as it doesn't start with a drive-letter/colon combination. However, in
// docker we need to verify things such as WORKDIR /windows/system32 in
// a Dockerfile (which gets translated to \windows\system32 when being processed
// by the daemon. This SHOULD be treated as absolute from a docker processing
// perspective.
func isAbs(path string) bool {
	if filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator)) {
		return true
	}
	return false
}
