package tracker

import (
	"math"

	"github.com/vsariola/sointu"
	"gopkg.in/yaml.v3"
)

type (
	Table struct {
		TableData
	}

	TableData interface {
		Cursor() Point
		Cursor2() Point
		SetCursor(Point)
		SetCursor2(Point)
		SetCursorFloat(x, y float32)
		Width() int
		Height() int
		MoveCursor(dx, dy int) (ok bool)

		clear(p Point)
		set(p Point, value int)
		add(rect Rect, delta int) (ok bool)
		marshal(rect Rect) (data []byte, ok bool)
		unmarshalAtCursor(data []byte) (ok bool)
		unmarshalRange(rect Rect, data []byte) (ok bool)
		change(kind string, severity ChangeSeverity) func()
		cancel()
	}

	Point struct {
		X, Y int
	}

	Rect struct {
		TopLeft, BottomRight Point
	}

	Order Model
	Notes Model
)

// Model methods

func (m *Model) Order() *Order { return (*Order)(m) }
func (m *Model) Notes() *Notes { return (*Notes)(m) }

// Rect methods

func (r *Rect) Contains(p Point) bool {
	return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X &&
		r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y
}

func (r *Rect) Width() int {
	return r.BottomRight.X - r.TopLeft.X + 1
}

func (r *Rect) Height() int {
	return r.BottomRight.Y - r.TopLeft.Y + 1
}

func (r *Rect) Limit(width, height int) {
	if r.TopLeft.X < 0 {
		r.TopLeft.X = 0
	}
	if r.TopLeft.Y < 0 {
		r.TopLeft.Y = 0
	}
	if r.BottomRight.X >= width {
		r.BottomRight.X = width - 1
	}
	if r.BottomRight.Y >= height {
		r.BottomRight.Y = height - 1
	}
}

// Table methods

func (v Table) Range() (rect Rect) {
	rect.TopLeft.X = min(v.Cursor().X, v.Cursor2().X)
	rect.TopLeft.Y = min(v.Cursor().Y, v.Cursor2().Y)
	rect.BottomRight.X = max(v.Cursor().X, v.Cursor2().X)
	rect.BottomRight.Y = max(v.Cursor().Y, v.Cursor2().Y)
	return
}

func (v Table) Copy() ([]byte, bool) {
	ret, ok := v.marshal(v.Range())
	if !ok {
		return nil, false
	}
	return ret, true
}

func (v Table) Paste(data []byte) bool {
	defer v.change("Paste", MajorChange)()
	if v.Cursor() == v.Cursor2() {
		return v.unmarshalAtCursor(data)
	} else {
		return v.unmarshalRange(v.Range(), data)
	}
}

func (v Table) Clear() {
	defer v.change("Clear", MajorChange)()
	rect := v.Range()
	rect.Limit(v.Width(), v.Height())
	for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
		for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
			v.clear(Point{x, y})
		}
	}
}

func (v Table) Set(value byte) {
	defer v.change("Set", MajorChange)()
	cursor := v.Cursor()
	// TODO: might check for visibility
	v.set(cursor, int(value))
}

func (v Table) Fill(value int) {
	defer v.change("Fill", MajorChange)()
	rect := v.Range()
	rect.Limit(v.Width(), v.Height())
	for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
		for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
			v.set(Point{x, y}, value)
		}
	}
}

func (v Table) Add(delta int) {
	defer v.change("Add", MinorChange)()
	if !v.add(v.Range(), delta) {
		v.cancel()
	}
}

func (v Table) SetCursorX(x int) {
	p := v.Cursor()
	p.X = x
	v.SetCursor(p)
}

func (v Table) SetCursorY(y int) {
	p := v.Cursor()
	p.Y = y
	v.SetCursor(p)
}

// Order methods

func (v *Order) Table() Table {
	return Table{v}
}

func (m *Order) Cursor() Point {
	t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
	p := max(min(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0)
	return Point{t, p}
}

func (m *Order) Cursor2() Point {
	t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
	p := max(min(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0)
	return Point{t, p}
}

func (m *Order) SetCursor(p Point) {
	m.d.Cursor.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
	y := max(min(p.Y, m.d.Song.Score.Length-1), 0)
	if y != m.d.Cursor.OrderRow {
		m.follow = false
	}
	m.d.Cursor.OrderRow = y
	m.updateCursorRows()
}

func (m *Order) SetCursor2(p Point) {
	m.d.Cursor2.Track = max(min(p.X, len(m.d.Song.Score.Tracks)-1), 0)
	m.d.Cursor2.OrderRow = max(min(p.Y, m.d.Song.Score.Length-1), 0)
	m.updateCursorRows()
}

func (m *Order) SetCursorFloat(x, y float32) {
	m.SetCursor(Point{int(x), int(y)})
}

func (v *Order) updateCursorRows() {
	if v.Cursor() == v.Cursor2() {
		v.d.Cursor.PatternRow = 0
		v.d.Cursor2.PatternRow = 0
		return
	}
	if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow {
		v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1
		v.d.Cursor2.PatternRow = 0
	} else {
		v.d.Cursor.PatternRow = 0
		v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1
	}
}

func (v *Order) Width() int {
	return len((*Model)(v).d.Song.Score.Tracks)
}

func (v *Order) Height() int {
	return (*Model)(v).d.Song.Score.Length
}

func (v *Order) MoveCursor(dx, dy int) (ok bool) {
	p := v.Cursor()
	p.X += dx
	p.Y += dy
	v.SetCursor(p)
	return p == v.Cursor()
}

func (m *Order) clear(p Point) {
	m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1)
}

func (m *Order) set(p Point, value int) {
	m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value)
}

func (v *Order) add(rect Rect, delta int) (ok bool) {
	for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
		for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
			if !v.add1(Point{x, y}, delta) {
				return false
			}
		}
	}
	return true
}

func (v *Order) add1(p Point, delta int) (ok bool) {
	if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) {
		return true
	}
	val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
	if val < 0 {
		return true
	}
	val += delta
	if val < 0 || val > 36 {
		return false
	}
	v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
	return true
}

type marshalOrder struct {
	Order []int `yaml:",flow"`
}

type marshalTracks struct {
	Tracks []marshalOrder
}

func (m *Order) marshal(rect Rect) (data []byte, ok bool) {
	width := rect.BottomRight.X - rect.TopLeft.X + 1
	height := rect.BottomRight.Y - rect.TopLeft.Y + 1
	var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)}
	for x := 0; x < width; x++ {
		ax := x + rect.TopLeft.X
		if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
			continue
		}
		table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)})
		for y := 0; y < height; y++ {
			table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y))
		}
	}
	ret, err := yaml.Marshal(table)
	if err != nil {
		return nil, false
	}
	return ret, true
}

func (m *Order) unmarshal(data []byte) (marshalTracks, bool) {
	var table marshalTracks
	yaml.Unmarshal(data, &table)
	if len(table.Tracks) == 0 {
		return marshalTracks{}, false
	}
	for i := 0; i < len(table.Tracks); i++ {
		if len(table.Tracks[i].Order) > 0 {
			return table, true
		}
	}
	return marshalTracks{}, false
}

func (v *Order) unmarshalAtCursor(data []byte) bool {
	table, ok := v.unmarshal(data)
	if !ok {
		return false
	}
	for i := 0; i < len(table.Tracks); i++ {
		for j, q := range table.Tracks[i].Order {
			if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 {
				continue
			}
			x := i + v.Cursor().X
			y := j + v.Cursor().Y
			if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
				continue
			}
			v.d.Song.Score.Tracks[x].Order.Set(y, q)
		}
	}
	return true
}

func (v *Order) unmarshalRange(rect Rect, data []byte) bool {
	table, ok := v.unmarshal(data)
	if !ok {
		return false
	}
	for i := 0; i < rect.Width(); i++ {
		for j := 0; j < rect.Height(); j++ {
			k := i % len(table.Tracks)
			l := j % len(table.Tracks[k].Order)
			a := table.Tracks[k].Order[l]
			if a < -1 || a > 36 {
				continue
			}
			x := i + rect.TopLeft.X
			y := j + rect.TopLeft.Y
			if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
				continue
			}
			v.d.Song.Score.Tracks[x].Order.Set(y, a)
		}
	}
	return true
}

func (v *Order) change(kind string, severity ChangeSeverity) func() {
	return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
}

func (v *Order) cancel() {
	v.changeCancel = true
}

func (m *Order) Value(p Point) int {
	if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
		return -1
	}
	return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
}

func (m *Order) SetValue(p Point, val int) {
	defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)()
	m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
}

// NoteTable

func (v *Notes) Table() Table {
	return Table{v}
}

func (m *Notes) Cursor() Point {
	t := max(min(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
	p := max(min(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
	return Point{t, p}
}

func (m *Notes) Cursor2() Point {
	t := max(min(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
	p := max(min(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
	return Point{t, p}
}

func (v *Notes) SetCursor(p Point) {
	v.d.Cursor.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
	newPos := v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
	if newPos != v.d.Cursor.SongPos {
		v.follow = false
	}
	v.d.Cursor.SongPos = newPos
}

func (v *Notes) SetCursor2(p Point) {
	v.d.Cursor2.Track = max(min(p.X, len(v.d.Song.Score.Tracks)-1), 0)
	v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(sointu.SongPos{PatternRow: p.Y})
}

func (m *Notes) SetCursorFloat(x, y float32) {
	m.SetCursor(Point{int(x), int(y)})
	m.d.LowNibble = math.Mod(float64(x), 1.0) > 0.5
}

func (v *Notes) Width() int {
	return len((*Model)(v).d.Song.Score.Tracks)
}

func (v *Notes) Height() int {
	return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern
}

func (v *Notes) MoveCursor(dx, dy int) (ok bool) {
	p := v.Cursor()
	for dx < 0 {
		if v.Effect(p.X) && v.d.LowNibble {
			v.d.LowNibble = false
		} else {
			p.X--
			v.d.LowNibble = true
		}
		dx++
	}
	for dx > 0 {
		if v.Effect(p.X) && !v.d.LowNibble {
			v.d.LowNibble = true
		} else {
			p.X++
			v.d.LowNibble = false
		}
		dx--
	}
	p.Y += dy
	v.SetCursor(p)
	return p == v.Cursor()
}

func (v *Notes) clear(p Point) {
	v.SetValue(p, 1)
}

func (v *Notes) set(p Point, value int) {
	v.SetValue(p, byte(value))
}

func (v *Notes) add(rect Rect, delta int) (ok bool) {
	for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- {
		for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- {
			if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
				continue
			}
			pos := v.d.Song.Score.SongPos(y)
			note := v.d.Song.Score.Tracks[x].Note(pos)
			if note <= 1 {
				continue
			}
			newVal := int(note) + delta
			if newVal < 2 {
				newVal = 2
			} else if newVal > 255 {
				newVal = 255
			}
			// only do all sets after all gets, so we don't accidentally adjust single note multiple times
			defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal), v.uniquePatterns)
		}
	}
	return true
}

type noteTable struct {
	Notes [][]byte `yaml:",flow"`
}

func (m *Notes) marshal(rect Rect) (data []byte, ok bool) {
	width := rect.BottomRight.X - rect.TopLeft.X + 1
	height := rect.BottomRight.Y - rect.TopLeft.Y + 1
	var table = noteTable{Notes: make([][]byte, 0, width)}
	for x := 0; x < width; x++ {
		table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
		for y := 0; y < height; y++ {
			pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y)
			ax := x + rect.TopLeft.X
			if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
				continue
			}
			table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos))
		}
	}
	ret, err := yaml.Marshal(table)
	if err != nil {
		return nil, false
	}
	return ret, true
}

func (v *Notes) unmarshal(data []byte) (noteTable, bool) {
	var table noteTable
	yaml.Unmarshal(data, &table)
	if len(table.Notes) == 0 {
		return noteTable{}, false
	}
	for i := 0; i < len(table.Notes); i++ {
		if len(table.Notes[i]) > 0 {
			return table, true
		}
	}
	return noteTable{}, false
}

func (v *Notes) unmarshalAtCursor(data []byte) bool {
	table, ok := v.unmarshal(data)
	if !ok {
		return false
	}
	for i := 0; i < len(table.Notes); i++ {
		for j, q := range table.Notes[i] {
			x := i + v.Cursor().X
			y := j + v.Cursor().Y
			if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
				continue
			}
			pos := v.d.Song.Score.SongPos(y)
			v.d.Song.Score.Tracks[x].SetNote(pos, q, v.uniquePatterns)
		}
	}
	return true
}

func (v *Notes) unmarshalRange(rect Rect, data []byte) bool {
	table, ok := v.unmarshal(data)
	if !ok {
		return false
	}
	for i := 0; i < rect.Width(); i++ {
		for j := 0; j < rect.Height(); j++ {
			k := i % len(table.Notes)
			l := j % len(table.Notes[k])
			a := table.Notes[k][l]
			x := i + rect.TopLeft.X
			y := j + rect.TopLeft.Y
			if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
				continue
			}
			pos := v.d.Song.Score.SongPos(y)
			v.d.Song.Score.Tracks[x].SetNote(pos, a, v.uniquePatterns)
		}
	}
	return true
}

func (v *Notes) change(kind string, severity ChangeSeverity) func() {
	return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
}

func (v *Notes) cancel() {
	v.changeCancel = true
}

func (m *Notes) Value(p Point) byte {
	if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
		return 1
	}
	pos := m.d.Song.Score.SongPos(p.Y)
	return m.d.Song.Score.Tracks[p.X].Note(pos)
}

func (m *Notes) Effect(x int) bool {
	if x < 0 || x >= len(m.d.Song.Score.Tracks) {
		return false
	}
	return m.d.Song.Score.Tracks[x].Effect
}

func (m *Notes) LowNibble() bool {
	return m.d.LowNibble
}

func (m *Notes) SetValue(p Point, val byte) {
	defer m.change("SetValue", MinorChange)()
	if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
		return
	}
	track := &(m.d.Song.Score.Tracks[p.X])
	pos := m.d.Song.Score.SongPos(p.Y)
	(*track).SetNote(pos, val, m.uniquePatterns)
}

func (v *Notes) FillNibble(value byte, lowNibble bool) {
	defer v.change("FillNibble", MajorChange)()
	rect := Table{v}.Range()
	for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
		for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
			val := v.Value(Point{x, y})
			if val == 1 {
				val = 0 // treat hold also as 0
			}
			if lowNibble {
				val = (val & 0xf0) | byte(value&15)
			} else {
				val = (val & 0x0f) | byte((value&15)<<4)
			}
			v.SetValue(Point{x, y}, val)
		}
	}
}