package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"sort"
	"strings"

	"github.com/codegangsta/cli"
)

// TODO
// - alphabetize output properties
// - stop using Sprint maybe, gets the job done for now
// - fix loop bug!

func main() {
	app := cli.NewApp()
	app.Name = "goscan"
	app.Usage = "Scan json and output go struct"

	app.Commands = []cli.Command{
		{
			Name:   "scan",
			Usage:  "Scan a local file",
			Action: CmdScan,
		},
		{
			Name:  "get",
			Usage: "Makes a GET call to given URL (expects json response)",
			Flags: []cli.Flag{
				cli.StringSliceFlag{
					Name:  "params, p",
					Value: &cli.StringSlice{},
					Usage: "URL parameters",
				},
			},
			Action: CmdGet,
		},
	}
	app.Run(os.Args)
}

func CmdScan(c *cli.Context) {}

func CmdGet(c *cli.Context) {
	var jsonInput interface{}
	var url string
	args := c.Args()
	if len(args) > 0 {
		url = args[0]
	} else {
		panic("No inputs given")
	}
	params := ""
	for _, param := range c.StringSlice("params") {
		params = fmt.Sprint(params, "&", param)
	}
	if params != "" {
		url = fmt.Sprint(url, "?", params)
	}
	fmt.Println("Calling: ", url)
	res, err := http.Get(url)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}
	err = json.Unmarshal(body, &jsonInput)
	if err != nil {
		panic(err)
	}
	fmt.Println(destruct(jsonInput.(map[string]interface{}), 0))
}

func destruct(objmap map[string]interface{}, numTabs int) string {
	output := make(map[string]string)
	for k, v := range objmap {
		switch vtyped := v.(type) {
		case string:
			output[k] = "string"
		case float64:
			output[k] = "float64"
		case bool:
			output[k] = "bool"
		case nil:
			output[k] = "<unknown>"
		case []interface{}:
			output[k] = evalJSONArray(vtyped, numTabs+1)
		case map[string]interface{}:
			output[k] = fmt.Sprint("struct{\n", destruct(vtyped, numTabs+1), int2tabs(numTabs), "}")
		default:
			panic("destruct error")
		}
	}
	return map2string(output, numTabs)
}

func evalJSONArray(arr []interface{}, numTabs int) string {
	output := ""
	overflow := false
	for _, typeValue := range arr {
		switch atyped := typeValue.(type) {
		case string:
			if output != "" && output != "string" {
				overflow = true
			} else {
				output = "string"
			}
		case float64:
			if output != "" && output != "float64" {
				overflow = true
			} else {
				output = "float64"
			}
		case bool:
			if output != "" && output != "bool" {
				overflow = true
			} else {
				output = "bool"
			}
		case map[string]interface{}:
			if output != "" && !strings.HasPrefix(output, "struct") { // TODO compare as slice (only struct part)
				overflow = true
			} else {
				// TODO since it's an array, each iteration can further identify a nil type
				output = fmt.Sprint(output, "struct{\n", destruct(atyped, numTabs), int2tabs(numTabs-1), "}")
			}
		default:
			// TODO: arrays
			panic("evalJSONArray Error")
		}
		if overflow {
			break
		}
		break
	}
	if overflow {
		output = "interface{}"
	}
	output = fmt.Sprint("[]", output)
	return output
}

func map2string(strmap map[string]string, numTabs int) string {
	output := ""
	for _, k := range alphabetizeMap(strmap) {
		output = fmt.Sprint(output, int2tabs(numTabs), key2prop(k), " ", strmap[k], "\n")
	}
	return output
}

func int2tabs(numTabs int) string {
	output := ""
	for i := 0; i < numTabs; i++ {
		output = fmt.Sprint(output, "\t")
	}
	return output
}

func key2prop(s string) string {
	s = strings.Replace(s, "_", " ", -1)
	s = strings.Title(s)
	s = strings.Replace(s, " ", "", -1)
	return s
}

func alphabetizeMap(objmap map[string]string) []string {
	keys := make([]string, 0, len(objmap))
	for key := range objmap {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func constructFile(objmap map[string]interface{}) {
	// checkErr := func(e error) {
	// 	if e != nil {
	// 		panic(e)
	// 	}
	// }
	// filename := "output.go"
	// file, err := os.Create(filename)
	// checkErr(err)
	// defer file.Close()

	// var oErr error
	// for k, v := range objmap {
	// 	switch vtype := v.(type) {
	// 	case string:
	// 		_, oErr = file.WriteString(fmt.Sprint(k, " string\n"))
	// 		checkErr(oErr)
	// 	case float64:
	// 		_, oErr = file.WriteString(fmt.Sprint(k, " float64\n"))
	// 		checkErr(oErr)
	// 	case bool:
	// 		_, oErr = file.WriteString(fmt.Sprint(k, "bool"))
	// 		checkErr(oErr)
	// 	case nil:
	// 		_, oErr = file.WriteString("TODO")
	// 	}
	// }
}
