package render

import (
	"fmt"
	"sort"

	"github.com/rs/zerolog/log"
)

const (
	// EventUnchanged notifies listener resource has not changed.
	EventUnchanged ResEvent = 1 << iota

	// EventAdd notifies listener of a resource was added.
	EventAdd

	// EventUpdate notifies listener of a resource updated.
	EventUpdate

	// EventDelete  notifies listener of a resource was deleted.
	EventDelete

	// EventClear the stack was reset.
	EventClear
)

// ResEvent represents a resource event.
type ResEvent int

// RowEvent tracks resource instance events.
type RowEvent struct {
	Kind   ResEvent
	Row    Row
	Deltas DeltaRow
}

// NewRowEvent returns a new row event.
func NewRowEvent(kind ResEvent, row Row) RowEvent {
	return RowEvent{
		Kind: kind,
		Row:  row,
	}
}

// NewRowEventWithDeltas returns a new row event with deltas.
func NewRowEventWithDeltas(row Row, delta DeltaRow) RowEvent {
	return RowEvent{
		Kind:   EventUpdate,
		Row:    row,
		Deltas: delta,
	}
}

// Clone returns a row event deep copy.
func (r RowEvent) Clone() RowEvent {
	return RowEvent{
		Kind:   r.Kind,
		Row:    r.Row.Clone(),
		Deltas: r.Deltas.Clone(),
	}
}

// Customize returns a new subset based on the given column indices.
func (r RowEvent) Customize(cols []int) RowEvent {
	delta := r.Deltas
	if !r.Deltas.IsBlank() {
		delta = make(DeltaRow, len(cols))
		r.Deltas.Customize(cols, delta)
	}

	return RowEvent{
		Kind:   r.Kind,
		Deltas: delta,
		Row:    r.Row.Customize(cols),
	}
}

// ExtractHeaderLabels extract collection of fields into header.
func (r RowEvent) ExtractHeaderLabels(labelCol int) []string {
	hh, _ := sortLabels(labelize(r.Row.Fields[labelCol]))
	return hh
}

// Labelize returns a new row event based on labels.
func (r RowEvent) Labelize(cols []int, labelCol int, labels []string) RowEvent {
	return RowEvent{
		Kind:   r.Kind,
		Deltas: r.Deltas.Labelize(cols, labelCol),
		Row:    r.Row.Labelize(cols, labelCol, labels),
	}
}

// Diff returns true if the row changed.
func (r RowEvent) Diff(re RowEvent, ageCol int) bool {
	if r.Kind != re.Kind {
		return true
	}
	if r.Deltas.Diff(re.Deltas, ageCol) {
		return true
	}
	return r.Row.Diff(re.Row, ageCol)
}

// ----------------------------------------------------------------------------

// RowEvents a collection of row events.
type RowEvents []RowEvent

// ExtractHeaderLabels extract header labels.
func (r RowEvents) ExtractHeaderLabels(labelCol int) []string {
	ll := make([]string, 0, 10)
	for _, re := range r {
		ll = append(ll, re.ExtractHeaderLabels(labelCol)...)
	}

	return ll
}

// Labelize converts labels into a row event.
func (r RowEvents) Labelize(cols []int, labelCol int, labels []string) RowEvents {
	out := make(RowEvents, 0, len(r))
	for _, re := range r {
		out = append(out, re.Labelize(cols, labelCol, labels))
	}

	return out
}

// Customize returns custom row events based on columns layout.
func (r RowEvents) Customize(cols []int) RowEvents {
	ee := make(RowEvents, 0, len(cols))
	for _, re := range r {
		ee = append(ee, re.Customize(cols))
	}
	return ee
}

// Diff returns true if the event changed.
func (r RowEvents) Diff(re RowEvents, ageCol int) bool {
	if len(r) != len(re) {
		return true
	}

	for i := range r {
		if r[i].Diff(re[i], ageCol) {
			return true
		}
	}

	return false
}

// Clone returns a rowevents deep copy.
func (r RowEvents) Clone() RowEvents {
	res := make(RowEvents, len(r))
	for i, re := range r {
		res[i] = re.Clone()
	}

	return res
}

// Upsert add or update a row if it exists.
func (r RowEvents) Upsert(re RowEvent) RowEvents {
	if idx, ok := r.FindIndex(re.Row.ID); ok {
		r[idx] = re
	} else {
		r = append(r, re)
	}
	return r
}

// Delete removes an element by id.
func (r RowEvents) Delete(id string) RowEvents {
	victim, ok := r.FindIndex(id)
	if !ok {
		return r
	}
	return append(r[0:victim], r[victim+1:]...)
}

// Clear delete all row events.
func (r RowEvents) Clear() RowEvents {
	return RowEvents{}
}

// FindIndex locates a row index by id. Returns false is not found.
func (r RowEvents) FindIndex(id string) (int, bool) {
	for i, re := range r {
		if re.Row.ID == id {
			return i, true
		}
	}

	return 0, false
}

// Sort rows based on column index and order.
func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, asc bool) {
	if sortCol == -1 {
		return
	}

	t := RowEventSorter{
		NS:         ns,
		Events:     r,
		Index:      sortCol,
		Asc:        asc,
		IsNumber:   numCol,
		IsDuration: isDuration,
	}
	sort.Sort(t)
}

// ----------------------------------------------------------------------------

// RowEventSorter sorts row events by a given colon.
type RowEventSorter struct {
	Events     RowEvents
	Index      int
	NS         string
	IsNumber   bool
	IsDuration bool
	Asc        bool
}

func (r RowEventSorter) Len() int {
	return len(r.Events)
}

func (r RowEventSorter) Swap(i, j int) {
	r.Events[i], r.Events[j] = r.Events[j], r.Events[i]
}

func (r RowEventSorter) Less(i, j int) bool {
	f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields
	id1, id2 := r.Events[i].Row.ID, r.Events[j].Row.ID
	less := Less(r.IsNumber, r.IsDuration, id1, id2, f1[r.Index], f2[r.Index])
	if r.Asc {
		return less
	}

	return !less
}

// ----------------------------------------------------------------------------

// IdSorter sorts row events by a given id.
type IdSorter struct {
	Ids    []string
	Events RowEvents
}

func (s IdSorter) Len() int {
	return len(s.Events)
}

func (s IdSorter) Swap(i, j int) {
	s.Events[i], s.Events[j] = s.Events[j], s.Events[i]
}

func (s IdSorter) Less(i, j int) bool {
	id1, id2 := s.Events[i].Row.ID, s.Events[j].Row.ID
	i1, i2 := findIndex(s.Ids, id1), findIndex(s.Ids, id2)
	return i1 < i2
}

func findIndex(ss []string, s string) int {
	for i := range ss {
		if ss[i] == s {
			return i
		}
	}
	log.Error().Err(fmt.Errorf("Doh! index not found for %s", s))
	return -1
}

// ----------------------------------------------------------------------------

// StringSet represents a collection of unique strings.
type StringSet []string

// Add adds a new item in the set.
func (ss StringSet) Add(item string) StringSet {
	if ss.In(item) {
		return ss
	}
	return append(ss, item)
}

// In checks if a string is in the set.
func (ss StringSet) In(item string) bool {
	return ss.indexOf(item) >= 0
}

func (ss StringSet) indexOf(item string) int {
	for i, s := range ss {
		if s == item {
			return i
		}
	}
	return -1
}
