// Copyright 2014-2022 Ulrich Kunitz. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package lzma

import (
	"bufio"
	"errors"
	"io"
)

// MinDictCap and MaxDictCap provide the range of supported dictionary
// capacities.
const (
	MinDictCap = 1 << 12
	MaxDictCap = 1<<32 - 1
)

// WriterConfig defines the configuration parameter for a writer.
type WriterConfig struct {
	// Properties for the encoding. If the it is nil the value
	// {LC: 3, LP: 0, PB: 2} will be chosen.
	Properties *Properties
	// The capacity of the dictionary. If DictCap is zero, the value
	// 8 MiB will be chosen.
	DictCap int
	// Size of the lookahead buffer; value 0 indicates default size
	// 4096
	BufSize int
	// Match algorithm
	Matcher MatchAlgorithm
	// SizeInHeader indicates that the header will contain an
	// explicit size.
	SizeInHeader bool
	// Size of the data to be encoded. A positive value will imply
	// than an explicit size will be set in the header.
	Size int64
	// EOSMarker requests whether the EOSMarker needs to be written.
	// If no explicit size is been given the EOSMarker will be
	// set automatically.
	EOSMarker bool
}

// fill converts zero-value fields to their explicit default values.
func (c *WriterConfig) fill() {
	if c.Properties == nil {
		c.Properties = &Properties{LC: 3, LP: 0, PB: 2}
	}
	if c.DictCap == 0 {
		c.DictCap = 8 * 1024 * 1024
	}
	if c.BufSize == 0 {
		c.BufSize = 4096
	}
	if c.Size > 0 {
		c.SizeInHeader = true
	}
	if !c.SizeInHeader {
		c.EOSMarker = true
	}
}

// Verify checks WriterConfig for errors. Verify will replace zero
// values with default values.
func (c *WriterConfig) Verify() error {
	c.fill()
	var err error
	if c == nil {
		return errors.New("lzma: WriterConfig is nil")
	}
	if c.Properties == nil {
		return errors.New("lzma: WriterConfig has no Properties set")
	}
	if err = c.Properties.verify(); err != nil {
		return err
	}
	if !(MinDictCap <= c.DictCap && int64(c.DictCap) <= MaxDictCap) {
		return errors.New("lzma: dictionary capacity is out of range")
	}
	if !(maxMatchLen <= c.BufSize) {
		return errors.New("lzma: lookahead buffer size too small")
	}
	if c.SizeInHeader {
		if c.Size < 0 {
			return errors.New("lzma: negative size not supported")
		}
	} else if !c.EOSMarker {
		return errors.New("lzma: EOS marker is required")
	}
	if err = c.Matcher.verify(); err != nil {
		return err
	}

	return nil
}

// header returns the header structure for this configuration.
func (c *WriterConfig) header() header {
	h := header{
		properties: *c.Properties,
		dictCap:    c.DictCap,
		size:       -1,
	}
	if c.SizeInHeader {
		h.size = c.Size
	}
	return h
}

// Writer writes an LZMA stream in the classic format.
type Writer struct {
	h   header
	bw  io.ByteWriter
	buf *bufio.Writer
	e   *encoder
}

// NewWriter creates a new LZMA writer for the classic format. The
// method will write the header to the underlying stream.
func (c WriterConfig) NewWriter(lzma io.Writer) (w *Writer, err error) {
	if err = c.Verify(); err != nil {
		return nil, err
	}
	w = &Writer{h: c.header()}

	var ok bool
	w.bw, ok = lzma.(io.ByteWriter)
	if !ok {
		w.buf = bufio.NewWriter(lzma)
		w.bw = w.buf
	}
	state := newState(w.h.properties)
	m, err := c.Matcher.new(w.h.dictCap)
	if err != nil {
		return nil, err
	}
	dict, err := newEncoderDict(w.h.dictCap, c.BufSize, m)
	if err != nil {
		return nil, err
	}
	var flags encoderFlags
	if c.EOSMarker {
		flags = eosMarker
	}
	if w.e, err = newEncoder(w.bw, state, dict, flags); err != nil {
		return nil, err
	}

	if err = w.writeHeader(); err != nil {
		return nil, err
	}
	return w, nil
}

// NewWriter creates a new LZMA writer using the classic format. The
// function writes the header to the underlying stream.
func NewWriter(lzma io.Writer) (w *Writer, err error) {
	return WriterConfig{}.NewWriter(lzma)
}

// writeHeader writes the LZMA header into the stream.
func (w *Writer) writeHeader() error {
	data, err := w.h.marshalBinary()
	if err != nil {
		return err
	}
	_, err = w.bw.(io.Writer).Write(data)
	return err
}

// Write puts data into the Writer.
func (w *Writer) Write(p []byte) (n int, err error) {
	if w.h.size >= 0 {
		m := w.h.size
		m -= w.e.Compressed() + int64(w.e.dict.Buffered())
		if m < 0 {
			m = 0
		}
		if m < int64(len(p)) {
			p = p[:m]
			err = ErrNoSpace
		}
	}
	var werr error
	if n, werr = w.e.Write(p); werr != nil {
		err = werr
	}
	return n, err
}

// Close closes the writer stream. It ensures that all data from the
// buffer will be compressed and the LZMA stream will be finished.
func (w *Writer) Close() error {
	if w.h.size >= 0 {
		n := w.e.Compressed() + int64(w.e.dict.Buffered())
		if n != w.h.size {
			return errSize
		}
	}
	err := w.e.Close()
	if w.buf != nil {
		ferr := w.buf.Flush()
		if err == nil {
			err = ferr
		}
	}
	return err
}
