package tui

import (
	"context"
	"errors"
	"fmt"
	"math"
	"sync"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

type updateType int

const (
	other updateType = iota
	moveToPrevPage
	moveToNextPage
	moveUp
	moveDown
	moveHome
	moveEnd
)

type RecordsView struct {
	*tview.Box

	mu sync.RWMutex

	onUnmount func()

	bucket  *Bucket
	records []*Record

	buffer chan *Record

	firstRecordIndex    int
	lastRecordIndex     int
	selectedRecordIndex int

	updateType updateType

	ui     *UI
	filter *Filter
}

func NewRecordsView(ui *UI, bucket *Bucket, filter *Filter) *RecordsView {
	return &RecordsView{
		Box:    tview.NewBox(),
		bucket: bucket,
		ui:     ui,
		filter: filter,
	}
}

func (v *RecordsView) Mount(ctx context.Context) error {
	if v.onUnmount != nil {
		return errors.New("try to mount already mounted component")
	}

	ctx, v.onUnmount = context.WithCancel(ctx)

	tempBuffer, err := LoadRecords(ctx, v.ui.db, v.bucket.Path, v.ui.loadBufferSize)
	if err != nil {
		return err
	}

	v.buffer = make(chan *Record, v.ui.loadBufferSize)
	go func() {
		defer close(v.buffer)

		for item := range tempBuffer {
			if item.err != nil {
				v.ui.stopOnError(err)
				break
			}
			record := item.val

			record.Entry, _, err = v.bucket.NextParser(record.Key, record.Value)
			if err != nil {
				v.ui.stopOnError(err)
				break
			}

			if v.filter.Apply(record.Entry).Result() != common.Yes {
				continue
			}

			v.buffer <- record
		}
	}()

	return nil
}

func (v *RecordsView) Unmount() {
	if v.onUnmount == nil {
		panic("try to unmount not mounted component")
	}
	v.onUnmount()
	v.onUnmount = nil
}

func (v *RecordsView) Update(ctx context.Context) error {
	_, _, _, recordsPerPage := v.GetInnerRect()
	firstRecordIndex, lastRecordIndex, selectedRecordIndex := v.getNewIndexes()

loop:
	for len(v.records) < lastRecordIndex {
		select {
		case <-ctx.Done():
			return nil
		case record, ok := <-v.buffer:
			if !ok {
				break loop
			}
			v.records = append(v.records, record)
		}
	}

	// Set the update type to its default value after some specific key event
	// has been handled.
	v.updateType = other

	firstRecordIndex = max(0, min(firstRecordIndex, len(v.records)-recordsPerPage))
	lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records))
	selectedRecordIndex = min(selectedRecordIndex, lastRecordIndex-1)

	v.mu.Lock()
	v.firstRecordIndex = firstRecordIndex
	v.lastRecordIndex = lastRecordIndex
	v.selectedRecordIndex = selectedRecordIndex
	v.mu.Unlock()

	return nil
}

func (v *RecordsView) getNewIndexes() (int, int, int) {
	v.mu.RLock()
	firstRecordIndex := v.firstRecordIndex
	lastRecordIndex := v.lastRecordIndex
	selectedRecordIndex := v.selectedRecordIndex
	v.mu.RUnlock()

	_, _, _, recordsPerPage := v.GetInnerRect()

	switch v.updateType {
	case moveUp:
		if selectedRecordIndex != firstRecordIndex {
			selectedRecordIndex--
			break
		}
		firstRecordIndex = max(0, firstRecordIndex-1)
		lastRecordIndex = min(firstRecordIndex+recordsPerPage, len(v.records))
		selectedRecordIndex = firstRecordIndex
	case moveToPrevPage:
		if selectedRecordIndex != firstRecordIndex {
			selectedRecordIndex = firstRecordIndex
			break
		}
		firstRecordIndex = max(0, firstRecordIndex-recordsPerPage)
		lastRecordIndex = firstRecordIndex + recordsPerPage
		selectedRecordIndex = firstRecordIndex
	case moveDown:
		if selectedRecordIndex != lastRecordIndex-1 {
			selectedRecordIndex++
			break
		}
		firstRecordIndex++
		lastRecordIndex++
		selectedRecordIndex++
	case moveToNextPage:
		if selectedRecordIndex != lastRecordIndex-1 {
			selectedRecordIndex = lastRecordIndex - 1
			break
		}
		firstRecordIndex += recordsPerPage
		lastRecordIndex = firstRecordIndex + recordsPerPage
		selectedRecordIndex = lastRecordIndex - 1
	case moveHome:
		firstRecordIndex = 0
		lastRecordIndex = firstRecordIndex + recordsPerPage
		selectedRecordIndex = 0
	case moveEnd:
		lastRecordIndex = math.MaxInt32
		firstRecordIndex = lastRecordIndex - recordsPerPage
		selectedRecordIndex = lastRecordIndex - 1
	default:
		lastRecordIndex = firstRecordIndex + recordsPerPage
	}

	return firstRecordIndex, lastRecordIndex, selectedRecordIndex
}

func (v *RecordsView) GetInnerRect() (int, int, int, int) {
	x, y, width, height := v.Box.GetInnerRect()

	// Left padding.
	x = min(x+3, x+width-1)
	width = max(width-3, 0)

	return x, y, width, height
}

func (v *RecordsView) Draw(screen tcell.Screen) {
	v.mu.RLock()
	firstRecordIndex := v.firstRecordIndex
	lastRecordIndex := v.lastRecordIndex
	selectedRecordIndex := v.selectedRecordIndex
	records := v.records
	v.mu.RUnlock()

	v.DrawForSubclass(screen, v)

	x, y, width, height := v.GetInnerRect()
	if height == 0 {
		return
	}

	// No records in that bucket.
	if firstRecordIndex == lastRecordIndex {
		tview.Print(
			screen, "Empty Bucket", x, y, width, tview.AlignCenter, tview.Styles.PrimaryTextColor,
		)
		return
	}

	for index := firstRecordIndex; index < lastRecordIndex; index++ {
		result := records[index].Entry
		text := result.String()

		if index == selectedRecordIndex {
			text = fmt.Sprintf("[:white]%s[:-]", text)
			tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimitiveBackgroundColor)
		} else {
			tview.Print(screen, text, x, y, width, tview.AlignLeft, tview.Styles.PrimaryTextColor)
		}

		y++
	}
}

func (v *RecordsView) InputHandler() func(event *tcell.EventKey, _ func(p tview.Primitive)) {
	return v.WrapInputHandler(func(event *tcell.EventKey, _ func(p tview.Primitive)) {
		switch m, k := event.Modifiers(), event.Key(); {
		case m == 0 && k == tcell.KeyPgUp:
			v.updateType = moveToPrevPage
		case m == 0 && k == tcell.KeyPgDn:
			v.updateType = moveToNextPage
		case m == 0 && k == tcell.KeyUp:
			v.updateType = moveUp
		case m == 0 && k == tcell.KeyDown:
			v.updateType = moveDown
		case m == 0 && k == tcell.KeyHome:
			v.updateType = moveHome
		case m == 0 && k == tcell.KeyEnd:
			v.updateType = moveEnd
		case k == tcell.KeyEnter:
			v.mu.RLock()
			selectedRecordIndex := v.selectedRecordIndex
			records := v.records
			v.mu.RUnlock()
			if len(records) != 0 {
				current := records[selectedRecordIndex]
				v.ui.moveNextPage(NewDetailedView(current.Entry.DetailedString()))
			}
		}
	})
}