package gocode

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"time"
)

//-------------------------------------------------------------------------
// out_buffers
//
// Temporary structure for writing autocomplete response.
//-------------------------------------------------------------------------

// fields must be exported for RPC
type candidate struct {
	Name    string
	Type    string
	Class   decl_class
	Package string
}

type out_buffers struct {
	tmpbuf            *bytes.Buffer
	candidates        []candidate
	canonical_aliases map[string]string
	ctx               *auto_complete_context
	tmpns             map[string]bool
	ignorecase        bool
}

func new_out_buffers(ctx *auto_complete_context) *out_buffers {
	b := new(out_buffers)
	b.tmpbuf = bytes.NewBuffer(make([]byte, 0, 1024))
	b.candidates = make([]candidate, 0, 64)
	b.ctx = ctx
	b.canonical_aliases = make(map[string]string)
	for _, imp := range b.ctx.current.packages {
		b.canonical_aliases[imp.abspath] = imp.alias
	}
	return b
}

func (b *out_buffers) Len() int {
	return len(b.candidates)
}

func (b *out_buffers) Less(i, j int) bool {
	x := b.candidates[i]
	y := b.candidates[j]
	if x.Class == y.Class {
		return x.Name < y.Name
	}
	return x.Class < y.Class
}

func (b *out_buffers) Swap(i, j int) {
	b.candidates[i], b.candidates[j] = b.candidates[j], b.candidates[i]
}

func (b *out_buffers) append_decl(p, name, pkg string, decl *decl, class decl_class) {
	c1 := !g_config.ProposeBuiltins && decl.scope == g_universe_scope && decl.name != "Error"
	c2 := class != decl_invalid && decl.class != class
	c3 := class == decl_invalid && !has_prefix(name, p, b.ignorecase)
	c4 := !decl.matches()
	c5 := !check_type_expr(decl.typ)

	if c1 || c2 || c3 || c4 || c5 {
		return
	}

	decl.pretty_print_type(b.tmpbuf, b.canonical_aliases)
	b.candidates = append(b.candidates, candidate{
		Name:    name,
		Type:    b.tmpbuf.String(),
		Class:   decl.class,
		Package: pkg,
	})
	b.tmpbuf.Reset()
}

func (b *out_buffers) append_embedded(p string, decl *decl, pkg string, class decl_class) {
	if decl.embedded == nil {
		return
	}

	first_level := false
	if b.tmpns == nil {
		// first level, create tmp namespace
		b.tmpns = make(map[string]bool)
		first_level = true

		// add all children of the current decl to the namespace
		for _, c := range decl.children {
			b.tmpns[c.name] = true
		}
	}

	for _, emb := range decl.embedded {
		typedecl := type_to_decl(emb, decl.scope)
		if typedecl == nil {
			continue
		}

		// could be type alias
		if typedecl.is_alias() {
			typedecl = typedecl.type_dealias()
		}

		// prevent infinite recursion here
		if typedecl.is_visited() {
			continue
		}
		typedecl.set_visited()
		defer typedecl.clear_visited()

		for _, c := range typedecl.children {
			if _, has := b.tmpns[c.name]; has {
				continue
			}
			b.append_decl(p, c.name, pkg, c, class)
			b.tmpns[c.name] = true
		}
		b.append_embedded(p, typedecl, pkg, class)
	}

	if first_level {
		// remove tmp namespace
		b.tmpns = nil
	}
}

//-------------------------------------------------------------------------
// auto_complete_context
//
// Context that holds cache structures for autocompletion needs. It
// includes cache for packages and for main package files.
//-------------------------------------------------------------------------

type auto_complete_context struct {
	current *auto_complete_file // currently edited file
	others  []*decl_file_cache  // other files of the current package
	pkg     *scope

	pcache    package_cache // packages cache
	declcache *decl_cache   // top-level declarations cache
}

func new_auto_complete_context(pcache package_cache, declcache *decl_cache) *auto_complete_context {
	c := new(auto_complete_context)
	c.current = new_auto_complete_file("", declcache.context)
	c.pcache = pcache
	c.declcache = declcache
	return c
}

func (c *auto_complete_context) update_caches() {
	// temporary map for packages that we need to check for a cache expiration
	// map is used as a set of unique items to prevent double checks
	ps := make(map[string]*package_file_cache)

	// collect import information from all of the files
	c.pcache.append_packages(ps, c.current.packages)
	c.others = get_other_package_files(c.current.name, c.current.package_name, c.declcache)
	for _, other := range c.others {
		c.pcache.append_packages(ps, other.packages)
	}

	update_packages(ps)

	// fix imports for all files
	fixup_packages(c.current.filescope, c.current.packages, c.pcache)
	for _, f := range c.others {
		fixup_packages(f.filescope, f.packages, c.pcache)
	}

	// At this point we have collected all top level declarations, now we need to
	// merge them in the common package block.
	c.merge_decls()
}

func (c *auto_complete_context) merge_decls() {
	c.pkg = new_scope(g_universe_scope)
	merge_decls(c.current.filescope, c.pkg, c.current.decls)
	merge_decls_from_packages(c.pkg, c.current.packages, c.pcache)
	for _, f := range c.others {
		merge_decls(f.filescope, c.pkg, f.decls)
		merge_decls_from_packages(c.pkg, f.packages, c.pcache)
	}

	// special pass for type aliases which also have methods, while this is
	// valid code, it shouldn't happen a lot in practice, so, whatever
	// let's move all type alias methods to their first non-alias type down in
	// the chain
	propagate_type_alias_methods(c.pkg)
}

func (c *auto_complete_context) make_decl_set(scope *scope) map[string]*decl {
	set := make(map[string]*decl, len(c.pkg.entities)*2)
	make_decl_set_recursive(set, scope)
	return set
}

func (c *auto_complete_context) get_candidates_from_set(set map[string]*decl, partial string, class decl_class, b *out_buffers) {
	for key, value := range set {
		if value == nil {
			continue
		}
		value.infer_type()
		pkgname := ""
		if pkg, ok := c.pcache[value.name]; ok {
			pkgname = pkg.import_name
		}
		b.append_decl(partial, key, pkgname, value, class)
	}
}

func (c *auto_complete_context) get_candidates_from_decl_alias(cc cursor_context, class decl_class, b *out_buffers) {
	if cc.decl.is_visited() {
		return
	}

	cc.decl = cc.decl.type_dealias()
	if cc.decl == nil {
		return
	}

	cc.decl.set_visited()
	defer cc.decl.clear_visited()

	c.get_candidates_from_decl(cc, class, b)
	return
}

func (c *auto_complete_context) decl_package_import_path(decl *decl) string {
	if decl == nil || decl.scope == nil {
		return ""
	}
	if pkg, ok := c.pcache[decl.scope.pkgname]; ok {
		return pkg.import_name
	}
	return ""
}

func (c *auto_complete_context) get_candidates_from_decl(cc cursor_context, class decl_class, b *out_buffers) {
	if cc.decl.is_alias() {
		c.get_candidates_from_decl_alias(cc, class, b)
		return
	}

	// propose all children of a subject declaration and
	for _, decl := range cc.decl.children {
		if cc.decl.class == decl_package && !ast.IsExported(decl.name) {
			continue
		}
		if cc.struct_field {
			// if we're autocompleting struct field init, skip all methods
			if _, ok := decl.typ.(*ast.FuncType); ok {
				continue
			}
		}
		b.append_decl(cc.partial, decl.name, c.decl_package_import_path(decl), decl, class)
	}
	// propose all children of an underlying struct/interface type
	adecl := advance_to_struct_or_interface(cc.decl)
	if adecl != nil && adecl != cc.decl {
		for _, decl := range adecl.children {
			if decl.class == decl_var {
				b.append_decl(cc.partial, decl.name, c.decl_package_import_path(decl), decl, class)
			}
		}
	}
	// propose all children of its embedded types
	b.append_embedded(cc.partial, cc.decl, c.decl_package_import_path(cc.decl), class)
}

func (c *auto_complete_context) get_import_candidates(partial string, b *out_buffers) {
	currentPackagePath, pkgdirs := g_daemon.context.pkg_dirs()
	resultSet := map[string]struct{}{}
	for _, pkgdir := range pkgdirs {
		// convert srcpath to pkgpath and get candidates
		get_import_candidates_dir(pkgdir, filepath.FromSlash(partial), b.ignorecase, currentPackagePath, resultSet)
	}
	for k := range resultSet {
		b.candidates = append(b.candidates, candidate{Name: k, Class: decl_import})
	}
}

func get_import_candidates_dir(root, partial string, ignorecase bool, currentPackagePath string, r map[string]struct{}) {
	var fpath string
	var match bool
	if strings.HasSuffix(partial, "/") {
		fpath = filepath.Join(root, partial)
	} else {
		fpath = filepath.Join(root, filepath.Dir(partial))
		match = true
	}
	fi := readdir(fpath)
	for i := range fi {
		name := fi[i].Name()
		rel, err := filepath.Rel(root, filepath.Join(fpath, name))
		if err != nil {
			panic(err)
		}
		if match && !has_prefix(rel, partial, ignorecase) {
			continue
		} else if fi[i].IsDir() {
			get_import_candidates_dir(root, rel+string(filepath.Separator), ignorecase, currentPackagePath, r)
		} else {
			ext := filepath.Ext(name)
			if ext != ".a" {
				continue
			} else {
				rel = rel[0 : len(rel)-2]
			}
			if ipath, ok := vendorlessImportPath(filepath.ToSlash(rel), currentPackagePath); ok {
				r[ipath] = struct{}{}
			}
		}
	}
}

// returns three slices of the same length containing:
// 1. apropos names
// 2. apropos types (pretty-printed)
// 3. apropos classes
// and length of the part that should be replaced (if any)
func (c *auto_complete_context) apropos(file []byte, filename string, cursor int) ([]candidate, int) {
	c.current.cursor = cursor
	c.current.name = filename

	// Update caches and parse the current file.
	// This process is quite complicated, because I was trying to design it in a
	// concurrent fashion. Apparently I'm not really good at that. Hopefully
	// will be better in future.

	// Ugly hack, but it actually may help in some cases. Insert a
	// semicolon right at the cursor location.
	filesemi := make([]byte, len(file)+1)
	copy(filesemi, file[:cursor])
	filesemi[cursor] = ';'
	copy(filesemi[cursor+1:], file[cursor:])

	// Does full processing of the currently edited file (top-level declarations plus
	// active function).
	c.current.process_data(filesemi)

	// Updates cache of other files and packages. See the function for details of
	// the process. At the end merges all the top-level declarations into the package
	// block.
	c.update_caches()

	// And we're ready to Go. ;)

	b := new_out_buffers(c)
	if g_config.IgnoreCase {
		if *g_debug {
			log.Printf("ignoring case sensitivity")
		}
		b.ignorecase = true
	}

	cc, ok := c.deduce_cursor_context(file, cursor)
	partial := len(cc.partial)
	if !g_config.Partials {
		if *g_debug {
			log.Printf("not performing partial prefix matching")
		}
		cc.partial = ""
	}
	if !ok {
		var d *decl
		if ident, ok := cc.expr.(*ast.Ident); ok && g_config.UnimportedPackages {
			p := resolveKnownPackageIdent(ident.Name, c.current.name, c.current.context)
			if p != nil {
				c.pcache[p.name] = p
				d = p.main
			}
		}
		if d == nil {
			return nil, 0
		}
		cc.decl = d
	}

	class := decl_invalid
	if g_config.ClassFiltering {
		switch cc.partial {
		case "const":
			class = decl_const
		case "var":
			class = decl_var
		case "type":
			class = decl_type
		case "func":
			class = decl_func
		case "package":
			class = decl_package
		}
	}

	if cc.decl_import {
		c.get_import_candidates(cc.partial, b)
		if cc.partial != "" && len(b.candidates) == 0 {
			// as a fallback, try case insensitive approach
			b.ignorecase = true
			c.get_import_candidates(cc.partial, b)
		}
	} else if cc.decl == nil {
		// In case if no declaraion is a subject of completion, propose all:
		set := c.make_decl_set(c.current.scope)
		c.get_candidates_from_set(set, cc.partial, class, b)
		if cc.partial != "" && len(b.candidates) == 0 {
			// as a fallback, try case insensitive approach
			b.ignorecase = true
			c.get_candidates_from_set(set, cc.partial, class, b)
		}
	} else {
		c.get_candidates_from_decl(cc, class, b)
		if cc.partial != "" && len(b.candidates) == 0 {
			// as a fallback, try case insensitive approach
			b.ignorecase = true
			c.get_candidates_from_decl(cc, class, b)
		}
	}

	if len(b.candidates) == 0 {
		return nil, 0
	}

	sort.Sort(b)
	return b.candidates, partial
}

func update_packages(ps map[string]*package_file_cache) {
	// initiate package cache update
	done := make(chan bool)
	for _, p := range ps {
		go func(p *package_file_cache) {
			defer func() {
				if err := recover(); err != nil {
					print_backtrace(err)
					done <- false
				}
			}()
			p.update_cache()
			done <- true
		}(p)
	}

	// wait for its completion
	for _ = range ps {
		if !<-done {
			panic("One of the package cache updaters panicked")
		}
	}
}

func collect_type_alias_methods(d *decl) map[string]*decl {
	if d == nil || d.is_visited() || !d.is_alias() {
		return nil
	}
	d.set_visited()
	defer d.clear_visited()

	// add own methods
	m := map[string]*decl{}
	for k, v := range d.children {
		m[k] = v
	}

	// recurse into more aliases
	dd := type_to_decl(d.typ, d.scope)
	for k, v := range collect_type_alias_methods(dd) {
		m[k] = v
	}

	return m
}

func propagate_type_alias_methods(s *scope) {
	for _, e := range s.entities {
		if !e.is_alias() {
			continue
		}

		methods := collect_type_alias_methods(e)
		if len(methods) == 0 {
			continue
		}

		dd := e.type_dealias()
		if dd == nil {
			continue
		}

		decl := dd.deep_copy()
		for _, v := range methods {
			decl.add_child(v)
		}
		s.entities[decl.name] = decl
	}
}

func merge_decls(filescope *scope, pkg *scope, decls map[string]*decl) {
	for _, d := range decls {
		pkg.merge_decl(d)
	}
	filescope.parent = pkg
}

func merge_decls_from_packages(pkgscope *scope, pkgs []package_import, pcache package_cache) {
	for _, p := range pkgs {
		path, alias := p.abspath, p.alias
		if alias != "." {
			continue
		}
		p := pcache[path].main
		if p == nil {
			continue
		}
		for _, d := range p.children {
			if ast.IsExported(d.name) {
				pkgscope.merge_decl(d)
			}
		}
	}
}

func fixup_packages(filescope *scope, pkgs []package_import, pcache package_cache) {
	for _, p := range pkgs {
		path, alias := p.abspath, p.alias
		if alias == "" {
			alias = pcache[path].defalias
		}
		// skip packages that will be merged to the package scope
		if alias == "." {
			continue
		}
		filescope.replace_decl(alias, pcache[path].main)
	}
}

func get_other_package_files(filename, packageName string, declcache *decl_cache) []*decl_file_cache {
	others := find_other_package_files(filename, packageName)

	ret := make([]*decl_file_cache, len(others))
	done := make(chan *decl_file_cache)

	for _, nm := range others {
		go func(name string) {
			defer func() {
				if err := recover(); err != nil {
					print_backtrace(err)
					done <- nil
				}
			}()
			done <- declcache.get_and_update(name)
		}(nm)
	}

	for i := range others {
		ret[i] = <-done
		if ret[i] == nil {
			panic("One of the decl cache updaters panicked")
		}
	}

	return ret
}

func find_other_package_files(filename, package_name string) []string {
	if filename == "" {
		return nil
	}

	dir, file := filepath.Split(filename)
	files_in_dir, err := readdir_lstat(dir)
	if err != nil {
		panic(err)
	}

	count := 0
	for _, stat := range files_in_dir {
		ok, _ := filepath.Match("*.go", stat.Name())
		if !ok || stat.Name() == file {
			continue
		}
		count++
	}

	out := make([]string, 0, count)
	for _, stat := range files_in_dir {
		const non_regular = os.ModeDir | os.ModeSymlink |
			os.ModeDevice | os.ModeNamedPipe | os.ModeSocket

		ok, _ := filepath.Match("*.go", stat.Name())
		if !ok || stat.Name() == file || stat.Mode()&non_regular != 0 {
			continue
		}

		abspath := filepath.Join(dir, stat.Name())
		if file_package_name(abspath) == package_name {
			n := len(out)
			out = out[:n+1]
			out[n] = abspath
		}
	}

	return out
}

func file_package_name(filename string) string {
	file, _ := parser.ParseFile(token.NewFileSet(), filename, nil, parser.PackageClauseOnly)
	return file.Name.Name
}

func make_decl_set_recursive(set map[string]*decl, scope *scope) {
	for name, ent := range scope.entities {
		if _, ok := set[name]; !ok {
			set[name] = ent
		}
	}
	if scope.parent != nil {
		make_decl_set_recursive(set, scope.parent)
	}
}

func check_func_field_list(f *ast.FieldList) bool {
	if f == nil {
		return true
	}

	for _, field := range f.List {
		if !check_type_expr(field.Type) {
			return false
		}
	}
	return true
}

// checks for a type expression correctness, it the type expression has
// ast.BadExpr somewhere, returns false, otherwise true
func check_type_expr(e ast.Expr) bool {
	switch t := e.(type) {
	case *ast.StarExpr:
		return check_type_expr(t.X)
	case *ast.ArrayType:
		return check_type_expr(t.Elt)
	case *ast.SelectorExpr:
		return check_type_expr(t.X)
	case *ast.FuncType:
		a := check_func_field_list(t.Params)
		b := check_func_field_list(t.Results)
		return a && b
	case *ast.MapType:
		a := check_type_expr(t.Key)
		b := check_type_expr(t.Value)
		return a && b
	case *ast.Ellipsis:
		return check_type_expr(t.Elt)
	case *ast.ChanType:
		return check_type_expr(t.Value)
	case *ast.BadExpr:
		return false
	default:
		return true
	}
}

//-------------------------------------------------------------------------
// Status output
//-------------------------------------------------------------------------

type decl_slice []*decl

func (s decl_slice) Less(i, j int) bool {
	if s[i].class != s[j].class {
		return s[i].name < s[j].name
	}
	return s[i].class < s[j].class
}
func (s decl_slice) Len() int      { return len(s) }
func (s decl_slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

const (
	color_red          = "\033[0;31m"
	color_red_bold     = "\033[1;31m"
	color_green        = "\033[0;32m"
	color_green_bold   = "\033[1;32m"
	color_yellow       = "\033[0;33m"
	color_yellow_bold  = "\033[1;33m"
	color_blue         = "\033[0;34m"
	color_blue_bold    = "\033[1;34m"
	color_magenta      = "\033[0;35m"
	color_magenta_bold = "\033[1;35m"
	color_cyan         = "\033[0;36m"
	color_cyan_bold    = "\033[1;36m"
	color_white        = "\033[0;37m"
	color_white_bold   = "\033[1;37m"
	color_none         = "\033[0m"
)

var g_decl_class_to_color = [...]string{
	decl_const:        color_white_bold,
	decl_var:          color_magenta,
	decl_type:         color_cyan,
	decl_func:         color_green,
	decl_package:      color_red,
	decl_methods_stub: color_red,
}

var g_decl_class_to_string_status = [...]string{
	decl_const:        "  const",
	decl_var:          "    var",
	decl_type:         "   type",
	decl_func:         "   func",
	decl_package:      "package",
	decl_methods_stub: "   stub",
}

func (c *auto_complete_context) status() string {

	buf := bytes.NewBuffer(make([]byte, 0, 4096))
	fmt.Fprintf(buf, "Server's GOMAXPROCS == %d\n", runtime.GOMAXPROCS(0))
	fmt.Fprintf(buf, "\nPackage cache contains %d entries\n", len(c.pcache))
	fmt.Fprintf(buf, "\nListing these entries:\n")
	for _, mod := range c.pcache {
		fmt.Fprintf(buf, "\tname: %s (default alias: %s)\n", mod.name, mod.defalias)
		fmt.Fprintf(buf, "\timports %d declarations and %d packages\n", len(mod.main.children), len(mod.others))
		if mod.mtime == -1 {
			fmt.Fprintf(buf, "\tthis package stays in cache forever (built-in package)\n")
		} else {
			mtime := time.Unix(0, mod.mtime)
			fmt.Fprintf(buf, "\tlast modification time: %s\n", mtime)
		}
		fmt.Fprintf(buf, "\n")
	}
	if c.current.name != "" {
		fmt.Fprintf(buf, "Last edited file: %s (package: %s)\n", c.current.name, c.current.package_name)
		if len(c.others) > 0 {
			fmt.Fprintf(buf, "\nOther files from the current package:\n")
		}
		for _, f := range c.others {
			fmt.Fprintf(buf, "\t%s\n", f.name)
		}
		fmt.Fprintf(buf, "\nListing declarations from files:\n")

		const status_decls = "\t%s%s" + color_none + " " + color_yellow + "%s" + color_none + "\n"
		const status_decls_children = "\t%s%s" + color_none + " " + color_yellow + "%s" + color_none + " (%d)\n"

		fmt.Fprintf(buf, "\n%s:\n", c.current.name)
		ds := make(decl_slice, len(c.current.decls))
		i := 0
		for _, d := range c.current.decls {
			ds[i] = d
			i++
		}
		sort.Sort(ds)
		for _, d := range ds {
			if len(d.children) > 0 {
				fmt.Fprintf(buf, status_decls_children,
					g_decl_class_to_color[d.class],
					g_decl_class_to_string_status[d.class],
					d.name, len(d.children))
			} else {
				fmt.Fprintf(buf, status_decls,
					g_decl_class_to_color[d.class],
					g_decl_class_to_string_status[d.class],
					d.name)
			}
		}

		for _, f := range c.others {
			fmt.Fprintf(buf, "\n%s:\n", f.name)
			ds = make(decl_slice, len(f.decls))
			i = 0
			for _, d := range f.decls {
				ds[i] = d
				i++
			}
			sort.Sort(ds)
			for _, d := range ds {
				if len(d.children) > 0 {
					fmt.Fprintf(buf, status_decls_children,
						g_decl_class_to_color[d.class],
						g_decl_class_to_string_status[d.class],
						d.name, len(d.children))
				} else {
					fmt.Fprintf(buf, status_decls,
						g_decl_class_to_color[d.class],
						g_decl_class_to_string_status[d.class],
						d.name)
				}
			}
		}
	}
	return buf.String()
}
