package gocode

import (
	"bytes"
	"fmt"
	"go/build"
	"log"
	"net"
	"net/rpc"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"time"
)

func do_server() int {
	g_config.read()
	if g_config.ForceDebugOutput != "" {
		// forcefully enable debugging and redirect logging into the
		// specified file
		*g_debug = true
		f, err := os.Create(g_config.ForceDebugOutput)
		if err != nil {
			panic(err)
		}
		log.SetOutput(f)
	}

	addr := *g_addr
	if *g_sock == "unix" {
		addr = get_socket_filename()
		if file_exists(addr) {
			log.Printf("unix socket: '%s' already exists\n", addr)
			return 1
		}
	}
	g_daemon = new_daemon(*g_sock, addr)
	if *g_sock == "unix" {
		// cleanup unix socket file
		defer os.Remove(addr)
	}

	rpc.Register(new(RPC))

	g_daemon.loop()
	return 0
}

//-------------------------------------------------------------------------
// daemon
//-------------------------------------------------------------------------

type daemon struct {
	listener     net.Listener
	cmd_in       chan int
	autocomplete *auto_complete_context
	pkgcache     package_cache
	declcache    *decl_cache
	context      package_lookup_context
}

func new_daemon(network, address string) *daemon {
	var err error

	d := new(daemon)
	d.listener, err = net.Listen(network, address)
	if err != nil {
		panic(err)
	}

	d.cmd_in = make(chan int, 1)
	d.pkgcache = new_package_cache()
	d.declcache = new_decl_cache(&d.context)
	d.autocomplete = new_auto_complete_context(d.pkgcache, d.declcache)
	return d
}

func (this *daemon) drop_cache() {
	this.pkgcache = new_package_cache()
	this.declcache = new_decl_cache(&this.context)
	this.autocomplete = new_auto_complete_context(this.pkgcache, this.declcache)
}

const (
	daemon_close = iota
)

func (this *daemon) loop() {
	conn_in := make(chan net.Conn)
	go func() {
		for {
			c, err := this.listener.Accept()
			if err != nil {
				panic(err)
			}
			conn_in <- c
		}
	}()

	timeout := time.Duration(g_config.CloseTimeout) * time.Second
	countdown := time.NewTimer(timeout)

	for {
		// handle connections or server CMDs (currently one CMD)
		select {
		case c := <-conn_in:
			rpc.ServeConn(c)
			countdown.Reset(timeout)
			runtime.GC()
		case cmd := <-this.cmd_in:
			switch cmd {
			case daemon_close:
				return
			}
		case <-countdown.C:
			return
		}
	}
}

func (this *daemon) close() {
	this.cmd_in <- daemon_close
}

var g_daemon *daemon

//-------------------------------------------------------------------------
// server_* functions
//
// Corresponding client_* functions are autogenerated by goremote.
//-------------------------------------------------------------------------

func server_auto_complete(file []byte, filename string, cursor int, context_packed go_build_context) (c []candidate, d int) {
	context := unpack_build_context(&context_packed)
	defer func() {
		if err := recover(); err != nil {
			print_backtrace(err)
			c = []candidate{
				{"PANIC", "PANIC", decl_invalid, "panic"},
			}

			// drop cache
			g_daemon.drop_cache()
		}
	}()
	// TODO: Probably we don't care about comparing all the fields, checking GOROOT and GOPATH
	// should be enough.
	if !reflect.DeepEqual(g_daemon.context.Context, context.Context) {
		g_daemon.context = context
		g_daemon.drop_cache()
	}
	switch g_config.PackageLookupMode {
	case "bzl":
		// when package lookup mode is bzl, we set GOPATH to "" explicitly and
		// BzlProjectRoot becomes valid (or empty)
		var err error
		g_daemon.context.GOPATH = ""
		g_daemon.context.BzlProjectRoot, err = find_bzl_project_root(g_config.LibPath, filename)
		if *g_debug && err != nil {
			log.Printf("Bzl project root not found: %s", err)
		}
	case "gb":
		// when package lookup mode is gb, we set GOPATH to "" explicitly and
		// GBProjectRoot becomes valid (or empty)
		var err error
		g_daemon.context.GOPATH = ""
		g_daemon.context.GBProjectRoot, err = find_gb_project_root(filename)
		if *g_debug && err != nil {
			log.Printf("Gb project root not found: %s", err)
		}
	case "go":
		// get current package path for GO15VENDOREXPERIMENT hack
		g_daemon.context.CurrentPackagePath = ""
		pkg, err := g_daemon.context.ImportDir(filepath.Dir(filename), build.FindOnly)
		if err == nil {
			if *g_debug {
				log.Printf("Go project path: %s", pkg.ImportPath)
			}
			g_daemon.context.CurrentPackagePath = pkg.ImportPath
		} else if *g_debug {
			log.Printf("Go project path not found: %s", err)
		}
	}
	if *g_debug {
		var buf bytes.Buffer
		log.Printf("Got autocompletion request for '%s'\n", filename)
		log.Printf("Cursor at: %d\n", cursor)
		if cursor > len(file) || cursor < 0 {
			log.Println("ERROR! Cursor is outside of the boundaries of the buffer, " +
				"this is most likely a text editor plugin bug. Text editor is responsible " +
				"for passing the correct cursor position to gocode.")
		} else {
			buf.WriteString("-------------------------------------------------------\n")
			buf.Write(file[:cursor])
			buf.WriteString("#")
			buf.Write(file[cursor:])
			log.Print(buf.String())
			log.Println("-------------------------------------------------------")
		}
	}
	candidates, d := g_daemon.autocomplete.apropos(file, filename, cursor)
	if *g_debug {
		log.Printf("Offset: %d\n", d)
		log.Printf("Number of candidates found: %d\n", len(candidates))
		log.Printf("Candidates are:\n")
		for _, c := range candidates {
			abbr := fmt.Sprintf("%s %s %s", c.Class, c.Name, c.Type)
			if c.Class == decl_func {
				abbr = fmt.Sprintf("%s %s%s", c.Class, c.Name, c.Type[len("func"):])
			}
			log.Printf("  %s\n", abbr)
		}
		log.Println("=======================================================")
	}
	return candidates, d
}

func server_close(notused int) int {
	g_daemon.close()
	return 0
}

func server_status(notused int) string {
	return g_daemon.autocomplete.status()
}

func server_drop_cache(notused int) int {
	// drop cache
	g_daemon.drop_cache()
	return 0
}

func server_set(key, value string) string {
	if key == "\x00" {
		return g_config.list()
	} else if value == "\x00" {
		return g_config.list_option(key)
	}
	// drop cache on settings changes
	g_daemon.drop_cache()
	return g_config.set_option(key, value)
}

func server_options(notused int) string {
	return g_config.options()
}
