package choose_files

import (
	"fmt"
	"io/fs"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"unicode/utf8"

	"github.com/kovidgoyal/go-parallel"
	"github.com/kovidgoyal/kitty/tools/highlight"
	"github.com/kovidgoyal/kitty/tools/icons"
	"github.com/kovidgoyal/kitty/tools/tui/loop"
	"github.com/kovidgoyal/kitty/tools/utils"
	"github.com/kovidgoyal/kitty/tools/utils/humanize"
	"github.com/kovidgoyal/kitty/tools/utils/style"
	"github.com/kovidgoyal/kitty/tools/wcswidth"
)

var _ = fmt.Print

type Preview interface {
	Render(h *Handler, x, y, width, height int)
	IsValidForColorScheme(light bool) bool
	Unload()
	IsReady() bool
	String() string
}

type PreviewManager struct {
	report_errors    chan error
	settings         Settings
	WakeupMainThread func() bool
	cache            map[string]Preview
	lock             sync.Mutex
	highlighter      highlight.Highlighter
}

func NewPreviewManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) (ans *PreviewManager) {
	defer func() { sanitize = ans.highlighter.Sanitize }()
	return &PreviewManager{
		report_errors: err_chan, settings: settings, WakeupMainThread: WakeupMainThread,
		cache: make(map[string]Preview), highlighter: highlight.NewHighlighter(nil),
	}
}

func (pm *PreviewManager) cached_preview(path string) Preview {
	pm.lock.Lock()
	defer pm.lock.Unlock()
	return pm.cache[path]
}

func (pm *PreviewManager) set_cached_preview(path string, val Preview) {
	pm.lock.Lock()
	defer pm.lock.Unlock()
	pm.cache[path] = val
}

func (h *Handler) render_wrapped_text_in_region(text string, x, y, width, height int, centered bool) int {
	lines := style.WrapTextAsLines(text, width, style.WrapOptions{})
	for i, line := range lines {
		extra := 0
		if centered {
			extra = max(0, width-wcswidth.Stringwidth(line)) / 2
		}
		h.lp.MoveCursorTo(x+extra, y+i)
		h.lp.QueueWriteString(line)
		if i >= height {
			break
		}
	}
	return len(lines)
}

type MessagePreview struct {
	title    string
	msg      string
	trailers []string
}

func (p MessagePreview) String() string                  { return fmt.Sprintf("MessagePreview{%#v}", p.title) }
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
func (p MessagePreview) IsReady() bool                   { return true }

func (p MessagePreview) Unload() {}
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
	offset := 0
	if p.title != "" {
		offset += h.render_wrapped_text_in_region(p.title, x, y, width, height, true)
	}
	offset += h.render_wrapped_text_in_region(p.msg, x, y+offset, width, height-offset, false)
	limit := height - offset
	if limit > 1 {
		for i, line := range p.trailers {
			text := wcswidth.TruncateToVisualLength(line, width-1)
			if len(text) < len(line) {
				text += "…"
			}
			h.lp.MoveCursorTo(x, y+offset+i-1)
			if i >= limit {
				h.lp.QueueWriteString("…")
				break
			}
			h.lp.QueueWriteString(text)
		}
	}
}

func NewErrorPreview(err error) Preview {
	sctx := style.Context{AllowEscapeCodes: true}
	text := fmt.Sprintf("%s: %s", sctx.SprintFunc("fg=red")("Error"), err)
	return &MessagePreview{msg: text}
}

var sanitize func(string) string

func write_file_metadata(abspath string, metadata fs.FileInfo, entries []fs.DirEntry) (header string, trailers []string) {
	buf := strings.Builder{}
	buf.Grow(4096)
	add := func(key, val string) { fmt.Fprintf(&buf, "%s: %s\n", key, val) }
	ftype := metadata.Mode().Type()
	const file_icon = " "
	fmt.Fprintln(&buf, filepath.Base(abspath))
	switch ftype {
	case 0:
		add("Size", humanize.Bytes(uint64(metadata.Size())))
	case fs.ModeSymlink:
		if tgt, err := os.Readlink(abspath); err == nil {
			add("Target", sanitize(tgt))
		} else {
			add("Target", err.Error())
		}
	case fs.ModeDir:
		num_files, num_dirs := 0, 0
		for _, e := range entries {
			if e.IsDir() {
				num_dirs++
			} else {
				num_files++
			}
		}
		add("Children", fmt.Sprintf("%d %s  %d %s", num_dirs, icons.IconForFileWithMode("dir", fs.ModeDir, false), num_files, file_icon))
	}
	add("Modified", humanize.Time(metadata.ModTime()))
	add("Mode", metadata.Mode().String())
	if len(entries) > 0 {
		type entry struct {
			lname string
			ftype fs.FileMode
		}
		type_map := make(map[string]entry, len(entries))
		for _, e := range entries {
			type_map[e.Name()] = entry{strings.ToLower(e.Name()), e.Type()}
		}
		names := utils.Map(func(e fs.DirEntry) string { return e.Name() }, entries)
		slices.SortFunc(names, func(a, b string) int { return strings.Compare(type_map[a].lname, type_map[b].lname) })
		fmt.Fprintln(&buf, "Contents:")
		for _, n := range names {
			trailers = append(trailers, icons.IconForFileWithMode(n, type_map[n].ftype, false)+"  "+sanitize(n))
		}
	}
	return buf.String(), trailers
}

func NewDirectoryPreview(abspath string, metadata fs.FileInfo) Preview {
	entries, err := os.ReadDir(abspath)
	if err != nil {
		return NewErrorPreview(fmt.Errorf("failed to read the directory %s with error: %w", abspath, err))
	}
	title := icons.IconForFileWithMode("dir", fs.ModeDir, false) + "  Directory\n"
	header, extra := write_file_metadata(abspath, metadata, entries)
	return &MessagePreview{title: title, msg: header, trailers: extra}
}

func NewFileMetadataPreview(abspath string, metadata fs.FileInfo) *MessagePreview {
	ext := filepath.Ext(abspath)
	if ext == "" {
		ext = "File"
	}
	title := icons.IconForFileWithMode(filepath.Base(abspath), metadata.Mode().Type(), false) + "  " + ext
	h, t := write_file_metadata(abspath, metadata, nil)
	return &MessagePreview{title: title, msg: h, trailers: t}
}

func NewFileMetadataPreviewWithError(abspath string, metadata fs.FileInfo, err error) *MessagePreview {
	ext := filepath.Ext(abspath)
	if ext == "" {
		ext = "File"
	}
	title := icons.IconForFileWithMode(filepath.Base(abspath), metadata.Mode().Type(), false) + "  " + ext
	h, t := write_file_metadata(abspath, metadata, nil)
	ans := &MessagePreview{title: title, msg: h, trailers: t}
	lines := style.WrapTextAsLines(err.Error(), 30, style.WrapOptions{})
	ans.trailers = append(ans.trailers, "")
	ans.trailers = append(ans.trailers, lines...)
	return ans
}

type highlighed_data struct {
	text  string
	light bool
	err   error
}

type TextFilePreview struct {
	plain_text, highlighted_text string
	highlighted_chan             chan highlighed_data
	ready                        atomic.Bool
	light                        bool
	path                         string
	metadata                     fs.FileInfo
}

func (p *TextFilePreview) String() string {
	return fmt.Sprintf("TextFilePreview{%#v, ready: %v}", p.path, p.ready.Load())
}

func (p *TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }

func (p *TextFilePreview) Unload() {}

func (p *TextFilePreview) IsReady() bool { return p.ready.Load() || p.highlighted_chan == nil }
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
	if p.highlighted_chan != nil {
		select {
		case hd := <-p.highlighted_chan:
			p.highlighted_chan = nil
			if hd.err == nil {
				p.highlighted_text = hd.text
			}
		default:
		}
	}
	text := p.highlighted_text
	if text == "" {
		text = p.plain_text
	}
	s := utils.NewLineScanner(text)
	buf := strings.Builder{}
	buf.Grow(1024 * height)
	title := icons.IconForPath(p.path) + "  " + filepath.Base(p.path) + fmt.Sprintf(" %s", humanize.Bytes(uint64(p.metadata.Size())))
	for num := 1 + h.render_wrapped_text_in_region(title, x, y, width, height, true); s.Scan() && num < height; num++ {
		line := s.Text()
		truncated := wcswidth.TruncateToVisualLength(line, width)
		buf.WriteString(fmt.Sprintf(loop.MoveCursorToTemplate, y+num, x))
		buf.WriteString(truncated)
		if len(truncated) < len(line) {
			wcswidth.KeepOnlyCSI(line[len(truncated):], &buf)
		}
	}
	buf.WriteString("\x1b[m") // reset any highlight styles
	h.lp.QueueWriteString(buf.String())
}

func NewTextFilePreview(abspath string, metadata fs.FileInfo, highlighted_chan chan highlighed_data, sanitize func(string) string) Preview {
	data, err := os.ReadFile(abspath)
	if err != nil {
		return NewFileMetadataPreview(abspath, metadata)
	}
	text := utils.UnsafeBytesToString(data)
	if !utf8.ValidString(text) {
		text = "Error: not valid utf-8 text"
	}
	return &TextFilePreview{
		path: abspath, plain_text: sanitize(text), highlighted_chan: highlighted_chan, light: use_light_colors, metadata: metadata}
}

type style_resolver struct {
	light                   bool
	light_style, dark_style string
	syntax_aliases          map[string]string
}

func (s style_resolver) StyleName() string {
	return utils.IfElse(s.light, s.light_style, s.dark_style)
}
func (s style_resolver) UseLightColors() bool             { return s.light }
func (s style_resolver) SyntaxAliases() map[string]string { return s.syntax_aliases }
func (s style_resolver) TextForPath(path string) (string, error) {
	ans, err := os.ReadFile(path)
	if err == nil {
		return utils.UnsafeBytesToString(ans), nil
	}
	return "", err
}

func (pm *PreviewManager) highlight_file_async(path string, output chan highlighed_data, ready *atomic.Bool) {
	s := style_resolver{light: use_light_colors, syntax_aliases: pm.settings.SyntaxAliases()}
	s.light_style, s.dark_style = pm.settings.HighlightStyles()
	go func() {
		defer func() {
			if r := recover(); r != nil {
				err := parallel.Format_stacktrace_on_panic(r, 1)
				output <- highlighed_data{err: err, light: s.light}
			}
			close(output)
			ready.Store(true)
			pm.WakeupMainThread()
		}()
		highlighted, err := pm.highlighter.HighlightFile(path, &s)
		output <- highlighed_data{text: highlighted, err: err, light: s.light}
	}()
}

func (pm *PreviewManager) invalidate_color_scheme_based_cached_items() {
	pm.lock.Lock()
	defer pm.lock.Unlock()
	maps.DeleteFunc(pm.cache, func(key string, p Preview) bool { return !p.IsValidForColorScheme(use_light_colors) })
}

func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Preview) {
	if ans = pm.cached_preview(abspath); ans != nil {
		return ans
	}
	defer func() { pm.set_cached_preview(abspath, ans) }()
	s, err := os.Lstat(abspath)
	if err != nil {
		return NewErrorPreview(err)
	}
	if s.IsDir() {
		return NewDirectoryPreview(abspath, s)
	}
	if ftype&fs.ModeSymlink != 0 && ftype&SymlinkToDir != 0 {
		s, err = os.Stat(abspath)
		if err != nil {
			return NewErrorPreview(err)
		}
		return NewDirectoryPreview(abspath, s)
	}
	fname := filepath.Base(abspath)
	mt := utils.GuessMimeType(fname)
	for _, q := range previewers {
		if q.matches(fname, mt) {
			return NewCmdPreview(abspath, s, pm.settings, pm.WakeupMainThread, q)
		}
	}
	const MAX_TEXT_FILE_SIZE = 16 * 1024 * 1024
	if s.Size() <= MAX_TEXT_FILE_SIZE && (utils.KnownTextualMimes[mt] || strings.HasPrefix(mt, "text/")) {
		ch := make(chan highlighed_data, 2)
		ans := NewTextFilePreview(abspath, s, ch, pm.highlighter.Sanitize)
		if p, ok := ans.(*TextFilePreview); ok {
			pm.highlight_file_async(abspath, ch, &p.ready)
		}
		return ans
	}
	switch {
	case strings.HasPrefix(mt, "image/"):
		var r ImagePreviewRenderer
		if ans, err := NewImagePreview(abspath, s, pm.settings, pm.WakeupMainThread, r); err == nil {
			return ans
		} else {
			return NewErrorPreview(err)
		}

	case strings.HasPrefix(mt, "video/"):
		return NewFFMpegPreview(abspath, s, pm.settings, pm.WakeupMainThread)

	case IsSupportedArchiveFile(abspath):
		return NewArchivePeview(abspath, s, pm.settings, pm.WakeupMainThread)

	case IsSupportedByCalibre(abspath):
		return NewCalibrePreview(abspath, s, pm.settings, pm.WakeupMainThread)
	}
	return NewFileMetadataPreview(abspath, s)
}

func (h *Handler) clear_cached_previews() {
	if h.last_rendered_preview != nil {
		h.last_rendered_preview.Unload()
		h.last_rendered_preview = nil
	}
	if h.prev_preview_for_smooth_transition != nil {
		h.prev_preview_for_smooth_transition.Unload()
		h.prev_preview_for_smooth_transition = nil
	}
}

func (h *Handler) draw_preview_content(x, y, width, height int) {
	matches, _ := h.get_results()
	r := matches.At(h.state.CurrentIndex())
	if r == nil {
		h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
		return
	}
	render := func() {
		p := h.last_rendered_preview
		if p.IsReady() || h.prev_preview_for_smooth_transition == nil {
			p.Render(h, x, y, width, height)
			if h.prev_preview_for_smooth_transition != nil {
				h.prev_preview_for_smooth_transition.Unload()
				h.prev_preview_for_smooth_transition = nil
			}
		} else {
			h.prev_preview_for_smooth_transition.Render(h, x, y, width, height)
		}
	}

	abspath := h.current_abspath()
	if h.last_rendered_preview != nil {
		if abspath == h.last_rendered_preview_abspath {
			render()
			return
		}
		if h.prev_preview_for_smooth_transition != nil {
			h.prev_preview_for_smooth_transition.Unload()
		}
		h.prev_preview_for_smooth_transition = h.last_rendered_preview
		h.last_rendered_preview = nil
	}
	if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
		h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
	} else {
		h.last_rendered_preview = p
		h.last_rendered_preview_abspath = abspath
		render()
	}
}
