package common

import (
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"time"

	"github.com/hashicorp/packer/packer"
)

// PackerKeyEnv is used to specify the key interval (delay) between keystrokes
// sent to the VM, typically in boot commands. This is to prevent host CPU
// utilization from causing key presses to be skipped or repeated incorrectly.
const PackerKeyEnv = "PACKER_KEY_INTERVAL"

// PackerKeyDefault 100ms is appropriate for shared build infrastructure while a
// shorter delay (e.g. 10ms) can be used on a workstation. See PackerKeyEnv.
const PackerKeyDefault = 100 * time.Millisecond

// ChooseString returns the first non-empty value.
func ChooseString(vals ...string) string {
	for _, el := range vals {
		if el != "" {
			return el
		}
	}

	return ""
}

// SupportedProtocol verifies that the url passed is actually supported or not
// This will also validate that the protocol is one that's actually implemented.
func SupportedProtocol(u *url.URL) bool {
	// url.Parse shouldn't return nil except on error....but it can.
	if u == nil {
		return false
	}

	// build a dummy NewDownloadClient since this is the only place that valid
	// protocols are actually exposed.
	cli := NewDownloadClient(&DownloadConfig{}, new(packer.NoopUi))

	// Iterate through each downloader to see if a protocol was found.
	ok := false
	for scheme := range cli.config.DownloaderMap {
		if strings.ToLower(u.Scheme) == strings.ToLower(scheme) {
			ok = true
		}
	}
	return ok
}

// DownloadableURL processes a URL that may also be a file path and returns
// a completely valid URL representing the requested file. For example,
// the original URL might be "local/file.iso" which isn't a valid URL,
// and so DownloadableURL will return "file://local/file.iso"
// No other transformations are done to the path.
func DownloadableURL(original string) (string, error) {
	var absPrefix, result string

	absPrefix = ""
	if runtime.GOOS == "windows" {
		absPrefix = "/"
	}

	// Check that the user specified a UNC path, and promote it to an smb:// uri.
	if strings.HasPrefix(original, "\\\\") && len(original) > 2 && original[2] != '?' {
		result = filepath.ToSlash(original[2:])
		return fmt.Sprintf("smb://%s", result), nil
	}

	// Fix the url if it's using bad characters commonly mistaken with a path.
	original = filepath.ToSlash(original)

	// Check to see that this is a parseable URL with a scheme and a host.
	// If so, then just pass it through.
	if u, err := url.Parse(original); err == nil && u.Scheme != "" && u.Host != "" {
		return original, nil
	}

	// If it's a file scheme, then convert it back to a regular path so the next
	// case which forces it to an absolute path, will correct it.
	if u, err := url.Parse(original); err == nil && strings.ToLower(u.Scheme) == "file" {
		original = u.Path
	}

	// If we're on Windows and we start with a slash, then this absolute path
	// is wrong. Fix it up, so the next case can figure out the absolute path.
	if rpath := strings.SplitN(original, "/", 2); rpath[0] == "" && runtime.GOOS == "windows" {
		result = rpath[1]
	} else {
		result = original
	}

	// Since we should be some kind of path (relative or absolute), check
	// that the file exists, then make it an absolute path so we can return an
	// absolute uri.
	if _, err := os.Stat(result); err == nil {
		result, err = filepath.Abs(filepath.FromSlash(result))
		if err != nil {
			return "", err
		}

		result, err = filepath.EvalSymlinks(result)
		if err != nil {
			return "", err
		}

		result = filepath.Clean(result)
		return fmt.Sprintf("file://%s%s", absPrefix, filepath.ToSlash(result)), nil
	}

	// Otherwise, check if it was originally an absolute path, and fix it if so.
	if strings.HasPrefix(original, "/") {
		return fmt.Sprintf("file://%s%s", absPrefix, result), nil
	}

	// Anything left should be a non-existent relative path. So fix it up here.
	result = filepath.ToSlash(filepath.Clean(result))
	return fmt.Sprintf("file://./%s", result), nil
}

// Force the parameter into a url. This will transform the parameter into
// a proper url, removing slashes, adding the proper prefix, etc.
func ValidatedURL(original string) (string, error) {

	// See if the user failed to give a url
	if ok, _ := regexp.MatchString("(?m)^[^[:punct:]]+://", original); !ok {

		// So since no magic was found, this must be a path.
		result, err := DownloadableURL(original)
		if err == nil {
			return ValidatedURL(result)
		}

		return "", err
	}

	// Verify that the url is parseable...just in case.
	u, err := url.Parse(original)
	if err != nil {
		return "", err
	}

	// We should now have a url, so verify that it's a protocol we support.
	if !SupportedProtocol(u) {
		return "", fmt.Errorf("Unsupported protocol scheme! (%#v)", u)
	}

	// We should now have a properly formatted and supported url
	return u.String(), nil
}

// FileExistsLocally takes the URL output from DownloadableURL, and determines
// whether it is present on the file system.
// example usage:
//
// myFile, err = common.DownloadableURL(c.SourcePath)
// ...
// fileExists := common.StatURL(myFile)
// possible output:
// true -- should occur if the file is present, or if the file is not present,
// but is not supposed to be (e.g. the schema is http://, not file://)
// false -- should occur if there was an error stating the file, so the
// file is not present when it should be.

func FileExistsLocally(original string) bool {
	// original should be something like file://C:/my/path.iso
	u, _ := url.Parse(original)

	// First create a dummy downloader so we can figure out which
	// protocol to use.
	cli := NewDownloadClient(&DownloadConfig{}, new(packer.NoopUi))
	d, ok := cli.config.DownloaderMap[u.Scheme]
	if !ok {
		return false
	}

	// Check to see that it's got a Local way of doing things.
	local, ok := d.(LocalDownloader)
	if !ok {
		return true // XXX: Remote URLs short-circuit this logic.
	}

	// Figure out where we're at.
	wd, err := os.Getwd()
	if err != nil {
		return false
	}

	// Now figure out the real path to the file.
	realpath, err := local.toPath(wd, *u)
	if err != nil {
		return false
	}

	// Finally we can seek the truth via os.Stat.
	_, err = os.Stat(realpath)
	if err != nil {
		return false
	}
	return true
}
