package runewidth

import (
	"os"
	"strings"
	"unicode/utf8"

	"github.com/clipperhouse/uax29/v2/graphemes"
)

//go:generate go run script/generate.go

var (
	// EastAsianWidth will be set true if the current locale is CJK
	EastAsianWidth bool

	// StrictEmojiNeutral should be set false if handle broken fonts
	StrictEmojiNeutral bool = true

	// DefaultCondition is a condition in current locale
	DefaultCondition = &Condition{
		EastAsianWidth:     false,
		StrictEmojiNeutral: true,
	}
)

var (
	zerowidth table // combining + nonprint merged for faster zero-width lookup
	widewidth table // ambiguous + doublewidth merged for EA path
)

func init() {
	zerowidth = mergeIntervals(combining, nonprint)
	widewidth = mergeIntervals(ambiguous, doublewidth)
	handleEnv()
}

func mergeIntervals(t1, t2 table) table {
	merged := make(table, 0, len(t1)+len(t2))
	i, j := 0, 0
	for i < len(t1) && j < len(t2) {
		if t1[i].first <= t2[j].first {
			merged = append(merged, t1[i])
			i++
		} else {
			merged = append(merged, t2[j])
			j++
		}
	}
	merged = append(merged, t1[i:]...)
	merged = append(merged, t2[j:]...)
	if len(merged) == 0 {
		return merged
	}
	result := merged[:1]
	for _, iv := range merged[1:] {
		last := &result[len(result)-1]
		if iv.first <= last.last+1 {
			if iv.last > last.last {
				last.last = iv.last
			}
		} else {
			result = append(result, iv)
		}
	}
	return result
}

func handleEnv() {
	env := os.Getenv("RUNEWIDTH_EASTASIAN")
	if env == "" {
		EastAsianWidth = IsEastAsian()
	} else {
		EastAsianWidth = env == "1"
	}
	// update DefaultCondition
	if DefaultCondition.EastAsianWidth != EastAsianWidth {
		DefaultCondition.EastAsianWidth = EastAsianWidth
		if len(DefaultCondition.combinedLut) > 0 {
			DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
			CreateLUT()
		}
	}
}

type interval struct {
	first rune
	last  rune
}

type table []interval

func inTable(r rune, t table) bool {
	if r < t[0].first {
		return false
	}
	if r > t[len(t)-1].last {
		return false
	}

	bot := 0
	top := len(t) - 1
	for top >= bot {
		mid := (bot + top) >> 1

		switch {
		case t[mid].last < r:
			bot = mid + 1
		case t[mid].first > r:
			top = mid - 1
		default:
			return true
		}
	}

	return false
}

var private = table{
	{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}

var nonprint = table{
	{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
	{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
	{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
	{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}

// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
	combinedLut        []byte
	EastAsianWidth     bool
	StrictEmojiNeutral bool
}

// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
	return &Condition{
		EastAsianWidth:     EastAsianWidth,
		StrictEmojiNeutral: StrictEmojiNeutral,
	}
}

// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
	if r < 0 || r > 0x10FFFF {
		return 0
	}
	if len(c.combinedLut) > 0 {
		return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
	}
	// optimized version, verified by TestRuneWidthChecksums()
	if !c.EastAsianWidth {
		switch {
		case r < 0x20:
			return 0
		case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
			return 0
		case r < 0x300:
			return 1
		case inTable(r, zerowidth):
			return 0
		case inTable(r, doublewidth):
			return 2
		default:
			return 1
		}
	} else {
		switch {
		case inTable(r, zerowidth):
			return 0
		case inTable(r, narrow):
			return 1
		case inTable(r, widewidth):
			return 2
		case !c.StrictEmojiNeutral && inTable(r, emoji):
			return 2
		default:
			return 1
		}
	}
}

// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
// This should not be called concurrently with other operations on c.
// If options in c is changed, CreateLUT should be called again.
func (c *Condition) CreateLUT() {
	const max = 0x110000
	lut := c.combinedLut
	if len(c.combinedLut) != 0 {
		// Remove so we don't use it.
		c.combinedLut = nil
	} else {
		lut = make([]byte, max/2)
	}
	for i := range lut {
		i32 := int32(i * 2)
		x0 := c.RuneWidth(i32)
		x1 := c.RuneWidth(i32 + 1)
		lut[i] = uint8(x0) | uint8(x1)<<4
	}
	c.combinedLut = lut
}

// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
	if len(s) > 0 && len(s) <= utf8.UTFMax {
		r, size := utf8.DecodeRuneInString(s)
		if size == len(s) {
			return c.RuneWidth(r)
		}
	}
	// ASCII fast path: no grapheme clustering needed for pure ASCII
	if isAllASCII(s) {
		for i := 0; i < len(s); i++ {
			b := s[i]
			if b >= 0x20 && b != 0x7F {
				width++
			}
		}
		return
	}
	g := graphemes.FromString(s)
	for g.Next() {
		var chWidth int
		for _, r := range g.Value() {
			chWidth = c.RuneWidth(r)
			if chWidth > 0 {
				break // Our best guess at this point is to use the width of the first non-zero-width rune.
			}
		}
		width += chWidth
	}
	return
}

func isAllASCII(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] >= 0x80 {
			return false
		}
	}
	return true
}

// Truncate return string truncated with w cells
func (c *Condition) Truncate(s string, w int, tail string) string {
	if c.StringWidth(s) <= w {
		return s
	}
	w -= c.StringWidth(tail)
	var width int
	pos := len(s)
	g := graphemes.FromString(s)
	for g.Next() {
		var chWidth int
		for _, r := range g.Value() {
			chWidth = c.RuneWidth(r)
			if chWidth > 0 {
				break // See StringWidth() for details.
			}
		}
		if width+chWidth > w {
			pos = g.Start()
			break
		}
		width += chWidth
	}
	return s[:pos] + tail
}

// TruncateLeft cuts w cells from the beginning of the `s`.
func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
	if c.StringWidth(s) <= w {
		return prefix
	}

	var width int
	pos := len(s)

	g := graphemes.FromString(s)
	for g.Next() {
		var chWidth int
		for _, r := range g.Value() {
			chWidth = c.RuneWidth(r)
			if chWidth > 0 {
				break // See StringWidth() for details.
			}
		}

		if width+chWidth > w {
			if width < w {
				pos = g.End()
				prefix += strings.Repeat(" ", width+chWidth-w)
			} else {
				pos = g.Start()
			}

			break
		}

		width += chWidth
	}

	return prefix + s[pos:]
}

// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
	width := 0
	var out strings.Builder
	out.Grow(len(s) + len(s)/w + 1)
	for _, r := range s {
		cw := c.RuneWidth(r)
		if r == '\n' {
			out.WriteRune(r)
			width = 0
			continue
		} else if width+cw > w {
			out.WriteByte('\n')
			width = 0
			out.WriteRune(r)
			width += cw
			continue
		}
		out.WriteRune(r)
		width += cw
	}
	return out.String()
}

// FillLeft return string filled in left by spaces in w cells
func (c *Condition) FillLeft(s string, w int) string {
	width := c.StringWidth(s)
	count := w - width
	if count > 0 {
		b := make([]byte, count)
		for i := range b {
			b[i] = ' '
		}
		return string(b) + s
	}
	return s
}

// FillRight return string filled in left by spaces in w cells
func (c *Condition) FillRight(s string, w int) string {
	width := c.StringWidth(s)
	count := w - width
	if count > 0 {
		b := make([]byte, count)
		for i := range b {
			b[i] = ' '
		}
		return s + string(b)
	}
	return s
}

// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func RuneWidth(r rune) int {
	return DefaultCondition.RuneWidth(r)
}

// IsAmbiguousWidth returns whether is ambiguous width or not.
func IsAmbiguousWidth(r rune) bool {
	return inTable(r, private) || inTable(r, ambiguous)
}

// IsCombiningWidth returns whether is combining width or not.
func IsCombiningWidth(r rune) bool {
	return inTable(r, combining)
}

// IsNeutralWidth returns whether is neutral width or not.
func IsNeutralWidth(r rune) bool {
	return inTable(r, neutral)
}

// StringWidth return width as you can see
func StringWidth(s string) (width int) {
	return DefaultCondition.StringWidth(s)
}

// Truncate return string truncated with w cells
func Truncate(s string, w int, tail string) string {
	return DefaultCondition.Truncate(s, w, tail)
}

// TruncateLeft cuts w cells from the beginning of the `s`.
func TruncateLeft(s string, w int, prefix string) string {
	return DefaultCondition.TruncateLeft(s, w, prefix)
}

// Wrap return string wrapped with w cells
func Wrap(s string, w int) string {
	return DefaultCondition.Wrap(s, w)
}

// FillLeft return string filled in left by spaces in w cells
func FillLeft(s string, w int) string {
	return DefaultCondition.FillLeft(s, w)
}

// FillRight return string filled in left by spaces in w cells
func FillRight(s string, w int) string {
	return DefaultCondition.FillRight(s, w)
}

// CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
// This should not be called concurrently with other operations.
func CreateLUT() {
	if len(DefaultCondition.combinedLut) > 0 {
		return
	}
	DefaultCondition.CreateLUT()
}
