package crypto

import (
	"io"
)

func NewEncoder(in io.Reader, cipher ICipher, minSize ...int) (out io.ReadCloser) {
	size := 128
	if len(minSize) > 0 && minSize[0] > 0 {
		size = minSize[0]
	}

	ce := &cryptoEncoder{
		source:    in,
		cipher:    cipher,
		autoFlush: false,

		blockSizeEncrypted: cipher.BlockSize(),
		blockSizePlain:     cipher.DecryptOutputSize(cipher.BlockSize()),
	}

	// Размер не может быть меньше блока незашифрованных данных
	if size < ce.blockSizePlain {
		size = ce.blockSizePlain
	} else if size%ce.blockSizePlain != 0 {
		// В size должно вмещаться ровное кол-во блоков, без остатков. Округляем в большую сторону.
		size = (size + (ce.blockSizePlain - (size % ce.blockSizePlain)))
	}
	// Хранит сырые не зашифрованные данные (размер >= blockSizePlain)
	ce.tmpBuff = make([]byte, size)
	ce.tmpSize = 0

	// Хранит зашифрованные данные (размер кратен blockSizeEncrypted)
	ce.buff = make([]byte, (size/ce.blockSizePlain)*ce.blockSizeEncrypted)
	ce.size = 0

	return ce
}

type cryptoEncoder struct {
	source    io.Reader
	readErr   error
	closed    bool
	autoFlush bool

	cipher ICipher

	buff   []byte
	offset int
	size   int

	blockSizeEncrypted int
	blockSizePlain     int

	// Буффер с остаточными данными, которые не поместились в BlockSize
	tmpBuff []byte
	tmpSize int
}

func (c *cryptoEncoder) Close() error {
	c.closed = true
	if _, err := c.flush(); err != nil {
		return err
	}
	if closer, ok := c.source.(io.Closer); ok {
		return closer.Close()
	}
	return nil
}

func (c *cryptoEncoder) flush() (n int, err error) {
	if c.tmpSize > 0 && c.offset == 0 {
		if _, err := c.write(c.tmpBuff[:c.tmpSize]); err != nil && err != io.EOF {
			return 0, err
		}
		n = c.tmpSize
		c.tmpSize = 0
	}
	return n, nil
}

func (c *cryptoEncoder) write(p []byte) (n int, err error) {
	n, err = c.cipher.Encrypt(p, c.buff[c.offset:])
	c.size += n
	return n, err
}

func (c *cryptoEncoder) Read(p []byte) (n int, err error) {
	//defer func() {
	//	log.Printf("CryptoEncoder, readed: %d\n", n)
	//}()

	plainSize := c.cipher.DecryptOutputSize(len(p))
	for i := 0; ; i++ {
		// В буфере есть данные для передачи?
		if c.offset < c.size {
			size := copy(p, c.buff[c.offset:c.size])
			n += size
			c.offset += size
			p = p[size:]
			// Все данные были скопированы?
			if c.offset == c.size {
				c.offset = 0
				c.size = 0
			}
			// Мы заполнили входящий буфер?
			if len(p) == 0 {
				return n, nil
			}
		}

		// Если пришли данные не кратные cipher.PlainTextBlockSize и размер необработанных данных равен их остатку,
		// то считаем это концом данных. В этом случае, мы ожидаем, что cipher.Encrypt умеет корректно обрабатывать
		// такие случаи с не полными блоками.
		if c.autoFlush && i != 0 && c.tmpSize > 0 {
			processed := c.cipher.DecryptOutputSize(n + c.tmpSize)
			if plainSize == processed {
				if _, err := c.flush(); err != nil {
					return n, err
				}
				continue
			}
		}

		// Мы должны пропустить шаг, если получим ошибку (io.EOF) с прошлого круга.
		// Она сигнализирует о завершения чтения.
		if c.readErr == nil {
			// Читаем новую порцию данных для шифрования.
			// tmpBuff может содержать остаток необработанных данных с прошлого круга.
			readed, err := c.source.Read(c.tmpBuff[c.tmpSize:])
			c.tmpSize += readed
			c.readErr = err
		}

		if c.readErr != nil {
			// Собрали все данные?
			if c.readErr == io.EOF {
				// Если данных меньше блока, мы прочитали все данные из b.source. Переходим к закрытию encoder.
				// Если больше, обрабатываем стандартным образом.
				if c.tmpSize < c.blockSizePlain {
					// Попадаем сюда повторно, когда все данные прочитаны, а encoder закрыт.
					if c.closed {
						return n, c.readErr
					}
					// При закрытии, не прочитанные данные из tmpBuff сбрасываются в cipher, как есть. Поэтому cipher
					// должен уметь обрабатывать случай, когда данные приходят меньшего размера, чем blockSize.
					if err := c.Close(); err != nil {
						return n, err
					}
					continue
				}
			} else {
				return n, c.readErr
			}
		}

		// b.Write передаются данные кратные blockSize, с округлением в меньшую сторону. Если данных меньше, чем
		// blockSize, то идём на новый круг и пробуем добрать остаток.
		if size := (c.tmpSize / c.blockSizePlain) * c.blockSizePlain; size > 0 {
			c.write(c.tmpBuff[:size])
			// Если после записи остался остаток, перемещаем его в начало tmpBuff
			if size < c.tmpSize {
				c.tmpSize = copy(c.tmpBuff, c.tmpBuff[size:c.tmpSize])
			} else {
				c.tmpSize = 0
			}
		}
	}
}
