/*
Copyright © 2025 Thomas von Dein

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package cmd

// pager setup using bubbletea
// file shamlelessly copied from:
// https://github.com/charmbracelet/bubbletea/tree/main/examples/pager

import (
	"bytes"
	"fmt"
	"image"
	"os"
	"strings"

	"github.com/blacktop/go-termimg"
	"github.com/charmbracelet/bubbles/help"
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/muesli/reflow/wordwrap"
	"golang.org/x/term"
)

var (
	titleStyle = func() lipgloss.Style {
		b := lipgloss.RoundedBorder()
		b.Right = "├"
		return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
	}()

	infoStyle = func() lipgloss.Style {
		b := lipgloss.RoundedBorder()
		b.Left = "┤"
		return titleStyle.BorderStyle(b)
	}()

	viewstyle = lipgloss.NewStyle()
)

const (
	MarginStep = 5
	MinSize    = 40
)

type Meta struct {
	lines           int
	currentline     int
	initialprogress int
}

type keyMap struct {
	Up       key.Binding
	Down     key.Binding
	Left     key.Binding
	Right    key.Binding
	Help     key.Binding
	Quit     key.Binding
	ToggleUI key.Binding
	Pad      key.Binding
}

func (k keyMap) FullHelp() [][]key.Binding {
	return [][]key.Binding{
		// every item is one column
		{k.Up, k.Down, k.Left, k.Right},
		{k.Pad}, // fake key, we use it as spacing between columns
		{k.Help, k.Quit, k.ToggleUI},
	}
}

func (k keyMap) ShortHelp() []key.Binding {
	return []key.Binding{k.Help, k.Quit}
}

var keys = keyMap{
	Pad: key.NewBinding(
		key.WithKeys("__"),
		key.WithHelp("  ", ""),
	),
	Up: key.NewBinding(
		key.WithKeys("up", "k"),
		key.WithHelp("↑", "move up"),
	),
	Down: key.NewBinding(
		key.WithKeys("down", "j"),
		key.WithHelp("↓", "move down"),
	),
	Left: key.NewBinding(
		key.WithKeys("left"),
		key.WithHelp("←", "decrease text width"),
	),
	Right: key.NewBinding(
		key.WithKeys("right"),
		key.WithHelp("→", "increase text width"),
	),
	Help: key.NewBinding(
		key.WithKeys("?"),
		key.WithHelp("?", "toggle help"),
	),
	Quit: key.NewBinding(
		key.WithKeys("q", "esc", "ctrl+c"),
		key.WithHelp("q", "quit"),
	),
	ToggleUI: key.NewBinding(
		key.WithKeys("h"),
		key.WithHelp("h", "toggle ui"),
	),
}

type Doc struct {
	content      string
	title        string
	ready        bool
	viewport     viewport.Model
	initialwidth int
	lastwidth    int
	margin       int
	marginMod    bool
	hideUi       bool
	meta         *Meta
	config       *Config
	Cover        *termimg.ImageWidget

	keys keyMap
	help help.Model
}

func (m Doc) Init() tea.Cmd {
	return nil
}

func (m Doc) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var (
		cmd  tea.Cmd
		cmds []tea.Cmd
	)

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.keys.Help):
			m.help.ShowAll = !m.help.ShowAll
		case key.Matches(msg, m.keys.Quit):
			return m, tea.Quit
		case key.Matches(msg, m.keys.Left):
			if m.lastwidth-(m.margin*2) >= MinSize {
				m.margin += MarginStep
				m.marginMod = true
			}
		case key.Matches(msg, m.keys.Right):
			if m.margin >= MarginStep {
				m.margin -= MarginStep
				m.marginMod = true
			}
		case key.Matches(msg, m.keys.ToggleUI):
			m.hideUi = !m.hideUi
		}

	case tea.WindowSizeMsg:
		headerHeight := lipgloss.Height(m.headerView())
		footerHeight := lipgloss.Height(m.footerView())
		verticalMarginHeight := headerHeight + footerHeight

		if !m.ready {
			// Since this program is using the full size of the viewport we
			// need to wait until we've received the window dimensions before
			// we can initialize the viewport. The initial dimensions come in
			// quickly, though asynchronously, which is why we wait for them
			// here.
			m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
			m.viewport.YPosition = headerHeight

			m.Rewrap()
			m.ready = true
		} else {
			m.viewport.Width = msg.Width
			m.viewport.Height = msg.Height - verticalMarginHeight
		}
	}

	m.Rewrap()

	// Handle keyboard and mouse events in the viewport
	m.viewport, cmd = m.viewport.Update(msg)
	cmds = append(cmds, cmd)

	return m, tea.Batch(cmds...)
}

// re-calculate word wrapping, add left margin, recalculate line count
func (m *Doc) Rewrap() {
	var content string

	// width has changed, either  because the terminal size changed or
	// because the user added some margin
	if m.lastwidth != m.viewport.Width || m.marginMod {
		content = wordwrap.String(m.content, m.viewport.Width-(m.margin*2))
		m.lastwidth = m.viewport.Width
		m.marginMod = false

		m.viewport.Style = viewstyle.MarginLeft(m.margin)
	}

	// bootstrapping, initialize with default width
	if !m.ready {
		content = wordwrap.String(m.content, m.initialwidth)
	}

	// wrapping has changed, update viewport and line count
	if content != "" {
		if m.Cover != nil {
			cover, _ := m.Cover.Render()
			m.viewport.SetContent(cover + "\n\n" + content)
		} else {
			m.viewport.SetContent(content)
		}
		m.meta.lines = len(strings.Split(content, "\n"))
	}

	// during bootstrapping: jump to last remembered position, if any
	if !m.ready {
		m.viewport.ScrollDown(m.meta.initialprogress)
	}
}

func (m Doc) View() string {
	if !m.ready {
		return "\n  Initializing..."
	}

	// update current line for later saving
	m.meta.currentline = int(float64(m.meta.lines) * m.viewport.ScrollPercent())

	var helpView string
	if m.help.ShowAll {
		helpView = "\n" + m.help.View(m.keys)
	}

	if m.hideUi {
		return fmt.Sprintf("%s\n%s", m.viewport.View(), helpView)
	}

	return fmt.Sprintf("%s\n%s\n%s%s", m.headerView(), m.viewport.View(), m.footerView(), helpView)
}

func (m Doc) headerView() string {
	title := m.config.Colors.Title.Render(m.title)
	line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
	return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}

func (m Doc) footerView() string {
	info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))

	line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

type Ebook struct {
	Config    *Config
	Title     string
	Body      string
	Cover     []byte
	MediaType string
}

func Pager(book *Ebook) (int, error) {
	width := 80
	scrollto := 0

	if term.IsTerminal(int(os.Stdout.Fd())) {
		w, _, err := term.GetSize(0)
		if err == nil {
			width = w
		}
	}

	if book.Config.StoreProgress {
		scrollto = book.Config.InitialProgress
	}

	if book.Config.LineNumbers {
		catn := ""
		for idx, line := range strings.Split(book.Body, "\n") {
			catn += fmt.Sprintf("%4d: %s\n", idx, line)
		}
		book.Body = catn
	}

	meta := Meta{
		initialprogress: scrollto,
		lines:           len(strings.Split(book.Body, "\n")),
	}

	doc := Doc{
		content:      book.Body,
		title:        book.Title,
		initialwidth: width,
		meta:         &meta,
		config:       book.Config,
		keys:         keys,
	}

	if book.MediaType != "" && book.Config.ShowCover {
		img, _, err := image.Decode(bytes.NewReader(book.Cover))
		if err != nil {
			return 0, fmt.Errorf("failed to load cover image: %w", err)
		}

		doc.Cover = termimg.NewImageWidgetFromImage(img)
		doc.Cover.SetSize(width, width).SetProtocol(termimg.Auto)
	}

	p := tea.NewProgram(
		doc,
		tea.WithAltScreen(),       // use the full size of the terminal in its "alternate screen buffer"
		tea.WithMouseCellMotion(), // turn on mouse support so we can track the mouse wheel
	)

	if _, err := p.Run(); err != nil {
		return 0, fmt.Errorf("could not run pager: %w", err)
	}

	if book.Config.Debug {
		fmt.Printf("scrollto: %d, last: %d, diff: %d\n",
			scrollto, meta.currentline, scrollto-meta.currentline)
	}

	return meta.currentline, nil
}
