// Copyright 2024 The Hugo 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 tasks

import (
	"sync"
	"time"
)

// RunEvery runs a function at intervals defined by the function itself.
// Functions can be added and removed while running.
type RunEvery struct {
	// Any error returned from the function will be passed to this function.
	HandleError func(string, error)

	// If set, the function will be run immediately.
	RunImmediately bool

	// The named functions to run.
	funcs map[string]*Func

	mu      sync.Mutex
	started bool
	closed  bool
	quit    chan struct{}
}

type Func struct {
	// The shortest interval between each run.
	IntervalLow time.Duration

	// The longest interval between each run.
	IntervalHigh time.Duration

	// The function to run.
	F func(interval time.Duration) (time.Duration, error)

	interval time.Duration
	last     time.Time
}

func (r *RunEvery) Start() error {
	if r.started {
		return nil
	}

	r.started = true
	r.quit = make(chan struct{})

	go func() {
		if r.RunImmediately {
			r.run()
		}
		ticker := time.NewTicker(500 * time.Millisecond)
		defer ticker.Stop()
		for {
			select {
			case <-r.quit:
				return
			case <-ticker.C:
				r.run()
			}
		}
	}()

	return nil
}

// Close stops the RunEvery from running.
func (r *RunEvery) Close() error {
	if r.closed {
		return nil
	}
	r.closed = true
	if r.quit != nil {
		close(r.quit)
	}
	return nil
}

// Add adds a function to the RunEvery.
func (r *RunEvery) Add(name string, f Func) {
	r.mu.Lock()
	defer r.mu.Unlock()
	if r.funcs == nil {
		r.funcs = make(map[string]*Func)
	}
	if f.IntervalLow == 0 {
		f.IntervalLow = 500 * time.Millisecond
	}
	if f.IntervalHigh <= f.IntervalLow {
		f.IntervalHigh = 20 * time.Second
	}

	start := max(f.IntervalHigh/3, f.IntervalLow)
	f.interval = start
	f.last = time.Now()

	r.funcs[name] = &f
}

// Remove removes a function from the RunEvery.
func (r *RunEvery) Remove(name string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	delete(r.funcs, name)
}

// Has returns whether the RunEvery has a function with the given name.
func (r *RunEvery) Has(name string) bool {
	r.mu.Lock()
	defer r.mu.Unlock()
	_, found := r.funcs[name]
	return found
}

func (r *RunEvery) run() {
	r.mu.Lock()
	defer r.mu.Unlock()
	for name, f := range r.funcs {
		if time.Now().Before(f.last.Add(f.interval)) {
			continue
		}
		f.last = time.Now()
		interval, err := f.F(f.interval)
		if err != nil && r.HandleError != nil {
			r.HandleError(name, err)
		}

		if interval < f.IntervalLow {
			interval = f.IntervalLow
		}

		if interval > f.IntervalHigh {
			interval = f.IntervalHigh
		}
		f.interval = interval
	}
}
