package hexdump

import (
	"io"
	"strings"

	"github.com/wader/fq/internal/columnwriter"
	"github.com/wader/fq/internal/mathex"
	"github.com/wader/fq/pkg/bitio"
)

type Dumper struct {
	addrLen          int
	addrBase         int
	lineBytes        int64
	columnW          *columnwriter.Writer
	separatorsW      io.Writer
	startOffset      int64
	offset           int64
	hexFn            func(b byte) string
	asciiFn          func(b byte) string
	dumpHeaderFn     func(s string) string
	dumpAddrFn       func(s string) string
	column           string
	hasWrittenHeader bool

	bitsBuf  []byte
	bitsBufN int64
}

// TODO: something more generic? bin, octal, arbitrary base?
// TODO: template for columns?
// TODO: merge with dump?
// TODO: replace addrLen with highest address and calc instead
// TODO: use dump options? config struct?
func New(w io.Writer, startOffset int64, addrLen int, addrBase int, lineBytes int,
	hexFn func(b byte) string,
	asciiFn func(b byte) string,
	dumpHeaderFn func(s string) string,
	dumpAddrFn func(s string) string,
	column string) *Dumper {
	cw := columnwriter.New(
		w,
		&columnwriter.MultiLineColumn{Width: addrLen},
		columnwriter.BarColumn(column),
		&columnwriter.MultiLineColumn{Width: lineBytes * 3},
		columnwriter.BarColumn(column),
		&columnwriter.MultiLineColumn{Width: lineBytes},
		columnwriter.BarColumn(column),
	)

	return &Dumper{
		addrLen:          addrLen,
		addrBase:         addrBase,
		lineBytes:        int64(lineBytes),
		columnW:          cw,
		separatorsW:      io.MultiWriter(cw.Columns[1], cw.Columns[3], cw.Columns[5]),
		startOffset:      startOffset,
		offset:           startOffset - startOffset%int64(lineBytes),
		hexFn:            hexFn,
		asciiFn:          asciiFn,
		dumpHeaderFn:     dumpHeaderFn,
		dumpAddrFn:       dumpAddrFn,
		column:           column,
		hasWrittenHeader: false,
		bitsBuf:          make([]byte, 1),
	}
}

func (d *Dumper) flush() error {
	if _, err := d.columnW.Columns[0].Write([]byte(
		d.dumpAddrFn(mathex.PadFormatInt(((d.offset-1)/d.lineBytes)*d.lineBytes, d.addrBase, true, d.addrLen)))); err != nil {
		return err
	}
	if _, err := d.separatorsW.Write([]byte(d.column)); err != nil {
		return err
	}
	if err := d.columnW.Flush(); err != nil {
		return err
	}
	return nil
}

func (d *Dumper) WriteBits(p []byte, nBits int64) (n int64, err error) {
	pos := int64(0)
	rBits := nBits
	if d.bitsBufN > 0 {
		r := mathex.Min(8-d.bitsBufN, nBits)
		v := bitio.Read64(p, 0, r)
		bitio.Write64(v, r, d.bitsBuf, d.bitsBufN)

		d.bitsBufN += r

		if d.bitsBufN < 8 {
			return nBits, nil
		}
		if n, err := d.Write(d.bitsBuf); err != nil {
			return int64(n) * 8, err
		}
		pos = r
		rBits -= r
	}

	for rBits >= 8 {
		b := [1]byte{0}

		b[0] = byte(bitio.Read64(p, pos, 8))
		if n, err := d.Write(b[:]); err != nil {
			return int64(n) * 8, err
		}

		pos += 8
		rBits -= 8

	}

	if rBits > 0 {
		d.bitsBuf[0] = byte(bitio.Read64(p, pos, rBits)) << (8 - rBits)
		d.bitsBufN = rBits
	} else {
		d.bitsBufN = 0
	}

	return nBits, nil
}

func (d *Dumper) Write(p []byte) (n int, err error) {
	if !d.hasWrittenHeader {
		if _, err := d.separatorsW.Write([]byte(d.column)); err != nil {
			return 0, err
		}
		for i := int64(0); i < d.lineBytes; i++ {
			headerSB := &strings.Builder{}
			if _, err := headerSB.Write([]byte(mathex.PadFormatInt(i, d.addrBase, false, 2))); err != nil {
				return 0, err
			}
			if i < d.lineBytes-1 {
				if _, err := headerSB.Write([]byte(" ")); err != nil {
					return 0, err
				}
			}

			if _, err := d.columnW.Columns[2].Write([]byte(d.dumpHeaderFn(headerSB.String()))); err != nil {
				return 0, err
			}
		}
		if err := d.columnW.Flush(); err != nil {
			return 0, err
		}
		d.hasWrittenHeader = true
	}

	if d.offset < d.startOffset {
		r := int(d.startOffset - d.offset)
		if _, err := d.columnW.Columns[2].Write([]byte(strings.Repeat("   ", r))); err != nil {
			return n, err
		}
		if _, err := d.columnW.Columns[4].Write([]byte(strings.Repeat(" ", r))); err != nil {
			return n, err
		}
		d.offset = d.startOffset
	}

	for len(p) > 0 {
		cl := d.lineBytes - d.offset%d.lineBytes
		if cl == 0 {
			cl = d.lineBytes
		}
		if cl > int64(len(p)) {
			cl = int64(len(p))
		}

		ps := p[0:cl]
		for _, b := range ps {
			d.offset++

			if _, err := d.columnW.Columns[2].Write([]byte(d.hexFn(b))); err != nil {
				return n, err
			}
			if d.offset%d.lineBytes != 0 {
				if _, err := d.columnW.Columns[2].Write([]byte(" ")); err != nil {
					return n, err
				}
			}
			if _, err := d.columnW.Columns[4].Write([]byte(d.asciiFn(b))); err != nil {
				return n, err
			}
			n++
		}

		if d.offset%d.lineBytes == 0 {
			if err := d.flush(); err != nil {
				return n, err
			}
		}

		p = p[cl:]
	}

	return n, nil
}

func (d *Dumper) Close() error {
	if d.offset == 0 || d.offset%d.lineBytes != 0 {
		return d.flush()
	}
	return nil
}
