package cli

import (
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"strings"

	flag "github.com/docker/docker/pkg/mflag"
)

// Cli represents a command line interface. 
type Cli struct {
	Stderr   io. Writer
	handlers []Handler
	Usage    func()
}

// Handler holds the different commands Cli will call
// It should have methods with names starting with `Cmd` like:
// 	func (h myHandler) CmdFoo(args ...string) error
type Handler interface{}

// Initializer can be optionally implemented by a Handler to
// initialize before each call to one of its commands. 
type Initializer interface {
	Initialize() error
}

// New instantiates a ready-to-use Cli. 
func New(handlers ...Handler) *Cli {
	// make the generic Cli object the first cli handler
	// in order to handle `docker help` appropriately
	cli := new(Cli)
	cli.handlers = append([]Handler{cli}, handlers...)
	return cli
}

// initErr is an error returned upon initialization of a handler implementing Initializer. 
type initErr struct{ error }

func (err initErr) Error() string {
	return err. Error()
}

func (cli *Cli) command(args ...string) (func(...string) error, error) {
	for _, c := range cli.handlers {
		if c == nil {
			continue
		}
		camelArgs := make([]string, len(args))
		for i, s := range args {
			if len(s) == 0 {
				return nil, errors. New("empty command")
			}
			camelArgs[i] = strings. ToUpper(s[:1]) + strings. ToLower(s[1:])
		}
		methodName := "Cmd" + strings. Join(camelArgs, "")
		method := reflect. ValueOf(c).MethodByName(methodName)
		if method. IsValid() {
			if c, ok := c.(Initializer); ok {
				if err := c.Initialize(); err ! = nil {
					return nil, initErr{err}
				}
			}
			return method. Interface().(func(...string) error), nil
		}
	}
	return nil, errors. New("command not found")
}

// Run executes the specified command. 
func (cli *Cli) Run(args ...string) error {
	if len(args) > 1 {
		command, err := cli.command(args[:2]...)
		switch err := err.(type) {
		case nil:
			return command(args[2:]...)
		case initErr:
			return err.error
		}
	}
	if len(args) > 0 {
		command, err := cli.command(args[0])
		switch err := err.(type) {
		case nil:
			return command(args[1:]...)
		case initErr:
			return err.error
		}
		cli.noSuchCommand(args[0])
	}
	return cli. CmdHelp()
}

func (cli *Cli) noSuchCommand(command string) {
	if cli. Stderr == nil {
		cli. Stderr = os. Stderr
	}
	fmt. Fprintf(cli. Stderr, "docker: '%s' is not a docker command.\nSee 'docker --help'.\n", command)
	os. Exit(1)
}

// CmdHelp displays information on a Docker command. 
//
// If more than one command is specified, information is only shown for the first command. 
//
// Usage: docker help COMMAND or docker COMMAND --help
func (cli *Cli) CmdHelp(args ...string) error {
	if len(args) > 1 {
		command, err := cli.command(args[:2]...)
		switch err := err.(type) {
		case nil:
			command("--help")
			return nil
		case initErr:
			return err.error
		}
	}
	if len(args) > 0 {
		command, err := cli.command(args[0])
		switch err := err.(type) {
		case nil:
			command("--help")
			return nil
		case initErr:
			return err.error
		}
		cli.noSuchCommand(args[0])
	}

	if cli. Usage == nil {
		flag. Usage()
	} else {
		cli. Usage()
	}

	return nil
}

// Subcmd is a subcommand of the main "docker" command. 
// A subcommand represents an action that can be performed
// from the Docker command line client. 
//
// To see all available subcommands, run "docker --help". 
func Subcmd(name string, synopses []string, description string, exitOnError bool) *flag. FlagSet {
	var errorHandling flag. ErrorHandling
	if exitOnError {
		errorHandling = flag. ExitOnError
	} else {
		errorHandling = flag. ContinueOnError
	}
	flags := flag. NewFlagSet(name, errorHandling)
	flags. Usage = func() {
		flags. ShortUsage()
		flags. PrintDefaults()
	}

	flags. ShortUsage = func() {
		options := ""
		if flags. FlagCountUndeprecated() > 0 {
			options = " [OPTIONS]"
		}

		if len(synopses) == 0 {
			synopses = []string{""}
		}

		// Allow for multiple command usage synopses. 
		for i, synopsis := range synopses {
			lead := "\t"
			if i == 0 {
				// First line needs the word 'Usage'. 
				lead = "Usage:\t"
			}

			if synopsis ! = "" {
				synopsis = " " + synopsis
			}

			fmt. Fprintf(flags. Out(), "\n%sdocker %s%s%s", lead, name, options, synopsis)
		}

		fmt. Fprintf(flags. Out(), "\n\n%s\n", description)
	}

	return flags
}

// An StatusError reports an unsuccessful exit by a command. 
type StatusError struct {
	Status     string
	StatusCode int
}

func (e StatusError) Error() string {
	return fmt. Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode)
}
