package mpb

import (
	"io"

	"github.com/mattn/go-runewidth"
	"github.com/vbauerster/mpb/v8/decor"
	"github.com/vbauerster/mpb/v8/internal"
)

const (
	iLbound = iota
	iRbound
	iRefiller
	iFiller
	iTip
	iPadding
	components
)

var defaultBarStyle = [components]string{"[", "]", "+", "=", ">", "-"}

// BarStyleComposer interface.
type BarStyleComposer interface {
	BarFillerBuilder
	Lbound(string) BarStyleComposer
	LboundMeta(func(string) string) BarStyleComposer
	Rbound(string) BarStyleComposer
	RboundMeta(func(string) string) BarStyleComposer
	Filler(string) BarStyleComposer
	FillerMeta(func(string) string) BarStyleComposer
	Refiller(string) BarStyleComposer
	RefillerMeta(func(string) string) BarStyleComposer
	Padding(string) BarStyleComposer
	PaddingMeta(func(string) string) BarStyleComposer
	Tip(frames ...string) BarStyleComposer
	TipMeta(func(string) string) BarStyleComposer
	TipOnComplete() BarStyleComposer
	Reverse() BarStyleComposer
}

type component struct {
	width int
	bytes []byte
}

type flushSection struct {
	meta  func(io.Writer, []byte) error
	bytes []byte
}

type bFiller struct {
	components [components]component
	meta       [components]func(io.Writer, []byte) error
	flush      func(io.Writer, ...flushSection) error
	tip        struct {
		onComplete bool
		count      uint
		frames     []component
	}
}

type barStyle struct {
	style         [components]string
	metaFuncs     [components]func(io.Writer, []byte) error
	tipFrames     []string
	tipOnComplete bool
	rev           bool
}

// BarStyle constructs default bar style which can be altered via
// BarStyleComposer interface.
func BarStyle() BarStyleComposer {
	bs := barStyle{
		style:     defaultBarStyle,
		tipFrames: []string{defaultBarStyle[iTip]},
	}
	for i := range bs.metaFuncs {
		bs.metaFuncs[i] = defaultMeta
	}
	return bs
}

func (s barStyle) Lbound(bound string) BarStyleComposer {
	s.style[iLbound] = bound
	return s
}

func (s barStyle) LboundMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iLbound] = makeMetaFunc(fn)
	return s
}

func (s barStyle) Rbound(bound string) BarStyleComposer {
	s.style[iRbound] = bound
	return s
}

func (s barStyle) RboundMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iRbound] = makeMetaFunc(fn)
	return s
}

func (s barStyle) Filler(filler string) BarStyleComposer {
	s.style[iFiller] = filler
	return s
}

func (s barStyle) FillerMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iFiller] = makeMetaFunc(fn)
	return s
}

func (s barStyle) Refiller(refiller string) BarStyleComposer {
	s.style[iRefiller] = refiller
	return s
}

func (s barStyle) RefillerMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iRefiller] = makeMetaFunc(fn)
	return s
}

func (s barStyle) Padding(padding string) BarStyleComposer {
	s.style[iPadding] = padding
	return s
}

func (s barStyle) PaddingMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iPadding] = makeMetaFunc(fn)
	return s
}

func (s barStyle) Tip(frames ...string) BarStyleComposer {
	if len(frames) != 0 {
		s.tipFrames = frames
	}
	return s
}

func (s barStyle) TipMeta(fn func(string) string) BarStyleComposer {
	s.metaFuncs[iTip] = makeMetaFunc(fn)
	return s
}

func (s barStyle) TipOnComplete() BarStyleComposer {
	s.tipOnComplete = true
	return s
}

func (s barStyle) Reverse() BarStyleComposer {
	s.rev = true
	return s
}

func (s barStyle) Build() BarFiller {
	bf := &bFiller{
		meta: s.metaFuncs,
	}
	bf.components[iLbound] = component{
		width: runewidth.StringWidth(s.style[iLbound]),
		bytes: []byte(s.style[iLbound]),
	}
	bf.components[iRbound] = component{
		width: runewidth.StringWidth(s.style[iRbound]),
		bytes: []byte(s.style[iRbound]),
	}
	bf.components[iFiller] = component{
		width: runewidth.StringWidth(s.style[iFiller]),
		bytes: []byte(s.style[iFiller]),
	}
	bf.components[iRefiller] = component{
		width: runewidth.StringWidth(s.style[iRefiller]),
		bytes: []byte(s.style[iRefiller]),
	}
	bf.components[iPadding] = component{
		width: runewidth.StringWidth(s.style[iPadding]),
		bytes: []byte(s.style[iPadding]),
	}
	bf.tip.onComplete = s.tipOnComplete
	bf.tip.frames = make([]component, len(s.tipFrames))
	for i, t := range s.tipFrames {
		bf.tip.frames[i] = component{
			width: runewidth.StringWidth(t),
			bytes: []byte(t),
		}
	}
	if s.rev {
		bf.flush = func(w io.Writer, sections ...flushSection) error {
			for i := len(sections) - 1; i >= 0; i-- {
				if s := sections[i]; len(s.bytes) != 0 {
					err := s.meta(w, s.bytes)
					if err != nil {
						return err
					}
				}
			}
			return nil
		}
	} else {
		bf.flush = func(w io.Writer, sections ...flushSection) error {
			for _, s := range sections {
				if len(s.bytes) != 0 {
					err := s.meta(w, s.bytes)
					if err != nil {
						return err
					}
				}
			}
			return nil
		}
	}
	return bf
}

func (s *bFiller) Fill(w io.Writer, stat decor.Statistics) error {
	width := internal.CheckRequestedWidth(stat.RequestedWidth, stat.AvailableWidth)
	// don't count brackets as progress
	width -= (s.components[iLbound].width + s.components[iRbound].width)
	if width < 0 {
		return nil
	}

	err := s.meta[iLbound](w, s.components[iLbound].bytes)
	if err != nil {
		return err
	}

	if width == 0 {
		return s.meta[iRbound](w, s.components[iRbound].bytes)
	}

	var tip component
	var refilling, filling, padding []byte
	var fillCount int
	curWidth := int(internal.PercentageRound(stat.Total, stat.Current, uint(width)))

	if curWidth != 0 {
		if !stat.Completed || s.tip.onComplete {
			tip = s.tip.frames[s.tip.count%uint(len(s.tip.frames))]
			s.tip.count++
			fillCount += tip.width
		}
		switch refWidth := 0; {
		case stat.Refill != 0:
			refWidth = int(internal.PercentageRound(stat.Total, stat.Refill, uint(width)))
			curWidth -= refWidth
			refWidth += curWidth
			fallthrough
		default:
			for w := s.components[iFiller].width; curWidth-fillCount >= w; fillCount += w {
				filling = append(filling, s.components[iFiller].bytes...)
			}
			for w := s.components[iRefiller].width; refWidth-fillCount >= w; fillCount += w {
				refilling = append(refilling, s.components[iRefiller].bytes...)
			}
		}
	}

	for w := s.components[iPadding].width; width-fillCount >= w; fillCount += w {
		padding = append(padding, s.components[iPadding].bytes...)
	}

	for w := 1; width-fillCount >= w; fillCount += w {
		padding = append(padding, "…"...)
	}

	err = s.flush(w,
		flushSection{s.meta[iRefiller], refilling},
		flushSection{s.meta[iFiller], filling},
		flushSection{s.meta[iTip], tip.bytes},
		flushSection{s.meta[iPadding], padding},
	)
	if err != nil {
		return err
	}
	return s.meta[iRbound](w, s.components[iRbound].bytes)
}

func makeMetaFunc(fn func(string) string) func(io.Writer, []byte) error {
	return func(w io.Writer, p []byte) (err error) {
		_, err = io.WriteString(w, fn(string(p)))
		return err
	}
}

func defaultMeta(w io.Writer, p []byte) (err error) {
	_, err = w.Write(p)
	return err
}
