package bpdoc

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/doc"
	"go/parser"
	"go/token"
	"io/ioutil"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync"
	"text/template"

	"github.com/google/blueprint"
	"github.com/google/blueprint/proptools"
)

type DocCollector struct {
	pkgFiles map[string][]string // Map of package name to source files, provided by constructor

	mutex   sync.Mutex
	pkgDocs map[string]*doc.Package        // Map of package name to parsed Go AST, protected by mutex
	docs    map[string]*PropertyStructDocs // Map of type name to docs, protected by mutex
}

func NewDocCollector(pkgFiles map[string][]string) *DocCollector {
	return &DocCollector{
		pkgFiles: pkgFiles,
		pkgDocs:  make(map[string]*doc.Package),
		docs:     make(map[string]*PropertyStructDocs),
	}
}

// Return the PropertyStructDocs associated with a property struct type.  The type should be in the
// format <package path>.<type name>
func (dc *DocCollector) Docs(pkg, name string, defaults reflect.Value) (*PropertyStructDocs, error) {
	docs := dc.getDocs(pkg, name)

	if docs == nil {
		pkgDocs, err := dc.packageDocs(pkg)
		if err != nil {
			return nil, err
		}

		for _, t := range pkgDocs.Types {
			if t.Name == name {
				docs, err = newDocs(t)
				if err != nil {
					return nil, err
				}
				docs = dc.putDocs(pkg, name, docs)
			}
		}
	}

	if docs == nil {
		return nil, fmt.Errorf("package %q type %q not found", pkg, name)
	}

	docs = docs.Clone()
	docs.SetDefaults(defaults)

	return docs, nil
}

func (dc *DocCollector) getDocs(pkg, name string) *PropertyStructDocs {
	dc.mutex.Lock()
	defer dc.mutex.Unlock()

	name = pkg + "." + name

	return dc.docs[name]
}

func (dc *DocCollector) putDocs(pkg, name string, docs *PropertyStructDocs) *PropertyStructDocs {
	dc.mutex.Lock()
	defer dc.mutex.Unlock()

	name = pkg + "." + name

	if dc.docs[name] != nil {
		return dc.docs[name]
	} else {
		dc.docs[name] = docs
		return docs
	}
}

type PropertyStructDocs struct {
	Name       string
	Text       string
	Properties []PropertyDocs
}

type PropertyDocs struct {
	Name       string
	OtherNames []string
	Type       string
	Tag        reflect.StructTag
	Text       string
	OtherTexts []string
	Properties []PropertyDocs
	Default    string
}

func (docs *PropertyStructDocs) Clone() *PropertyStructDocs {
	ret := *docs
	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
	for i, prop := range ret.Properties {
		ret.Properties[i] = prop.Clone()
	}

	return &ret
}

func (docs *PropertyDocs) Clone() PropertyDocs {
	ret := *docs
	ret.Properties = append([]PropertyDocs(nil), ret.Properties...)
	for i, prop := range ret.Properties {
		ret.Properties[i] = prop.Clone()
	}

	return ret
}

func (docs *PropertyDocs) Equal(other PropertyDocs) bool {
	return docs.Name == other.Name && docs.Type == other.Type && docs.Tag == other.Tag &&
		docs.Text == other.Text && docs.Default == other.Default &&
		stringArrayEqual(docs.OtherNames, other.OtherNames) &&
		stringArrayEqual(docs.OtherTexts, other.OtherTexts) &&
		docs.SameSubProperties(other)
}

func (docs *PropertyStructDocs) SetDefaults(defaults reflect.Value) {
	setDefaults(docs.Properties, defaults)
}

func setDefaults(properties []PropertyDocs, defaults reflect.Value) {
	for i := range properties {
		prop := &properties[i]
		fieldName := proptools.FieldNameForProperty(prop.Name)
		f := defaults.FieldByName(fieldName)
		if (f == reflect.Value{}) {
			panic(fmt.Errorf("property %q does not exist in %q", fieldName, defaults.Type()))
		}

		if reflect.DeepEqual(f.Interface(), reflect.Zero(f.Type()).Interface()) {
			continue
		}

		if f.Type().Kind() == reflect.Interface {
			f = f.Elem()
		}

		if f.Type().Kind() == reflect.Ptr {
			f = f.Elem()
		}

		if f.Type().Kind() == reflect.Struct {
			setDefaults(prop.Properties, f)
		} else {
			prop.Default = fmt.Sprintf("%v", f.Interface())
		}
	}
}

func stringArrayEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}

	return true
}

func (docs *PropertyDocs) SameSubProperties(other PropertyDocs) bool {
	if len(docs.Properties) != len(other.Properties) {
		return false
	}

	for i := range docs.Properties {
		if !docs.Properties[i].Equal(other.Properties[i]) {
			return false
		}
	}

	return true
}

func (docs *PropertyStructDocs) GetByName(name string) *PropertyDocs {
	return getByName(name, "", &docs.Properties)
}

func getByName(name string, prefix string, props *[]PropertyDocs) *PropertyDocs {
	for i := range *props {
		if prefix+(*props)[i].Name == name {
			return &(*props)[i]
		} else if strings.HasPrefix(name, prefix+(*props)[i].Name+".") {
			return getByName(name, prefix+(*props)[i].Name+".", &(*props)[i].Properties)
		}
	}
	return nil
}

func (prop *PropertyDocs) Nest(nested *PropertyStructDocs) {
	//prop.Name += "(" + nested.Name + ")"
	//prop.Text += "(" + nested.Text + ")"
	prop.Properties = append(prop.Properties, nested.Properties...)
}

func newDocs(t *doc.Type) (*PropertyStructDocs, error) {
	typeSpec := t.Decl.Specs[0].(*ast.TypeSpec)
	docs := PropertyStructDocs{
		Name: t.Name,
		Text: t.Doc,
	}

	structType, ok := typeSpec.Type.(*ast.StructType)
	if !ok {
		return nil, fmt.Errorf("type of %q is not a struct", t.Name)
	}

	var err error
	docs.Properties, err = structProperties(structType)
	if err != nil {
		return nil, err
	}

	return &docs, nil
}

func structProperties(structType *ast.StructType) (props []PropertyDocs, err error) {
	for _, f := range structType.Fields.List {
		names := f.Names
		if names == nil {
			// Anonymous fields have no name, use the type as the name
			// TODO: hide the name and make the properties show up in the embedding struct
			if t, ok := f.Type.(*ast.Ident); ok {
				names = append(names, t)
			}
		}
		for _, n := range names {
			var name, typ, tag, text string
			var innerProps []PropertyDocs
			if n != nil {
				name = proptools.PropertyNameForField(n.Name)
			}
			if f.Doc != nil {
				text = f.Doc.Text()
			}
			if f.Tag != nil {
				tag, err = strconv.Unquote(f.Tag.Value)
				if err != nil {
					return nil, err
				}
			}
			switch a := f.Type.(type) {
			case *ast.ArrayType:
				typ = "list of strings"
			case *ast.InterfaceType:
				typ = "interface"
			case *ast.Ident:
				typ = a.Name
			case *ast.StructType:
				innerProps, err = structProperties(a)
				if err != nil {
					return nil, err
				}
			default:
				typ = fmt.Sprintf("%T", f.Type)
			}

			props = append(props, PropertyDocs{
				Name:       name,
				Type:       typ,
				Tag:        reflect.StructTag(tag),
				Text:       text,
				Properties: innerProps,
			})
		}
	}

	return props, nil
}

func (docs *PropertyStructDocs) ExcludeByTag(key, value string) {
	filterPropsByTag(&docs.Properties, key, value, true)
}

func (docs *PropertyStructDocs) IncludeByTag(key, value string) {
	filterPropsByTag(&docs.Properties, key, value, false)
}

func filterPropsByTag(props *[]PropertyDocs, key, value string, exclude bool) {
	// Create a slice that shares the storage of props but has 0 length.  Appending up to
	// len(props) times to this slice will overwrite the original slice contents
	filtered := (*props)[:0]
	for _, x := range *props {
		tag := x.Tag.Get(key)
		for _, entry := range strings.Split(tag, ",") {
			if (entry == value) == !exclude {
				filtered = append(filtered, x)
			}
		}
	}

	*props = filtered
}

// Package AST generation and storage
func (dc *DocCollector) packageDocs(pkg string) (*doc.Package, error) {
	pkgDocs := dc.getPackageDocs(pkg)
	if pkgDocs == nil {
		if files, ok := dc.pkgFiles[pkg]; ok {
			var err error
			pkgAST, err := NewPackageAST(files)
			if err != nil {
				return nil, err
			}
			pkgDocs = doc.New(pkgAST, pkg, doc.AllDecls)
			pkgDocs = dc.putPackageDocs(pkg, pkgDocs)
		} else {
			return nil, fmt.Errorf("unknown package %q", pkg)
		}
	}
	return pkgDocs, nil
}

func (dc *DocCollector) getPackageDocs(pkg string) *doc.Package {
	dc.mutex.Lock()
	defer dc.mutex.Unlock()

	return dc.pkgDocs[pkg]
}

func (dc *DocCollector) putPackageDocs(pkg string, pkgDocs *doc.Package) *doc.Package {
	dc.mutex.Lock()
	defer dc.mutex.Unlock()

	if dc.pkgDocs[pkg] != nil {
		return dc.pkgDocs[pkg]
	} else {
		dc.pkgDocs[pkg] = pkgDocs
		return pkgDocs
	}
}

func NewPackageAST(files []string) (*ast.Package, error) {
	asts := make(map[string]*ast.File)

	fset := token.NewFileSet()
	for _, file := range files {
		ast, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
		if err != nil {
			return nil, err
		}
		asts[file] = ast
	}

	pkg, _ := ast.NewPackage(fset, asts, nil, nil)
	return pkg, nil
}

func Write(filename string, pkgFiles map[string][]string,
	moduleTypePropertyStructs map[string][]interface{}) error {

	docSet := NewDocCollector(pkgFiles)

	var moduleTypeList []*moduleTypeDoc
	for moduleType, propertyStructs := range moduleTypePropertyStructs {
		mtDoc, err := getModuleTypeDoc(docSet, moduleType, propertyStructs)
		if err != nil {
			return err
		}
		removeEmptyPropertyStructs(mtDoc)
		collapseDuplicatePropertyStructs(mtDoc)
		collapseNestedPropertyStructs(mtDoc)
		combineDuplicateProperties(mtDoc)
		moduleTypeList = append(moduleTypeList, mtDoc)
	}

	sort.Sort(moduleTypeByName(moduleTypeList))

	buf := &bytes.Buffer{}

	unique := 0

	tmpl, err := template.New("file").Funcs(map[string]interface{}{
		"unique": func() int {
			unique++
			return unique
		}}).Parse(fileTemplate)
	if err != nil {
		return err
	}

	err = tmpl.Execute(buf, moduleTypeList)
	if err != nil {
		return err
	}

	err = ioutil.WriteFile(filename, buf.Bytes(), 0666)
	if err != nil {
		return err
	}

	return nil
}

func getModuleTypeDoc(docSet *DocCollector, moduleType string,
	propertyStructs []interface{}) (*moduleTypeDoc, error) {
	mtDoc := &moduleTypeDoc{
		Name: moduleType,
		//Text: docSet.ModuleTypeDocs(moduleType),
	}

	for _, s := range propertyStructs {
		v := reflect.ValueOf(s).Elem()
		t := v.Type()

		// Ignore property structs with unexported or unnamed types
		if t.PkgPath() == "" {
			continue
		}
		psDoc, err := docSet.Docs(t.PkgPath(), t.Name(), v)
		if err != nil {
			return nil, err
		}
		psDoc.ExcludeByTag("blueprint", "mutated")

		for nested, nestedValue := range nestedPropertyStructs(v) {
			nestedType := nestedValue.Type()

			// Ignore property structs with unexported or unnamed types
			if nestedType.PkgPath() == "" {
				continue
			}
			nestedDoc, err := docSet.Docs(nestedType.PkgPath(), nestedType.Name(), nestedValue)
			if err != nil {
				return nil, err
			}
			nestedDoc.ExcludeByTag("blueprint", "mutated")
			nestPoint := psDoc.GetByName(nested)
			if nestPoint == nil {
				return nil, fmt.Errorf("nesting point %q not found", nested)
			}

			key, value, err := blueprint.HasFilter(nestPoint.Tag)
			if err != nil {
				return nil, err
			}
			if key != "" {
				nestedDoc.IncludeByTag(key, value)
			}

			nestPoint.Nest(nestedDoc)
		}
		mtDoc.PropertyStructs = append(mtDoc.PropertyStructs, psDoc)
	}

	return mtDoc, nil
}

func nestedPropertyStructs(s reflect.Value) map[string]reflect.Value {
	ret := make(map[string]reflect.Value)
	var walk func(structValue reflect.Value, prefix string)
	walk = func(structValue reflect.Value, prefix string) {
		typ := structValue.Type()
		for i := 0; i < structValue.NumField(); i++ {
			field := typ.Field(i)
			if field.PkgPath != "" {
				// The field is not exported so just skip it.
				continue
			}

			fieldValue := structValue.Field(i)

			switch fieldValue.Kind() {
			case reflect.Bool, reflect.String, reflect.Slice, reflect.Int, reflect.Uint:
				// Nothing
			case reflect.Struct:
				walk(fieldValue, prefix+proptools.PropertyNameForField(field.Name)+".")
			case reflect.Ptr, reflect.Interface:
				if !fieldValue.IsNil() {
					// We leave the pointer intact and zero out the struct that's
					// pointed to.
					elem := fieldValue.Elem()
					if fieldValue.Kind() == reflect.Interface {
						if elem.Kind() != reflect.Ptr {
							panic(fmt.Errorf("can't get type of field %q: interface "+
								"refers to a non-pointer", field.Name))
						}
						elem = elem.Elem()
					}
					if elem.Kind() == reflect.Struct {
						nestPoint := prefix + proptools.PropertyNameForField(field.Name)
						ret[nestPoint] = elem
						walk(elem, nestPoint+".")
					}
				}
			default:
				panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
					field.Name, fieldValue.Kind()))
			}
		}

	}

	walk(s, "")
	return ret
}

// Remove any property structs that have no exported fields
func removeEmptyPropertyStructs(mtDoc *moduleTypeDoc) {
	for i := 0; i < len(mtDoc.PropertyStructs); i++ {
		if len(mtDoc.PropertyStructs[i].Properties) == 0 {
			mtDoc.PropertyStructs = append(mtDoc.PropertyStructs[:i], mtDoc.PropertyStructs[i+1:]...)
			i--
		}
	}
}

// Squashes duplicates of the same property struct into single entries
func collapseDuplicatePropertyStructs(mtDoc *moduleTypeDoc) {
	var collapsedDocs []*PropertyStructDocs

propertyStructLoop:
	for _, from := range mtDoc.PropertyStructs {
		for _, to := range collapsedDocs {
			if from.Name == to.Name {
				collapseDuplicateProperties(&to.Properties, &from.Properties)
				continue propertyStructLoop
			}
		}
		collapsedDocs = append(collapsedDocs, from)
	}
	mtDoc.PropertyStructs = collapsedDocs
}

func collapseDuplicateProperties(to, from *[]PropertyDocs) {
propertyLoop:
	for _, f := range *from {
		for i := range *to {
			t := &(*to)[i]
			if f.Name == t.Name {
				collapseDuplicateProperties(&t.Properties, &f.Properties)
				continue propertyLoop
			}
		}
		*to = append(*to, f)
	}
}

// Find all property structs that only contain structs, and move their children up one with
// a prefixed name
func collapseNestedPropertyStructs(mtDoc *moduleTypeDoc) {
	for _, ps := range mtDoc.PropertyStructs {
		collapseNestedProperties(&ps.Properties)
	}
}

func collapseNestedProperties(p *[]PropertyDocs) {
	var n []PropertyDocs

	for _, parent := range *p {
		var containsProperty bool
		for j := range parent.Properties {
			child := &parent.Properties[j]
			if len(child.Properties) > 0 {
				collapseNestedProperties(&child.Properties)
			} else {
				containsProperty = true
			}
		}
		if containsProperty || len(parent.Properties) == 0 {
			n = append(n, parent)
		} else {
			for j := range parent.Properties {
				child := parent.Properties[j]
				child.Name = parent.Name + "." + child.Name
				n = append(n, child)
			}
		}
	}
	*p = n
}

func combineDuplicateProperties(mtDoc *moduleTypeDoc) {
	for _, ps := range mtDoc.PropertyStructs {
		combineDuplicateSubProperties(&ps.Properties)
	}
}

func combineDuplicateSubProperties(p *[]PropertyDocs) {
	var n []PropertyDocs
propertyLoop:
	for _, child := range *p {
		if len(child.Properties) > 0 {
			combineDuplicateSubProperties(&child.Properties)
			for i := range n {
				s := &n[i]
				if s.SameSubProperties(child) {
					s.OtherNames = append(s.OtherNames, child.Name)
					s.OtherTexts = append(s.OtherTexts, child.Text)
					continue propertyLoop
				}
			}
		}
		n = append(n, child)
	}

	*p = n
}

type moduleTypeByName []*moduleTypeDoc

func (l moduleTypeByName) Len() int           { return len(l) }
func (l moduleTypeByName) Less(i, j int) bool { return l[i].Name < l[j].Name }
func (l moduleTypeByName) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }

type moduleTypeDoc struct {
	Name            string
	Text            string
	PropertyStructs []*PropertyStructDocs
}

var (
	fileTemplate = `
<html>
<head>
<title>Build Docs</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</head>
<body>
<h1>Build Docs</h1>
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
  {{range .}}
    {{ $collapseIndex := unique }}
    <div class="panel panel-default">
      <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
        <h2 class="panel-title">
          <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
             {{.Name}}
          </a>
        </h2>
      </div>
    </div>
    <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
      <div class="panel-body">
        <p>{{.Text}}</p>
        {{range .PropertyStructs}}
          <p>{{.Text}}</p>
          {{template "properties" .Properties}}
        {{end}}
      </div>
    </div>
  {{end}}
</div>
</body>
</html>

{{define "properties"}}
  <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
    {{range .}}
      {{$collapseIndex := unique}}
      {{if .Properties}}
        <div class="panel panel-default">
          <div class="panel-heading" role="tab" id="heading{{$collapseIndex}}">
            <h4 class="panel-title">
              <a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapse{{$collapseIndex}}" aria-expanded="false" aria-controls="collapse{{$collapseIndex}}">
                 {{.Name}}{{range .OtherNames}}, {{.}}{{end}}
              </a>
            </h4>
          </div>
        </div>
        <div id="collapse{{$collapseIndex}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="heading{{$collapseIndex}}">
          <div class="panel-body">
            <p>{{.Text}}</p>
            {{range .OtherTexts}}<p>{{.}}</p>{{end}}
            {{template "properties" .Properties}}
          </div>
        </div>
      {{else}}
        <div>
          <h4>{{.Name}}{{range .OtherNames}}, {{.}}{{end}}</h4>
          <p>{{.Text}}</p>
          {{range .OtherTexts}}<p>{{.}}</p>{{end}}
          <p><i>Type: {{.Type}}</i></p>
          {{if .Default}}<p><i>Default: {{.Default}}</i></p>{{end}}
        </div>
      {{end}}
    {{end}}
  </div>
{{end}}
`
)