// Copyright 2015 The Tcell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package views

import (
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/uniseg"
)

// Text is a Widget with containing a block of text, which can optionally
// be styled.
type Text struct {
	view    View
	align   Alignment
	style   tcell.Style
	text    []rune
	widths  []int
	styles  []tcell.Style
	lengths []int
	width   int
	height  int

	WidgetWatchers
}

func (t *Text) clear() {
	v := t.view
	w, h := v.Size()
	v.Clear()
	for y := range h {
		for x := range w {
			v.Put(x, y, " ", t.style)
		}
	}
}

// calcY figures the initial Y offset.  Alignment is top by default.
func (t *Text) calcY(height int) int {
	if t.align&VAlignCenter != 0 {
		return (height - len(t.lengths)) / 2
	}
	if t.align&VAlignBottom != 0 {
		return height - len(t.lengths)
	}
	return 0
}

// calcX figures the initial X offset for the given line.
// Alignment is left by default.
func (t *Text) calcX(width, line int) int {
	if t.align&HAlignCenter != 0 {
		return (width - t.lengths[line]) / 2
	}
	if t.align&HAlignRight != 0 {
		return width - t.lengths[line]
	}
	return 0
}

// Draw draws the Text.
func (t *Text) Draw() {
	v := t.view
	if v == nil {
		return
	}

	width, height := v.Size()
	if width == 0 || height == 0 {
		return
	}

	t.clear()

	// Note that we might wind up with a negative X if the width
	// is larger than the length.  That's OK, and correct even.
	// The view will clip it properly in that case.

	// We align to the left & top by default.
	y := t.calcY(height)
	r := rune(0)
	w := 0
	x := 0
	var styl tcell.Style
	var comb []rune
	line := 0
	newline := true
	for i, l := range t.text {

		if newline {
			x = t.calcX(width, line)
			newline = false
		}
		if l == '\n' {
			if w != 0 {
				v.SetContent(x, y, r, comb, styl)
			}
			newline = true
			w = 0
			comb = nil
			line++
			y++
			continue
		}
		if t.widths[i] == 0 {
			comb = append(comb, l)
			continue
		}
		if w != 0 {
			v.SetContent(x, y, r, comb, styl)
			x += w
		}
		r = l
		w = t.widths[i]
		styl = t.styles[i]
		comb = nil
	}
	if w != 0 {
		v.SetContent(x, y, r, comb, styl)
	}
}

// Size returns the width and height in character cells of the Text.
func (t *Text) Size() (int, int) {
	if len(t.text) != 0 {
		return t.width, t.height
	}
	return 0, 0
}

// SetAlignment sets the alignment.  Negative values
// indicate right justification, positive values are left,
// and zero indicates center aligned.
func (t *Text) SetAlignment(align Alignment) {
	if align != t.align {
		t.align = align
		t.PostEventWidgetContent(t)
	}
}

// Alignment returns the alignment of the Text.
func (t *Text) Alignment() Alignment {
	return t.align
}

// SetView sets the View object used for the text bar.
func (t *Text) SetView(view View) {
	t.view = view
}

// HandleEvent implements a tcell.EventHandler, but does nothing.
func (t *Text) HandleEvent(tcell.Event) bool {
	return false
}

// SetText sets the text used for the string.  Any previously set
// styles on individual rune indices are reset, and the default style
// for the widget is set.
func (t *Text) SetText(s string) {
	t.width = 0
	t.text = []rune(s)
	if len(t.widths) < len(t.text) {
		t.widths = make([]int, len(t.text))
	} else {
		t.widths = t.widths[0:len(t.text)]
	}
	if len(t.styles) < len(t.text) {
		t.styles = make([]tcell.Style, len(t.text))
	} else {
		t.styles = t.styles[0:len(t.text)]
	}
	t.lengths = []int{}
	length := 0
	for i, r := range t.text {
		t.widths[i] = uniseg.StringWidth(string(r))
		t.styles[i] = t.style
		if r == '\n' {
			t.lengths = append(t.lengths, length)
			if length > t.width {
				t.width = length
			}
			length = 0
		} else if t.widths[i] == 0 && length == 0 {
			// If first character on line is combining, inject
			// a leading space.  (Shame on the caller!)
			t.widths = append(t.widths, 0)
			copy(t.widths[i+1:], t.widths[i:])
			t.widths[i] = 1

			t.text = append(t.text, ' ')
			copy(t.text[i+1:], t.text[i:])
			t.text[i] = ' '

			t.styles = append(t.styles, t.style)
			copy(t.styles[i+1:], t.styles[i:])
			t.styles[i] = t.style
			length++
		} else {
			length += t.widths[i]
		}
	}
	if length > 0 {
		t.lengths = append(t.lengths, length)
		if length > t.width {
			t.width = length
		}
	}
	t.height = len(t.lengths)
	t.PostEventWidgetContent(t)
}

// Text returns the text that was set.
func (t *Text) Text() string {
	return string(t.text)
}

// SetStyle sets the style used.  This applies to every cell in the
// in the text.
func (t *Text) SetStyle(style tcell.Style) {
	t.style = style
	for i := 0; i < len(t.text); i++ {
		if t.widths[i] != 0 {
			t.styles[i] = t.style
		}
	}
	t.PostEventWidgetContent(t)
}

// Style returns the previously set default style.  Note that
// individual characters may have different styles.
func (t *Text) Style() tcell.Style {
	return t.style
}

// SetStyleAt sets the style at the given rune index.  Note that for
// strings containing combining characters, it is not possible to
// change the style at the position of the combining character, but
// those positions *do* count for calculating the index.  A lot of
// complexity can be avoided by avoiding the use of combining characters.
func (t *Text) SetStyleAt(pos int, style tcell.Style) {
	if pos < 0 || pos >= len(t.text) || t.widths[pos] < 1 {
		return
	}
	t.styles[pos] = style
	t.PostEventWidgetContent(t)
}

// StyleAt gets the style at the given rune index.  If an invalid
// index is given, or the index is a combining character, then
// tcell.StyleDefault is returned.
func (t *Text) StyleAt(pos int) tcell.Style {
	if pos < 0 || pos >= len(t.text) || t.widths[pos] < 1 {
		return tcell.StyleDefault
	}
	return t.styles[pos]
}

// Resize is called when our View changes sizes.
func (t *Text) Resize() {
	t.PostEventWidgetResize(t)
}

// NewText creates an empty Text.
func NewText() *Text {
	return &Text{}
}
