package golang

import (
	"bytes"
	"fmt"
	"log"
	"reflect"
	"regexp"
	"sort"
	"strings"
	"text/template"

	"github.com/brutella/hap/characteristic"
	"github.com/brutella/hap/gen"
)

// CharStructTemplate is template for a characteristic struct.
const CharStructTemplate = `package characteristic
// THIS FILE IS AUTO-GENERATED

{{if .HasConsts}}
const (
    {{range .Consts}}
    {{.Identifier}} {{.TypeName}} = {{.Value}}{{end}}
)
{{end}}

const {{.TypeName}} = "{{.TypeValue}}"

type {{.StructName}} struct {
    *{{.EmbeddedStructName}}
}

func New{{.StructName}}() *{{.StructName}} {
    c := New{{.EmbeddedStructName}}({{.TypeName}})
    c.Format = {{.FormatTypeName}}
    c.Permissions = {{.Permissions}}
    {{if .HasMinValue}}c.SetMinValue({{.MinValue}}){{end}}
    {{if .HasMaxValue}}c.SetMaxValue({{.MaxValue}}){{end}}
    {{if .HasStepValue}}c.SetStepValue({{.StepValue}}){{end}}
    {{if .HasDefaultValue}}c.SetValue({{.DefaultValue}}){{end}}
    {{if .UnitName}}c.Unit = {{.UnitName}}{{end}}
    
	return &{{.StructName}}{c}
}`

// Characteristic holds characteristic template data
type Characteristic struct {
	Name               string      // Name of the characteristic
	EmbeddedStructName string      // Name of the embedded struct (e.g. Int)
	FormatTypeName     string      // Name of the format type (e.g. FormatInt32)
	StructName         string      // Name of the struct (e.g. Brightness)
	FileName           string      // Name of the file (e.g. brightness.go)
	LocalFilePath      string      // Path to the file (e.g. ~/User/Go/src/github.com/brutella/hap/characteristic/brightness.go)
	RelFilePath        string      // Relative path to the file from the project root (e.g. characteristic/brightness.go)
	Permissions        string      // Permissions declaration (e.g. []string{PermissionRead, PermissionWrite, PermissionEvents})
	TypeName           string      // Name of type e.g. TypeBrightness
	TypeValue          string      // Value of the type e.g. 00000008-0000-1000-8000-0026BB765291
	DefaultValue       interface{} // e.g. 0
	MinValue           interface{} // e.g. 0
	MaxValue           interface{} // e.g. 100
	StepValue          interface{} // e.g. 1
	UnitName           string      // Name of the unit e.g. UnitPercentage

	Consts []ConstDecl
}

func NewCharacteristic(char *gen.CharacteristicMetadata) *Characteristic {
	data := Characteristic{
		Name:               char.Name,
		EmbeddedStructName: embeddedStructNames[char.Format],
		FormatTypeName:     formatConstants[char.Format],
		StructName:         structName(char),
		FileName:           CharacteristicFileName(char),
		LocalFilePath:      CharacteristicLocalFilePath(char),
		RelFilePath:        CharacteristicRelativeFilePath(char),
		Permissions:        permissions(char),
		TypeName:           typeName(char),
		TypeValue:          minifyUUID(char.UUID),
		DefaultValue:       defaultValue(char),
		MinValue:           minValue(char),
		MaxValue:           maxValue(char),
		StepValue:          stepValue(char),
		UnitName:           unitName(char),
		Consts:             constDecls(char),
	}

	return &data
}

// HasMinValue returns true if characteristic has a min value
func (d Characteristic) HasMinValue() bool {
	return d.MinValue != nil
}

// HasMaxValue returns true if characteristic has a max value
func (d Characteristic) HasMaxValue() bool {
	return d.MaxValue != nil
}

// HasStepValue returns true if characteristic has a step value
func (d Characteristic) HasStepValue() bool {
	return d.StepValue != nil
}

// HasConsts returns true if characteristic has const declarations
func (d Characteristic) HasConsts() bool {
	return len(d.Consts) > 0
}

// HasDefaultValue returns true if characteristic has a default value
func (d Characteristic) HasDefaultValue() bool {
	return d.DefaultValue != nil
}

// ConstDecl is a constant declaration
type ConstDecl struct {
	Identifier string
	TypeName   string
	Value      interface{}
}

// ByValue defines a type for sorting const declarations by value by Impelementing the sort.Interface interface
type ByValue []ConstDecl

func (v ByValue) Len() int {
	return len(v)
}

func (v ByValue) Less(i, j int) bool {
	iValue, iOK := v[i].Value.(string)
	jValue, jOK := v[j].Value.(string)

	if iOK == false {
		log.Fatalf("Expected string, got %v", reflect.TypeOf(v[i].Value))
	}
	if jOK == false {
		log.Fatalf("Expected string, got %v", reflect.TypeOf(v[j].Value))
	}

	return iValue < jValue
}

func (v ByValue) Swap(i, j int) {
	v[i], v[j] = v[j], v[i]
}

// FileName returns the filename for a characteristic
func CharacteristicFileName(char *gen.CharacteristicMetadata) string {
	return fmt.Sprintf("%s.go", underscored(char.Name))
}

// CharacteristicGoCode returns the o code for a characteristic file
func CharacteristicGoCode(char *gen.CharacteristicMetadata) ([]byte, error) {
	var err error
	var buf bytes.Buffer

	data := NewCharacteristic(char)

	t := template.New("Test Template")

	t, err = t.Parse(CharStructTemplate)
	t.Execute(&buf, data)

	return buf.Bytes(), err
}

var formatConstants = map[string]string{
	"string": "FormatString",
	"bool":   "FormatBool",
	"float":  "FormatFloat",
	"uint8":  "FormatUInt8",
	"uint16": "FormatUInt16",
	"uint32": "FormatUInt32",
	"int32":  "FormatInt32",
	"uint64": "FormatUInt64",
	"tlv8":   "FormatTLV8",
}

var constTypes = map[string]string{
	"string": "string",
	"bool":   "bool",
	"float":  "float",
	"uint8":  "int",
	"uint16": "int",
	"uint32": "int",
	"int32":  "int",
	"uint64": "int",
	"tlv8":   "[]byte",
}

var embeddedStructNames = map[string]string{
	"string": "String",
	"bool":   "Bool",
	"float":  "Float",
	"uint8":  "Int",
	"uint16": "Int",
	"uint32": "Int",
	"int32":  "Int",
	"tlv8":   "Bytes",
}

// isReadable returns true the characteristic contains the readable property
func isReadable(char *gen.CharacteristicMetadata) bool {
	for _, perm := range char.Properties {
		if perm == "read" {
			return true
		}
	}

	return false
}

// defaultValue returns the default value of a characteristic, based on the characteristic format and properties (readable)
func defaultValue(char *gen.CharacteristicMetadata) interface{} {
	if isReadable(char) == false {
		return nil
	}

	switch char.Format {
	case "string":
		return `""`
	case "bool":
		return "false"
	case "int", "float", "uint8", "uint16", "uint32", "int32", "uint64":
		if min := minValue(char); min != nil {
			return min
		}

		return 0
	case "tlv8":
		return "[]byte{}"
	default:
		break
	}

	return nil
}

// minifyUUID returns a minified version of s by removing unneeded characters.
// For example the UUID "0000008C-0000-1000-8000-0026BB765291" the Window Covering
// service will be minified to "8C".
func minifyUUID(s string) string {
	authRegexp := regexp.MustCompile(`^([0-9a-fA-F]*)`)
	if str := authRegexp.FindString(s); len(str) > 0 {
		return strings.TrimLeft(str, "0")
	}

	return s
}

// Return the name of the characteristic type name
func typeName(char *gen.CharacteristicMetadata) string {
	return "Type" + camelCased(char.Name)
}

func structName(char *gen.CharacteristicMetadata) string {
	return camelCased(char.Name)
}

// strip removes any leading and trailing white spaces, and make the following substitutions: "." => "_", ","|"-"|"("|")" => "" (empty string)
func strip(s string) string {
	trimmed := strings.TrimSpace(s)

	r := strings.NewReplacer(".", "_", ",", "", "-", "", "(", "", ")", "")
	return r.Replace(trimmed)
}

func underscored(s string) string {
	lowered := strings.ToLower(strip(s))
	return strings.Replace(lowered, " ", "_", -1)
}

func camelCased(s string) string {
	lowered := strings.Title(strip(s))
	return strings.Replace(lowered, " ", "", -1)
}

func permissions(char *gen.CharacteristicMetadata) string {
	var perms []string
	for _, perm := range char.Properties {
		switch perm {
		case "read":
			perms = append(perms, "PermissionRead")
		case "write":
			perms = append(perms, "PermissionWrite")
		case "cnotify":
			perms = append(perms, "PermissionEvents")
		case "uncnotify":
			// TODO(mah)
			break
		default:
			log.Panic(fmt.Sprintf("Undefined characteristic permission %s", perm))
		}
	}

	return "[]string{" + strings.Join(perms, ",") + "}"
}

func unitName(char *gen.CharacteristicMetadata) string {
	if len(char.Unit) == 0 {
		return ""
	}

	switch char.Unit {
	case characteristic.UnitPercentage:
		return "UnitPercentage"
	case characteristic.UnitArcDegrees:
		return "UnitArcDegrees"
	case characteristic.UnitCelsius:
		return "UnitCelsius"
	case characteristic.UnitLux:
		return "UnitLux"
	case characteristic.UnitSeconds:
		return "UnitSeconds"
	default:
		log.Fatalf("Could not find unit name for %s\n", char.Unit)
	}

	return ""
}

func constraints(char *gen.CharacteristicMetadata) map[string]interface{} {
	if char.Constraints != nil {
		if constraints, ok := char.Constraints.(map[string]interface{}); ok == true {
			return constraints
		}
	}
	return nil
}

func constraintWithKey(char *gen.CharacteristicMetadata, key string) interface{} {
	if constr := constraints(char); constr != nil {
		return constr[key]
	}
	return nil
}

func constrainedValues(char *gen.CharacteristicMetadata) map[string]interface{} {
	if values := constraintWithKey(char, "ValidValues"); values != nil {
		return values.(map[string]interface{})
	}

	return nil
}

func minValue(char *gen.CharacteristicMetadata) interface{} {
	return constraintWithKey(char, "MinimumValue")
}

func maxValue(char *gen.CharacteristicMetadata) interface{} {
	return constraintWithKey(char, "MaximumValue")
}

func stepValue(char *gen.CharacteristicMetadata) interface{} {
	return constraintWithKey(char, "StepValue")
}

func constDecls(char *gen.CharacteristicMetadata) []ConstDecl {
	if values := constrainedValues(char); values != nil {
		name := camelCased(char.Name)
		typ := constTypes[char.Format]

		var decls []ConstDecl
		for key, value := range values {
			str := fmt.Sprintf("%s", value)

			c := ConstDecl{name + camelCased(str), typ, key}
			decls = append(decls, c)
		}

		// Sort by value in ascending order
		sort.Sort(ByValue(decls))
		return decls
	}

	return nil
}
