// Copyright 2016 CoreOS, Inc.
//
// 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 runtime

import (
	"context"
	"fmt"
	"os"
	"strings"
	"sync"
)

var (
	ErrNoExist  = fmt.Errorf("failpoint: failpoint does not exist")
	ErrDisabled = fmt.Errorf("failpoint: failpoint is disabled")

	failpoints   map[string]*Failpoint
	failpointsMu sync.RWMutex
	envTerms     map[string]string
)

func init() {
	failpoints = make(map[string]*Failpoint)
	envTerms = make(map[string]string)
	if s := os.Getenv("GOFAIL_FAILPOINTS"); len(s) > 0 {
		// format is <FAILPOINT>=<TERMS>[;<FAILPOINT>=<TERMS>;...]
		for _, fp := range strings.Split(s, ";") {
			fpTerm := strings.Split(fp, "=")
			if len(fpTerm) != 2 {
				fmt.Printf("bad failpoint %q\n", fp)
				os.Exit(1)
			}
			envTerms[fpTerm[0]] = fpTerm[1]
		}
	}
	if s := os.Getenv("GOFAIL_HTTP"); len(s) > 0 {
		if err := serve(s); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	}
}

// Enable sets a failpoint to a given failpoint description.
func Enable(failpath, inTerms string) error {
	unlock, err := enableAndLock(failpath, inTerms)
	if unlock != nil {
		unlock()
	}
	return err
}

// enableAndLock enables a failpoint and returns a function to unlock it
func enableAndLock(failpath, inTerms string) (func(), error) {
	t, err := newTerms(failpath, inTerms)
	if err != nil {
		fmt.Printf("failed to enable \"%s=%s\" (%v)\n", failpath, inTerms, err)
		return nil, err
	}
	failpointsMu.RLock()
	fp := failpoints[failpath]
	failpointsMu.RUnlock()
	if fp == nil {
		return nil, ErrNoExist
	}
	fp.mu.Lock()
	fp.t = t
	fp.released = false
	return func() { fp.mu.Unlock() }, nil
}

// Disable stops a failpoint from firing.
func Disable(failpath string) error {
	failpointsMu.RLock()
	fp := failpoints[failpath]
	failpointsMu.RUnlock()
	if fp == nil {
		return ErrNoExist
	}

	fp.cmu.RLock()
	cancel := fp.cancel
	donec := fp.donec
	fp.cmu.RUnlock()
	if cancel != nil && donec != nil {
		cancel()
		<-donec

		fp.cmu.Lock()
		fp.ctx, fp.cancel = context.WithCancel(context.Background())
		fp.donec = make(chan struct{})
		fp.released = true
		fp.cmu.Unlock()
	}

	fp.mu.Lock()
	defer fp.mu.Unlock()
	if fp.t == nil {
		return ErrDisabled
	}
	fp.t = nil
	return nil
}

// Status gives the current setting for the failpoint
func Status(failpath string) (string, error) {
	failpointsMu.RLock()
	fp := failpoints[failpath]
	failpointsMu.RUnlock()
	if fp == nil {
		return "", ErrNoExist
	}
	fp.mu.RLock()
	t := fp.t
	fp.mu.RUnlock()
	if t == nil {
		return "", ErrDisabled
	}
	return t.desc, nil
}

func List() []string {
	failpointsMu.RLock()
	ret := make([]string, 0, len(failpoints))
	for fp := range failpoints {
		ret = append(ret, fp)
	}
	failpointsMu.RUnlock()
	return ret
}

func register(failpath string, fp *Failpoint) {
	failpointsMu.Lock()
	failpoints[failpath] = fp
	failpointsMu.Unlock()
	if t, ok := envTerms[failpath]; ok {
		Enable(failpath, t)
	}
}
