package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

// Timer implements an
type Timer struct {
	// Updated is used to notify UI about changes in the timer.
	Updated chan struct{}

	// mu locks the state such that it can be modified and accessed
	// from multiple goroutines.
	mu       sync.Mutex
	start    time.Time     // start corresponds to when the timer was started.
	now      time.Time     // now corresponds to the last updated time.
	duration time.Duration // duration is the maximum progress.
}

// NewTimer creates a new timer with the specified timer.
func NewTimer(initialDuration time.Duration) *Timer {
	return &Timer{
		Updated:  make(chan struct{}),
		duration: initialDuration,
	}
}

// Start the timer goroutine and return a cancel func that
// that can be used to stop it.
func (t *Timer) Start() context.CancelFunc {
	// initialize the timer state.
	now := time.Now()
	t.now = now
	t.start = now

	// we use done to signal stopping the goroutine.
	// a context.Context could be also used.
	done := make(chan struct{})
	go t.run(done)
	return func() { close(done) }
}

// run is the main loop for the timer.
func (t *Timer) run(done chan struct{}) {
	// we use a time.Ticker to update the state,
	// in many cases, this could be a network access instead.
	tick := time.NewTicker(50 * time.Millisecond)
	defer tick.Stop()

	for {
		select {
		case now := <-tick.C:
			t.update(now)
		case <-done:
			return
		}
	}
}

// invalidate sends a signal to the UI that
// the internal state has changed.
func (t *Timer) invalidate() {
	// we use a non-blocking send, that way the Timer
	// can continue updating internally.
	select {
	case t.Updated <- struct{}{}:
	default:
	}
}

func (t *Timer) update(now time.Time) {
	t.mu.Lock()
	defer t.mu.Unlock()

	previousNow := t.now
	t.now = now

	// first check whether we have not exceeded the duration.
	// in that case the progress advanced and we need to notify
	// about a change.
	progressAfter := t.now.Sub(t.start)
	if progressAfter <= t.duration {
		t.invalidate()
		return
	}

	// when we had progressed beyond the duration we also
	// need to update the first time it happens.
	progressBefore := previousNow.Sub(t.start)
	if progressBefore <= t.duration {
		t.invalidate()
		return
	}
}

// Reset resets timer to the last know time.
func (t *Timer) Reset() {
	t.mu.Lock()
	defer t.mu.Unlock()

	t.start = t.now
	t.invalidate()
}

// SetDuration changes the duration of the timer.
func (t *Timer) SetDuration(duration time.Duration) {
	t.mu.Lock()
	defer t.mu.Unlock()

	if t.duration == duration {
		return
	}
	t.duration = duration
	t.invalidate()
}

// Info returns the latest know info about the timer.
func (t *Timer) Info() (info Info) {
	t.mu.Lock()
	defer t.mu.Unlock()

	info.Progress = t.now.Sub(t.start)
	info.Duration = t.duration
	if info.Progress > info.Duration {
		info.Progress = info.Duration
	}
	return info
}

// Info is the information about the timer.
type Info struct {
	Progress time.Duration
	Duration time.Duration
}

// ProgressString returns the progress formatted as seconds.
func (info *Info) ProgressString() string {
	return fmt.Sprintf("%.1fs", info.Progress.Seconds())
}