package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math"
	"os"
	"path"
	"path/filepath"
	"time"

	"github.com/mdiluz/rove/cmd/rove/internal"
	"github.com/mdiluz/rove/pkg/version"
	"github.com/mdiluz/rove/proto/roveapi"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

var home = os.Getenv("HOME")
var defaultDataPath = path.Join(home, ".local/share/")

// Command usage
func printUsage() {
	fmt.Fprintf(os.Stderr, "Usage: rove COMMAND [ARGS...]\n")
	fmt.Fprintln(os.Stderr, "\nCommands")
	fmt.Fprintln(os.Stderr, "\tserver-status              prints the server status")
	fmt.Fprintln(os.Stderr, "\tregister NAME              registers an account and stores it (use with -name)")
	fmt.Fprintln(os.Stderr, "\tcommand COMMAND [VAL...]   issue commands to rover, accepts multiple, see below")
	fmt.Fprintln(os.Stderr, "\tradar                      gathers radar data for the current rover")
	fmt.Fprintln(os.Stderr, "\tstatus                     gets status info for current rover")
	fmt.Fprintln(os.Stderr, "\tconfig [HOST]              outputs the local config info, optionally sets host")
	fmt.Fprintln(os.Stderr, "\thelp                       outputs this usage information")
	fmt.Fprintln(os.Stderr, "\tversion                    outputs version info")
	fmt.Fprintln(os.Stderr, "\nRover commands:")
	fmt.Fprintln(os.Stderr, "\ttoggle                     toggles the sails, either catching the wind, or charging from the sun")
	fmt.Fprintln(os.Stderr, "\tstash                      stores the object at the rover location in the inventory")
	fmt.Fprintln(os.Stderr, "\trepair                     uses an inventory object to repair the rover")
	fmt.Fprintln(os.Stderr, "\tbroadcast MSG              broadcast a simple ASCII triplet to nearby rovers")
	fmt.Fprintln(os.Stderr, "\tsalvage                    salvages a dormant rover for parts")
	fmt.Fprintln(os.Stderr, "\nEnvironment")
	fmt.Fprintln(os.Stderr, "\tROVE_USER_DATA             path to user data, defaults to "+defaultDataPath)
}

const gRPCport = 9090

// Account stores data for an account
type Account struct {
	Name   string
	Secret string
}

// Config is used to store internal data
type Config struct {
	Host    string
	Account Account
}

// ConfigPath returns the configuration path
func ConfigPath() string {
	// Allow overriding the data path
	var datapath = defaultDataPath
	var override = os.Getenv("ROVE_USER_DATA")
	if len(override) > 0 {
		datapath = override
	}
	datapath = path.Join(datapath, "roveapi.json")

	return datapath
}

// LoadConfig loads the config from a chosen path
func LoadConfig() (config Config, err error) {

	datapath := ConfigPath()

	// Create the path if needed
	path := filepath.Dir(datapath)
	if _, err := os.Stat(path); os.IsNotExist(err) {
		if err := os.MkdirAll(path, os.ModePerm); err != nil {
			return Config{}, fmt.Errorf("Failed to create data path %s: %s", path, err)
		}
	} else {
		// Read the file
		_, err = os.Stat(datapath)
		if !os.IsNotExist(err) {
			if b, err := ioutil.ReadFile(datapath); err != nil {
				return Config{}, fmt.Errorf("failed to read file %s error: %s", datapath, err)

			} else if len(b) == 0 {
				return Config{}, fmt.Errorf("file %s was empty, assumin fresh data", datapath)

			} else if err := json.Unmarshal(b, &config); err != nil {
				return Config{}, fmt.Errorf("failed to unmarshal file %s error: %s", datapath, err)

			}
		}
	}

	return
}

// SaveConfig saves the config out
func SaveConfig(config Config) error {
	// Save out the persistent file
	datapath := ConfigPath()
	if b, err := json.MarshalIndent(config, "", "\t"); err != nil {
		return fmt.Errorf("failed to marshal data error: %s", err)
	} else if err := ioutil.WriteFile(datapath, b, os.ModePerm); err != nil {
		return fmt.Errorf("failed to save file %s error: %s", datapath, err)
	}

	return nil
}

// checkAccount will verify an account ID
func checkAccount(a Account) error {
	if len(a.Name) == 0 {
		return fmt.Errorf("no account ID set, must register first")
	} else if len(a.Secret) == 0 {
		return fmt.Errorf("empty account secret, must register first")
	}
	return nil
}

// BearingFromString converts a string to a bearing
func BearingFromString(s string) roveapi.Bearing {
	switch s {
	case "N":
		return roveapi.Bearing_North
	case "NE":
		return roveapi.Bearing_NorthEast
	case "E":
		return roveapi.Bearing_East
	case "SE":
		return roveapi.Bearing_SouthEast
	case "S":
		return roveapi.Bearing_South
	case "SW":
		return roveapi.Bearing_SouthWest
	case "W":
		return roveapi.Bearing_West
	case "NW":
		return roveapi.Bearing_NorthWest
	}
	return roveapi.Bearing_BearingUnknown
}

// InnerMain wraps the main function so we can test it
func InnerMain(command string, args ...string) error {

	// Early simple bails
	switch command {
	case "help":
		printUsage()
		return nil
	case "version":
		fmt.Println(version.Version)
		return nil
	}

	// Load in the persistent file
	config, err := LoadConfig()
	if err != nil {
		return err
	}

	// Run config command before server needed
	if command == "config" {
		if len(args) > 0 {
			config.Host = args[0]
		}
		fmt.Printf("host: %s\taccount: %s\n", config.Host, config.Account)
		return SaveConfig(config)
	}

	// If there's still no host, bail
	if len(config.Host) == 0 {
		return fmt.Errorf("no host set in %s, set one with '%s config {HOST}'", ConfigPath(), os.Args[0])
	}

	// Set up the server
	clientConn, err := grpc.Dial(fmt.Sprintf("%s:%d", config.Host, gRPCport), grpc.WithInsecure())
	if err != nil {
		return err
	}
	var client = roveapi.NewRoveClient(clientConn)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Handle all the commands
	switch command {
	case "server-status":
		response, err := client.ServerStatus(ctx, &roveapi.ServerStatusRequest{})
		switch {
		case err != nil:
			return err

		default:
			fmt.Printf("Ready: %t\n", response.Ready)
			fmt.Printf("Version: %s\n", response.Version)
			fmt.Printf("Tick Rate: %d\n", response.TickRate)
			fmt.Printf("Current Tick: %d\n", response.CurrentTick)
			fmt.Printf("Next Tick: %s\n", response.NextTick)
		}

	case "register":
		if len(args) == 0 || len(args[0]) == 0 {
			return fmt.Errorf("must pass name to 'register'")
		}

		resp, err := client.Register(ctx, &roveapi.RegisterRequest{
			Name: args[0],
		})
		switch {
		case err != nil:
			return err

		default:
			fmt.Printf("Registered account with id: %s\n", resp.Account.Name)
			config.Account.Name = resp.Account.Name
			config.Account.Secret = resp.Account.Secret
		}

	case "command":
		if err := checkAccount(config.Account); err != nil {
			return err
		} else if len(args) == 0 {
			return fmt.Errorf("must pass commands to 'commands'")
		}

		// Iterate through each command
		var commands []*roveapi.Command
		for i := 0; i < len(args); i++ {
			switch args[i] {
			case "turn":
				i++
				if len(args) == i {
					return fmt.Errorf("turn command must be passed a compass bearing")
				}
				b := BearingFromString(args[i])
				if b == roveapi.Bearing_BearingUnknown {
					return fmt.Errorf("turn command must be given a valid bearing %s", args[i])
				}
				commands = append(commands,
					&roveapi.Command{
						Command: roveapi.CommandType_turn,
						Bearing: b,
					},
				)
			case "broadcast":
				i++
				if len(args) == i {
					return fmt.Errorf("broadcast command must be passed an ASCII triplet")
				} else if len(args[i]) > 3 {
					return fmt.Errorf("broadcast command must be given ASCII triplet of 3 or less: %s", args[i])
				}
				commands = append(commands,
					&roveapi.Command{
						Command: roveapi.CommandType_broadcast,
						Data:    []byte(args[i]),
					},
				)
			default:
				// By default just use the command literally
				commands = append(commands,
					&roveapi.Command{
						Command: roveapi.CommandType(roveapi.CommandType_value[args[i]]),
					},
				)
			}
		}

		_, err := client.Command(ctx, &roveapi.CommandRequest{
			Account: &roveapi.Account{
				Name:   config.Account.Name,
				Secret: config.Account.Secret,
			},
			Commands: commands,
		})

		switch {
		case err != nil:
			return err

		default:
			fmt.Printf("Request succeeded\n")
		}

	case "radar":
		if err := checkAccount(config.Account); err != nil {
			return err
		}

		response, err := client.Radar(ctx, &roveapi.RadarRequest{
			Account: &roveapi.Account{
				Name:   config.Account.Name,
				Secret: config.Account.Secret,
			},
		})

		switch {
		case err != nil:
			return err

		default:

			// Print out the radar
			num := int(math.Sqrt(float64(len(response.Tiles))))
			for j := num - 1; j >= 0; j-- {
				for i := 0; i < num; i++ {
					t := response.Tiles[i+num*j]
					o := response.Objects[i+num*j]
					if o != roveapi.Object_ObjectUnknown {
						fmt.Printf("%c", internal.ObjectGlyph(o))
					} else {
						fmt.Printf("%c", internal.TileGlyph(t))
					}

				}
				fmt.Print("\n")
			}
		}

	case "status":
		if err := checkAccount(config.Account); err != nil {
			return err
		}

		response, err := client.Status(ctx, &roveapi.StatusRequest{
			Account: &roveapi.Account{
				Name:   config.Account.Name,
				Secret: config.Account.Secret,
			},
		})

		switch {
		case err != nil:
			return err

		default:
			fmt.Printf("rover info: %+v\n", response)
		}

	default:
		// Print the usage
		fmt.Fprintf(os.Stderr, "Error: unknown command %s\n", command)
		printUsage()
		os.Exit(1)
	}

	return SaveConfig(config)
}

// Simple main
func main() {
	// Bail without any args
	if len(os.Args) == 1 {
		printUsage()
		os.Exit(1)
	}

	// Run the inner main
	if err := InnerMain(os.Args[1], os.Args[2:]...); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %s\n", err)
		os.Exit(1)
	}
}
