package display

import (
	"fmt"
	"net/url"
	"path"
	"strconv"
	"strings"

	"code.rocketnine.space/tslocum/cview"
	"github.com/atotto/clipboard"
	"github.com/gdamore/tcell/v2"
	"github.com/makeworld-the-better-one/amfora/config"
	"github.com/makeworld-the-better-one/amfora/structs"
)

type tabMode int

const (
	tabModeDone tabMode = iota
	tabModeLoading
)

// tabHistoryPageCache is fields from the Page struct, cached here to solve #122
// See structs/structs.go for an explanation of the fields.
type tabHistoryPageCache struct {
	row        int
	column     int
	selected   string
	selectedID string
	mode       structs.PageMode
}

type tabHistory struct {
	urls      []string
	pos       int // Position: where in the list of URLs we are
	pageCache []*tabHistoryPageCache
}

// tab hold the information needed for each browser tab.
type tab struct {
	page             *structs.Page
	view             *cview.TextView
	history          *tabHistory
	mode             tabMode
	barLabel         string // The bottomBar label for the tab
	barText          string // The bottomBar text for the tab
	preferURLHandler bool   // For #143, use URL handler over proxy
}

// makeNewTab initializes an tab struct with no content.
func makeNewTab() *tab {
	t := tab{
		page:    &structs.Page{Mode: structs.ModeOff},
		view:    cview.NewTextView(),
		history: &tabHistory{},
		mode:    tabModeDone,
	}
	t.view.SetDynamicColors(true)
	t.view.SetRegions(true)
	t.view.SetScrollable(true)
	t.view.SetWrap(false)
	t.view.SetScrollBarVisibility(config.ScrollBar)
	t.view.SetScrollBarColor(config.GetColor("scrollbar"))
	t.view.SetChangedFunc(func() {
		App.Draw()
	})
	t.view.SetDoneFunc(func(key tcell.Key) {
		// Altered from:
		// https://gitlab.com/tslocum/cview/-/blob/1f765c8695c3f4b35dae57f469d3aee0b1adbde7/demos/textview/main.go
		// Handles being able to select and "click" links with the enter and tab keys

		tab := curTab // Don't let it change in the middle of the code

		if tabs[tab].mode != tabModeDone {
			return
		}

		if key == tcell.KeyEsc {
			// Stop highlighting
			bottomBar.SetLabel("")
			bottomBar.SetText(tabs[tab].page.URL)
			tabs[tab].clearSelected()
			tabs[tab].saveBottomBar()
			return
		}

		if len(tabs[tab].page.Links) == 0 {
			// No links on page
			return
		}

		currentSelection := tabs[tab].view.GetHighlights()
		numSelections := len(tabs[tab].page.Links)

		if key == tcell.KeyEnter && len(currentSelection) > 0 {
			// A link is selected and enter was pressed: "click" it and load the page it's for
			bottomBar.SetLabel("")
			linkN, _ := strconv.Atoi(currentSelection[0])
			tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
			tabs[tab].page.SelectedID = currentSelection[0]
			tabs[tab].preferURLHandler = false // Reset in case
			go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
			return
		}
		if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) {
			// They've started link highlighting
			tabs[tab].page.Mode = structs.ModeLinkSelect

			tabs[tab].view.Highlight("0")
			tabs[tab].scrollToHighlight()
			// Display link URL in bottomBar
			bottomBar.SetLabel("[::b]Link: [::-]")
			bottomBar.SetText(tabs[tab].page.Links[0])
			tabs[tab].saveBottomBar()
			tabs[tab].page.Selected = tabs[tab].page.Links[0]
			tabs[tab].page.SelectedID = "0"
		}

		if len(currentSelection) > 0 {
			// There's still a selection, but a different key was pressed, not Enter

			index, _ := strconv.Atoi(currentSelection[0])
			if key == tcell.KeyTab {
				index = (index + 1) % numSelections
			} else if key == tcell.KeyBacktab {
				index = (index - 1 + numSelections) % numSelections
			} else {
				return
			}
			tabs[tab].view.Highlight(strconv.Itoa(index))
			tabs[tab].scrollToHighlight()
			// Display link URL in bottomBar
			bottomBar.SetLabel("[::b]Link: [::-]")
			bottomBar.SetText(tabs[tab].page.Links[index])
			tabs[tab].saveBottomBar()
			tabs[tab].page.Selected = tabs[tab].page.Links[index]
			tabs[tab].page.SelectedID = strconv.Itoa(index)
		}
	})
	t.view.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		// Capture scrolling and change the left margin size accordingly, see #197
		// This was also touched by #222
		// This also captures any tab-specific events now

		if t.mode != tabModeDone {
			// Any events that should be caught when the tab is loading is handled in display.go
			return nil
		}

		cmd := config.TranslateKeyEvent(event)

		// Cmds that aren't single row/column scrolling
		//nolint:exhaustive
		switch cmd {
		case config.CmdBookmarks:
			Bookmarks(&t)
			t.addToHistory("about:bookmarks")
			return nil
		case config.CmdAddBookmark:
			go addBookmark()
			return nil
		case config.CmdPgup:
			t.pageUp()
			return nil
		case config.CmdPgdn:
			t.pageDown()
			return nil
		case config.CmdSave:
			if t.hasContent() {
				savePath, err := downloadPage(t.page)
				if err != nil {
					go Error("Download Error", fmt.Sprintf("Error saving page content: %v", err))
				} else {
					go Info(fmt.Sprintf("Page content saved to %s. ", savePath))
				}
			} else {
				go Info("The current page has no content, so it couldn't be downloaded.")
			}
			return nil
		case config.CmdBack:
			histBack(&t)
			return nil
		case config.CmdForward:
			histForward(&t)
			return nil
		case config.CmdSub:
			Subscriptions(&t, "about:subscriptions")
			tabs[curTab].addToHistory("about:subscriptions")
			return nil
		case config.CmdCopyPageURL:
			currentURL := tabs[curTab].page.URL
			err := clipboard.WriteAll(currentURL)
			if err != nil {
				go Error("Copy Error", err.Error())
				return nil
			}
			return nil
		case config.CmdCopyTargetURL:
			currentURL := t.page.URL
			selectedURL := t.highlightedURL()
			if selectedURL == "" {
				return nil
			}
			u, _ := url.Parse(currentURL)
			copiedURL, err := u.Parse(selectedURL)
			if err != nil {
				err := clipboard.WriteAll(selectedURL)
				if err != nil {
					go Error("Copy Error", err.Error())
					return nil
				}
				return nil
			}
			err = clipboard.WriteAll(copiedURL.String())
			if err != nil {
				go Error("Copy Error", err.Error())
				return nil
			}
			return nil
		case config.CmdURLHandlerOpen:
			currentSelection := t.view.GetHighlights()
			t.preferURLHandler = true
			// Copied code from when enter key is pressed
			if len(currentSelection) > 0 {
				bottomBar.SetLabel("")
				linkN, _ := strconv.Atoi(currentSelection[0])
				t.page.Selected = t.page.Links[linkN]
				t.page.SelectedID = currentSelection[0]
				go followLink(&t, t.page.URL, t.page.Links[linkN])
			}
			return nil
		}
		// Number key: 1-9, 0, LINK1-LINK10
		if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
			if int(cmd) <= len(t.page.Links) {
				// It's a valid link number
				t.preferURLHandler = false // Reset in case
				go followLink(&t, t.page.URL, t.page.Links[cmd-1])
				return nil
			}
		}

		// Scrolling stuff
		// Copied in scrollTo

		key := event.Key()
		mod := event.Modifiers()
		height, width := t.view.GetBufferSize()
		_, _, boxW, boxH := t.view.GetInnerRect()

		// Make boxW accurate by subtracting one if a scrollbar is covering the last
		// column of text
		if config.ScrollBar == cview.ScrollBarAlways ||
			(config.ScrollBar == cview.ScrollBarAuto && height > boxH) {
			boxW--
		}

		if cmd == config.CmdMoveRight || (key == tcell.KeyRight && mod == tcell.ModNone) {
			// Scrolling to the right

			if t.page.Column >= leftMargin() {
				// Scrolled right far enought that no left margin is needed
				if (t.page.Column-leftMargin())+boxW >= width {
					// And scrolled as far as possible to the right
					return nil
				}
			} else {
				// Left margin still exists
				if boxW-(leftMargin()-t.page.Column) >= width {
					// But still scrolled as far as possible
					return nil
				}
			}
			t.page.Column++
		} else if cmd == config.CmdMoveLeft || (key == tcell.KeyLeft && mod == tcell.ModNone) {
			// Scrolling to the left
			if t.page.Column == 0 {
				// Can't scroll to the left anymore
				return nil
			}
			t.page.Column--
		} else if cmd == config.CmdMoveUp || (key == tcell.KeyUp && mod == tcell.ModNone) {
			// Scrolling up
			if t.page.Row > 0 {
				t.page.Row--
			}
			return event
		} else if cmd == config.CmdMoveDown || (key == tcell.KeyDown && mod == tcell.ModNone) {
			// Scrolling down
			if t.page.Row < height {
				t.page.Row++
			}
			return event
		} else if cmd == config.CmdBeginning {
			t.page.Row = 0
			// This is required because cview will also set the column (incorrectly)
			// if it handles this event itself
			t.applyScroll()
			App.Draw()
			return nil
		} else if cmd == config.CmdEnd {
			t.page.Row = height
			t.applyScroll()
			App.Draw()
			return nil
		} else {
			// Some other key, stop processing it
			return event
		}

		t.applyHorizontalScroll()
		App.Draw()
		return nil
	})

	return &t
}

// historyCachePage caches certain info about the current page in the tab's history,
// see #122 for details.
func (t *tab) historyCachePage() {
	if t.page == nil || t.page.URL == "" || t.history.pageCache == nil || len(t.history.pageCache) == 0 {
		return
	}
	t.history.pageCache[t.history.pos] = &tabHistoryPageCache{
		row:        t.page.Row,
		column:     t.page.Column,
		selected:   t.page.Selected,
		selectedID: t.page.SelectedID,
		mode:       t.page.Mode,
	}
}

// addToHistory adds the given URL to history.
// It assumes the URL is currently being loaded and displayed on the page.
func (t *tab) addToHistory(u string) {
	if t.history.pos < len(t.history.urls)-1 {
		// We're somewhere in the middle of the history instead, with URLs ahead and behind.
		// The URLs ahead need to be removed so this new URL is the most recent item in the history
		t.history.urls = t.history.urls[:t.history.pos+1]
		// Same for page cache
		t.history.pageCache = t.history.pageCache[:t.history.pos+1]
	}
	t.history.urls = append(t.history.urls, u)
	t.history.pos++

	// Cache page info for #122
	t.history.pageCache = append(t.history.pageCache, &tabHistoryPageCache{}) // Add new spot
	t.historyCachePage()                                                      // Fill it with data
}

// pageUp scrolls up 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageUp() {
	t.page.Row -= termH / 2
	if t.page.Row < 0 {
		t.page.Row = 0
	}
	t.applyScroll()
}

// pageDown scrolls down 75% of the height of the terminal, like Bombadillo.
func (t *tab) pageDown() {
	height, _ := t.view.GetBufferSize()

	t.page.Row += termH / 2
	if t.page.Row > height {
		t.page.Row = height
	}

	t.applyScroll()
}

// hasContent returns false when the tab's page is malformed,
// has no content or URL.
func (t *tab) hasContent() bool {
	if t.page == nil || t.view == nil {
		return false
	}
	if t.page.URL == "" {
		return false
	}
	if t.page.Content == "" {
		return false
	}
	return true
}

// isAnAboutPage returns true when the tab's page is an about page
func (t *tab) isAnAboutPage() bool {
	return strings.HasPrefix(t.page.URL, "about:")
}

// applyHorizontalScroll handles horizontal scroll logic including left margin resizing,
// see #197 for details. Use applyScroll instead.
//
// In certain cases it will still use and apply the saved Row.
func (t *tab) applyHorizontalScroll() {
	i := tabNumber(t)
	if i == -1 {
		// Tab is not actually being used and should not be (re)added to the browser
		return
	}
	if t.page.Column >= leftMargin() {
		// Scrolled to the right far enough that no left margin is needed
		browser.AddTab(
			strconv.Itoa(i),
			t.label(),
			makeContentLayout(t.view, 0),
		)
		t.view.ScrollTo(t.page.Row, t.page.Column-leftMargin())
	} else {
		// Left margin is still needed, but is not necessarily at the right size by default
		browser.AddTab(
			strconv.Itoa(i),
			t.label(),
			makeContentLayout(t.view, leftMargin()-t.page.Column),
		)
	}
}

// applyScroll applies the saved scroll values to the page and tab.
// It should only be used when going backward and forward.
func (t *tab) applyScroll() {
	t.view.ScrollTo(t.page.Row, 0)
	t.applyHorizontalScroll()
}

// scrollTo scrolls the current tab to specified position. Like
// cview.TextView.ScrollTo but using the custom scrolling logic required by #196.
func (t *tab) scrollTo(row, col int) {
	height, width := t.view.GetBufferSize()

	// Keep row and col within limits

	if row < 0 {
		row = 0
	} else if row > height {
		row = height
	}
	if col < 0 {
		col = 0
	} else if col > width {
		col = width
	}

	t.page.Row = row
	t.page.Column = col
	t.applyScroll()
	App.Draw()
}

// scrollToHighlight scrolls the current tab to specified position. Like
// cview.TextView.ScrollToHighlight but using the custom scrolling logic
// required by #196.
func (t *tab) scrollToHighlight() {
	t.view.ScrollToHighlight()
	App.Draw()
	t.scrollTo(t.view.GetScrollOffset())
}

// saveBottomBar saves the current bottomBar values in the tab.
func (t *tab) saveBottomBar() {
	t.barLabel = bottomBar.GetLabel()
	t.barText = bottomBar.GetText()
}

// applyBottomBar sets the bottomBar using the stored tab values
func (t *tab) applyBottomBar() {
	bottomBar.SetLabel(t.barLabel)
	bottomBar.SetText(t.barText)
}

// clearSelected turns off any selection that was going on.
// It does not affect the bottomBar.
func (t *tab) clearSelected() {
	t.page.Mode = structs.ModeOff
	t.page.Selected = ""
	t.page.SelectedID = ""
	t.view.Highlight("")
}

// applySelected selects whatever is stored as the selected element in the struct,
// and sets the mode accordingly.
// It is safe to call if nothing was selected previously.
//
// applyBottomBar should be called after, as this func might set some bottomBar values.
func (t *tab) applySelected() {
	if t.page.Mode == structs.ModeOff {
		// Just in case
		t.page.Selected = ""
		t.page.SelectedID = ""
		t.view.Highlight("")
		return
	} else if t.page.Mode == structs.ModeLinkSelect {
		t.view.Highlight(t.page.SelectedID)

		if t.mode == tabModeDone {
			// Page is not loading so bottomBar can change
			t.barLabel = "[::b]Link: [::-]"
			t.barText = t.page.Selected
		}
	}
}

// applyAll uses applyScroll and applySelected to put a tab's TextView back the way it was.
// It also uses applyBottomBar if this is the current tab.
func (t *tab) applyAll() {
	t.applySelected()
	t.applyScroll()
	if t == tabs[curTab] {
		t.applyBottomBar()
	}
}

// highlightedURL returns the currently selected URL
func (t *tab) highlightedURL() string {
	currentSelection := tabs[curTab].view.GetHighlights()

	if len(currentSelection) > 0 {
		linkN, _ := strconv.Atoi(currentSelection[0])
		selectedURL := tabs[curTab].page.Links[linkN]
		return selectedURL
	}
	return ""
}

// label returns the label to use for the tab name
func (t *tab) label() string {
	tn := tabNumber(t)
	if tn < 0 {
		// Invalid tab, shouldn't happen
		return ""
	}

	// Increment so there's no tab 0 in the label
	tn++

	if t.page.URL == "" || t.page.URL == "about:newtab" {
		// Just use tab number
		// Spaces around to keep original Amfora look
		return fmt.Sprintf(" %d ", tn)
	}
	if strings.HasPrefix(t.page.URL, "about:") {
		// Don't look for domain, put the whole URL except query strings
		return strings.SplitN(t.page.URL, "?", 2)[0]
	}
	if strings.HasPrefix(t.page.URL, "file://") {
		// File URL, use file or folder as tab name
		return cview.Escape(path.Base(t.page.URL[7:]))
	}
	// Otherwise, it's a Gemini URL
	pu, err := url.Parse(t.page.URL)
	if err != nil {
		return fmt.Sprintf(" %d ", tn)
	}
	return pu.Host
}
