// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>

package tui

import (
	"fmt"
	"os"
	"strings"
	"sync"
	"time"

	"kitty/tools/tui/loop"
	"kitty/tools/utils"
	"kitty/tools/utils/humanize"
)

var _ = fmt.Print

type dl_data struct {
	mutex               sync.Mutex
	canceled_by_user    bool
	error_from_download error
	done, total         uint64
	download_started    bool
	download_finished   bool
	temp_file_path      string
}

type render_data struct {
	done, total  uint64
	screen_width int
	spinner      *Spinner
	started_at   time.Time
}

func render_without_total(rd *render_data) string {
	return fmt.Sprint(rd.spinner.Tick(), humanize.Bytes(rd.done), " downloaded so far. Started %s", humanize.Time(rd.started_at))
}

func format_time(d time.Duration) string {
	d = d.Round(time.Second)
	ans := ""
	if d.Hours() > 1 {
		h := d / time.Hour
		d -= h * time.Hour
		ans += fmt.Sprintf("%02d:", h)
	}
	m := d / time.Minute
	d -= m * time.Minute
	s := d / time.Second
	return fmt.Sprintf("%s%02d:%02d", ans, m, s)
}

func render_progress(rd *render_data) string {
	if rd.total == 0 {
		return render_without_total(rd)
	}
	now := time.Now()
	duration := now.Sub(rd.started_at)
	rate := float64(rd.done) / float64(duration)
	frac := float64(rd.done) / float64(rd.total)
	bytes_left := rd.total - rd.done
	time_left := time.Duration(float64(bytes_left) / rate)
	speed := rate * float64(time.Second)
	before := rd.spinner.Tick()
	after := fmt.Sprintf(" %d%% %s/s %s", int(frac*100), strings.ReplaceAll(humanize.Bytes(uint64(speed)), " ", ""), format_time(time_left))
	available_width := rd.screen_width - len("T  100% 1000 MB/s 11:11:11")
	// fmt.Println("\r\n", frac, available_width)
	progress_bar := ""
	if available_width > 10 {
		progress_bar = " " + RenderProgressBar(frac, available_width)
	}
	return before + progress_bar + after
}

func DownloadFileWithProgress(destpath, url string, kill_if_signaled bool) (err error) {
	lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
	if err != nil {
		return
	}
	dl_data := dl_data{}
	rd := render_data{spinner: NewSpinner("dots"), started_at: time.Now()}

	register_temp_file_path := func(path string) {
		dl_data.mutex.Lock()
		dl_data.temp_file_path = path
		dl_data.mutex.Unlock()
	}

	report_progress := func(done, total uint64) error {
		dl_data.mutex.Lock()
		dl_data.done = done
		dl_data.total = total
		canceled := dl_data.canceled_by_user
		dl_data.mutex.Unlock()
		if canceled {
			return Canceled
		}
		lp.WakeupMainThread()
		return nil
	}

	do_download := func() {
		dl_data.mutex.Lock()
		dl_data.download_started = true
		dl_data.mutex.Unlock()
		err := utils.DownloadToFile(destpath, url, report_progress, register_temp_file_path)
		dl_data.mutex.Lock()
		dl_data.download_finished = true
		if err != Canceled && err != nil {
			dl_data.error_from_download = err
		}
		dl_data.mutex.Unlock()
		lp.WakeupMainThread()
	}

	redraw := func() {
		lp.StartAtomicUpdate()
		lp.AllowLineWrapping(false)
		defer func() {
			lp.AllowLineWrapping(true)
			lp.EndAtomicUpdate()
		}()
		lp.QueueWriteString("\r")
		lp.ClearToEndOfLine()
		dl_data.mutex.Lock()
		rd.done, rd.total = dl_data.done, dl_data.total
		dl_data.mutex.Unlock()
		if rd.done+rd.total == 0 {
			lp.QueueWriteString("Waiting for download to start...")
		} else {
			sz, err := lp.ScreenSize()
			w := sz.WidthCells
			if err != nil {
				w = 80
			}
			rd.screen_width = int(w)
			lp.QueueWriteString(render_progress(&rd))
		}
	}

	lp.OnInitialize = func() (string, error) {
		go do_download()
		lp.QueueWriteString("Downloading: " + url + "\r\n")
		return "\r\n", nil
	}

	lp.OnResumeFromStop = func() error {
		redraw()
		return nil
	}
	lp.OnResize = func(old_size, new_size loop.ScreenSize) error {
		redraw()
		return nil
	}
	lp.OnWakeup = func() error {
		dl_data.mutex.Lock()
		err := dl_data.error_from_download
		finished := dl_data.download_finished
		dl_data.mutex.Unlock()
		if err != nil {
			return dl_data.error_from_download
		}
		if finished {
			lp.Quit(0)
			return nil
		}
		redraw()
		return nil
	}
	lp.OnKeyEvent = func(event *loop.KeyEvent) error {
		if event.MatchesPressOrRepeat("ctrl+c") || event.MatchesPressOrRepeat("esc") {
			event.Handled = true
			dl_data.mutex.Lock()
			dl_data.canceled_by_user = true
			dl_data.mutex.Unlock()
			return Canceled
		}
		return nil
	}

	on_timer_tick := func(timer_id loop.IdType) error {
		return lp.OnWakeup()
	}

	lp.AddTimer(rd.spinner.interval, true, on_timer_tick)
	err = lp.Run()
	dl_data.mutex.Lock()
	if dl_data.temp_file_path != "" && !dl_data.download_finished {
		os.Remove(dl_data.temp_file_path)
	}
	dl_data.mutex.Unlock()
	if err != nil {
		return
	}
	ds := lp.DeathSignalName()
	if ds != "" {
		if kill_if_signaled {
			lp.KillIfSignalled()
			return
		}
		return &KilledBySignal{Msg: fmt.Sprint("Killed by signal: ", ds), SignalName: ds}
	}

	return
}
