package cache

import (
	"encoding/json"
	"io/ioutil"
	"os"
	"os/signal"
	"path/filepath"
	"time"

	"github.com/xkeyideal/glide/msg"
	gpath "github.com/xkeyideal/glide/path"
)

var isStarted bool

// If the global cache lock file should be written
var shouldWriteLock = true

// SystemLock starts a system rather than application lock. This way multiple
// app instances don't cause race conditions when working in the cache.
func SystemLock() error {
	if isStarted {
		return nil
	}
	err := waitOnLock()
	if err != nil {
		return err
	}
	err = startLock()
	isStarted = true
	return err
}

// SystemUnlock removes the system wide Glide cache lock.
func SystemUnlock() {
	lockdone <- struct{}{}
	os.Remove(lockFileName)
}

var lockdone = make(chan struct{}, 1)

type lockdata struct {
	Comment string `json:"comment"`
	Pid     int    `json:"pid"`
	Time    string `json:"time"`
}

var lockFileName = filepath.Join(gpath.Home(), "lock.json")

// Write a lock for now.
func writeLock() error {

	// If the lock should not be written exit immediately. This happens in cases
	// where shutdown/clean is happening.
	if !shouldWriteLock {
		return nil
	}

	ld := &lockdata{
		Comment: "File managed by Glide (https://glide.sh)",
		Pid:     os.Getpid(),
		Time:    time.Now().Format(time.RFC3339Nano),
	}

	out, err := json.Marshal(ld)
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(lockFileName, out, 0755)
	return err
}

func startLock() error {
	err := writeLock()
	if err != nil {
		return err
	}

	go func() {
		for {
			select {
			case <-lockdone:
				return
			default:
				time.Sleep(10 * time.Second)
				err := writeLock()
				if err != nil {
					msg.Die("Error using Glide lock: %s", err)
				}
			}
		}
	}()

	// Capture ctrl-c or other interruptions then clean up the global lock.
	ch := make(chan os.Signal)
	signal.Notify(ch, os.Interrupt, os.Kill)
	go func(cc <-chan os.Signal) {
		s := <-cc
		shouldWriteLock = false
		SystemUnlock()

		// Exiting with the expected exit codes when we can.
		if s == os.Interrupt {
			os.Exit(130)
		} else if s == os.Kill {
			os.Exit(137)
		} else {
			os.Exit(1)
		}
	}(ch)

	return nil
}

func waitOnLock() error {
	var announced bool
	for {
		fi, err := os.Stat(lockFileName)
		if err != nil && os.IsNotExist(err) {
			return nil
		} else if err != nil {
			return err
		}

		diff := time.Now().Sub(fi.ModTime())
		if diff.Seconds() > 15 {
			return nil
		}

		if !announced {
			announced = true
			msg.Info("Waiting on Glide global cache access")
		}

		// Check on the lock file every second.
		time.Sleep(time.Second)
	}
}
