//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris || windows
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris windows

/* Copyright 2018 The Bazel Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"io"
	"log"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"github.com/fsnotify/fsnotify"
)

// startServer starts a new server process. This is called by the client.
func startServer() error {
	exe, err := os.Executable()
	if err != nil {
		return err
	}
	args := []string{"-server"}
	args = append(args, os.Args[1:]...)
	cmd := exec.Command(exe, args...)
	log.Printf("starting server: %s", strings.Join(cmd.Args, " "))
	if err := cmd.Start(); err != nil {
		return err
	}
	if err := cmd.Process.Release(); err != nil {
		return err
	}
	return nil
}

// runServer performs the main work of the server. Once started, the server
// will:
//
// * Copy BUILD.in and BUILD.bazel.in files to BUILD and BUILD.bazel.
// * Watch for file system writes in the whole repository.
// * Listen for clients on a UNIX-domain socket.
//
// When the server accepts a connection, it runs Gazelle. On the first run,
// it runs Gazelle on the entire repository. On subsequent runs, it runs
// Gazelle only in directories that have changed.
//
// The server stops after being idle for a while. It can also be stopped
// with SIGINT or SIGTERM.
func runServer() error {
	// Begin logging to the log file.
	logFile, err := os.OpenFile(*logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o666)
	if err != nil {
		return err
	}
	defer logFile.Close()
	log.SetOutput(logFile)

	// Start listening on the socket before other initialization work. The client
	// will dial immediately after starting the server, and we don't want
	// the client to time out.
	os.Remove(*socketPath)
	ln, err := net.Listen("unix", *socketPath)
	if err != nil {
		return err
	}
	uln := ln.(*net.UnixListener)
	uln.SetUnlinkOnClose(true)
	defer ln.Close()
	if err := uln.SetDeadline(time.Now().Add(*serverTimeout)); err != nil {
		return err
	}
	log.Printf("started server with pid %d", os.Getpid())

	// Copy BUILD.in files to BUILD.
	restoreBuildFilesInRepo()

	// Listen for file writes within the repository.
	cancelWatch, err := watchDir(".", recordWrite)
	isWatching := err == nil
	if err != nil {
		log.Print(err)
	}
	if isWatching {
		defer cancelWatch()
	}

	// Wait for clients to connect. Each time the client connects, we run
	// gazelle, either in the whole repository or in changed directories.
	mode := fullMode
	for {
		c, err := ln.Accept()
		if err != nil {
			if operr, ok := err.(*net.OpError); ok {
				if operr.Timeout() {
					return nil
				}
				if operr.Temporary() {
					log.Printf("temporary watch error: %v", err)
					continue
				}
			}
			return err
		}

		log.SetOutput(io.MultiWriter(c, logFile))
		dirs := getAndClearWrittenDirs()
		for _, dir := range dirs {
			restoreBuildFilesInDir(dir)
		}
		if err := runGazelle(mode, dirs); err != nil {
			log.Print(err)
		}
		log.SetOutput(logFile)
		c.Close()
		if isWatching {
			mode = fastMode
		}
	}
}

// watchDir listens for file system changes in root and its
// subdirectories. The record function is called with directories whose
// contents have changed. New directories are watched recursively.
// The returned cancel function may be called to stop watching.
func watchDir(root string, record func(string)) (cancel func(), err error) {
	w, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}

	dirs, errs := listDirs(root)
	for _, err := range errs {
		log.Print(err)
	}
	gitDir := filepath.Join(root, ".git")
	for _, dir := range dirs {
		if dir == gitDir {
			continue
		}
		if err := w.Add(dir); err != nil {
			log.Print(err)
		}
	}

	done := make(chan struct{})
	go func() {
		for {
			select {
			case ev := <-w.Events:
				if shouldIgnore(ev.Name) {
					continue
				}
				if ev.Op == fsnotify.Create {
					if st, err := os.Lstat(ev.Name); err != nil {
						log.Print(err)
					} else if st.IsDir() {
						dirs, errs := listDirs(ev.Name)
						for _, err := range errs {
							log.Print(err)
						}
						for _, dir := range dirs {
							if err := w.Add(dir); err != nil {
								log.Print(err)
							}
							recordWrite(dir)
						}
					}
				} else {
					recordWrite(filepath.Dir(ev.Name))
				}
			case err := <-w.Errors:
				log.Print(err)
			case <-done:
				if err := w.Close(); err != nil {
					log.Print(err)
				}
				return
			}
		}
	}()
	return func() { close(done) }, nil
}

// listDirs returns a slice containing all the subdirectories under dir,
// including dir itself.
func listDirs(dir string) ([]string, []error) {
	var dirs []string
	var errs []error
	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			errs = append(errs, err)
			return nil
		}
		if info.IsDir() {
			dirs = append(dirs, path)
		}
		return nil
	})
	if err != nil {
		errs = append(errs, err)
	}
	return dirs, errs
}

// shouldIgnore returns whether a write to the given file should be ignored
// because they were caused by gazelle or autogazelle or something unrelated
// to the build.
func shouldIgnore(p string) bool {
	p = strings.TrimPrefix(filepath.ToSlash(p), "./")
	base := path.Base(p)
	return strings.HasPrefix(p, "tools/") || base == ".git" || base == "BUILD" || base == "BUILD.bazel"
}

var (
	dirSetMutex sync.Mutex
	dirSet      = map[string]bool{}
)

// recordWrite records that a directory has been modified and that its build
// file should be updated the next time gazelle runs.
func recordWrite(path string) {
	dirSetMutex.Lock()
	defer dirSetMutex.Unlock()
	dirSet[path] = true
}

// getAndClearWrittenDirs retrieves a list of directories that have been
// modified since the last time getAndClearWrittenDirs was called.
func getAndClearWrittenDirs() []string {
	dirSetMutex.Lock()
	defer dirSetMutex.Unlock()
	dirs := make([]string, 0, len(dirSet))
	for d := range dirSet {
		dirs = append(dirs, d)
	}
	dirSet = make(map[string]bool)
	return dirs
}
