// Package webbrowser provides a simple API for opening web pages on your
// default browser.
package webbrowser

import (
	"errors"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"runtime"
	"strings"
)

var (
	ErrCantOpenBrowser = errors.New("webbrowser: can't open browser")
	ErrNoCandidates    = errors.New("webbrowser: no browser candidate found for your OS")
)

// Candidates contains a list of registered `Browser`s that will be tried with Open.
var Candidates []Browser

type Browser interface {
	// Command returns a ready to be used Cmd that will open an URL.
	Command(string) (*exec.Cmd, error)
	// Open tries to open a URL in your default browser. NOTE: This may cause
	// your program to hang until the browser process is closed in some OSes,
	// see https://github.com/toqueteos/webbrowser/issues/4.
	Open(string) error
}

// Open tries to open a URL in your default browser ensuring you have a display
// set up and not running this from SSH. NOTE: This may cause your program to
// hang until the browser process is closed in some OSes, see
// https://github.com/toqueteos/webbrowser/issues/4.
func Open(s string) (err error) {
	if len(Candidates) == 0 {
		return ErrNoCandidates
	}

	// Try to determine if there's a display available (only linux) and we
	// aren't on a terminal (all but windows).
	switch runtime.GOOS {
	case "linux":
		// No display, no need to open a browser. Lynx users **MAY** have
		// something to say about this.
		if os.Getenv("DISPLAY") == "" {
			return fmt.Errorf("webbrowser: tried to open %q, no screen found", s)
		}
		fallthrough
	case "darwin":
		// Check SSH env vars.
		if os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" {
			return fmt.Errorf("webbrowser: tried to open %q, but you are running a shell session", s)
		}
	}

	// Try all candidates
	for _, candidate := range Candidates {
		err := candidate.Open(s)
		if err == nil {
			return nil
		}
	}

	return ErrCantOpenBrowser
}

func init() {
	// Register the default Browser for current OS, if it exists.
	if os, ok := osCommand[runtime.GOOS]; ok {
		Candidates = append(Candidates, browserCommand{os.cmd, os.args})
	}
}

var (
	osCommand = map[string]*browserCommand{
		"android": &browserCommand{"xdg-open", nil},
		"darwin":  &browserCommand{"open", nil},
		"freebsd": &browserCommand{"xdg-open", nil},
		"linux":   &browserCommand{"xdg-open", nil},
		"netbsd":  &browserCommand{"xdg-open", nil},
		"openbsd": &browserCommand{"xdg-open", nil}, // It may be open instead
		"windows": &browserCommand{"cmd", []string{"/c", "start"}},
	}
	winSchemes = [3]string{"https", "http", "file"}
)

type browserCommand struct {
	cmd  string
	args []string
}

func (b browserCommand) Command(s string) (*exec.Cmd, error) {
	u, err := url.Parse(s)
	if err != nil {
		return nil, err
	}

	validUrl := ensureValidURL(u)

	b.args = append(b.args, validUrl)

	return exec.Command(b.cmd, b.args...), nil
}

func (b browserCommand) Open(s string) error {
	cmd, err := b.Command(s)
	if err != nil {
		return err
	}

	return cmd.Run()
}

func ensureScheme(u *url.URL) {
	for _, s := range winSchemes {
		if u.Scheme == s {
			return
		}
	}
	u.Scheme = "http"
}

func ensureValidURL(u *url.URL) string {
	// Enforce a scheme (windows requires scheme to be set to work properly).
	ensureScheme(u)
	s := u.String()

	// Escape characters not allowed by cmd/bash
	switch runtime.GOOS {
	case "windows":
		s = strings.Replace(s, "&", `^&`, -1)
	}

	return s
}