// Copyright (c) 2015, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"mvdan.cc/fdroidcl/basedir"
)

const cmdName = "fdroidcl"

const version = "v0.7.0"

func subdir(dir, name string) string {
	p := filepath.Join(dir, name)
	if err := os.MkdirAll(p, 0o755); err != nil {
		fmt.Fprintf(os.Stderr, "Could not create dir '%s': %v\n", p, err)
	}
	return p
}

func mustCache() string {
	dir, err := os.UserCacheDir()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		panic("TODO: return an error")
	}
	return subdir(dir, cmdName)
}

func mustData() string {
	dir := basedir.Data()
	if dir == "" {
		fmt.Fprintln(os.Stderr, "Could not determine data dir")
		panic("TODO: return an error")
	}
	return subdir(dir, cmdName)
}

func configPath() string {
	return filepath.Join(mustData(), "config.json")
}

type repo struct {
	ID      string `json:"id"`
	URL     string `json:"url"`
	Enabled bool   `json:"enabled"`
}

type userConfig struct {
	Repos []repo `json:"repos"`
}

var config = userConfig{
	Repos: []repo{
		{
			ID:      "f-droid",
			URL:     "https://f-droid.org/repo",
			Enabled: true,
		},
		{
			ID:      "f-droid-archive",
			URL:     "https://f-droid.org/archive",
			Enabled: false,
		},
	},
}

func readConfig() error {
	f, err := os.Open(configPath())
	if err != nil {
		// ignore error, if file does not exist
		return nil
	}
	defer f.Close()
	fileConfig := userConfig{}
	err = json.NewDecoder(f).Decode(&fileConfig)
	if err != nil {
		return err
	}
	config = fileConfig
	return nil
}

// A Command is an implementation of a go command
// like go build or go fix.
type Command struct {
	// Run runs the command.
	// The args are the arguments after the command name.
	Run func(args []string) error

	// UsageLine is the one-line usage message.
	// The first word in the line is taken to be the command name.
	UsageLine string

	// Short is the short, single-line description.
	Short string

	// Long is an optional longer version of the Short description.
	Long string

	Fset flag.FlagSet
}

// Name returns the command's name: the first word in the usage line.
func (c *Command) Name() string {
	name := c.UsageLine
	i := strings.Index(name, " ")
	if i >= 0 {
		name = name[:i]
	}
	return name
}

func (c *Command) usage() {
	fmt.Fprintf(os.Stderr, "usage: %s %s\n\n", cmdName, c.UsageLine)
	if c.Long == "" {
		fmt.Fprintf(os.Stderr, "%s.\n", c.Short)
	} else {
		fmt.Fprint(os.Stderr, c.Long)
	}
	anyFlags := false
	c.Fset.VisitAll(func(f *flag.Flag) { anyFlags = true })
	if anyFlags {
		fmt.Fprintf(os.Stderr, "\nAvailable options:\n")
		c.Fset.PrintDefaults()
	}
}

func init() {
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, "usage: %s [-h] <command> [<args>]\n\n", cmdName)
		fmt.Fprintf(os.Stderr, "Available commands:\n")
		maxUsageLen := 0
		for _, c := range commands {
			if len(c.UsageLine) > maxUsageLen {
				maxUsageLen = len(c.UsageLine)
			}
		}
		for _, c := range commands {
			fmt.Fprintf(os.Stderr, "   %s%s  %s\n", c.UsageLine,
				strings.Repeat(" ", maxUsageLen-len(c.UsageLine)), c.Short)
		}
		fmt.Fprintf(os.Stderr, `
An appid is just an app's unique package name. A specific version of an app can
be selected by following the appid with a colon and the version code. The
'search' and 'show' commands can be used to find these strings. For example:

	$ fdroidcl search redreader
	$ fdroidcl show org.quantumbadger.redreader
	$ fdroidcl install org.quantumbadger.redreader:85
`)
		fmt.Fprintf(os.Stderr, "\nUse %s <command> -h for more information.\n", cmdName)
	}
}

// Commands lists the available commands.
var commands = []*Command{
	cmdUpdate,
	cmdSearch,
	cmdShow,
	cmdInstall,
	cmdUninstall,
	cmdDownload,
	cmdDevices,
	cmdList,
	cmdRepo,
	cmdClean,
	cmdDefaults,
	cmdVersion,
}

var cmdVersion = &Command{
	UsageLine: "version",
	Short:     "Print version information",
	Run: func(args []string) error {
		if len(args) > 0 {
			return fmt.Errorf("no arguments allowed")
		}
		fmt.Println(version)
		return nil
	},
}

func main() {
	os.Exit(main1())
}

func main1() int {
	flag.Parse()
	args := flag.Args()

	if len(args) < 1 {
		flag.Usage()
		return 2
	}

	cmdName := args[0]
	for _, cmd := range commands {
		if cmd.Name() != cmdName {
			continue
		}
		cmd.Fset.Init(cmdName, flag.ContinueOnError)
		cmd.Fset.Usage = cmd.usage
		if err := cmd.Fset.Parse(args[1:]); err != nil {
			if err != flag.ErrHelp {
				fmt.Fprintf(os.Stderr, "flag: %v\n", err)
				cmd.Fset.Usage()
			}
			return 2
		}

		err := readConfig()
		if err != nil {
			fmt.Fprintf(os.Stderr, "config %s: %v\n", configPath(), err)
			return 1
		}

		if err := cmd.Run(cmd.Fset.Args()); err != nil {
			fmt.Fprintf(os.Stderr, "%s: %v\n", cmdName, err)
			return 1
		}
		return 0
	}
	fmt.Fprintf(os.Stderr, "Unrecognised command '%s'\n\n", cmdName)
	flag.Usage()
	return 2
}
