/*
Copyright (c) 2016, The `code.ivysaur/chameleon` author(s)

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above copyright
notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
*/

package chameleon

import (
	"encoding/binary"
	"errors"
	"io"
)

func chameleonHash(in uint32) uint16 {
	var tmp uint32
	tmp = in * 2641295638 // must overflow
	return uint16(tmp >> 16)
}

// Encode copies from src to dest by way of the Chameleon compression algorithm.
// It returns the number of bytes read from the input stream.
// Multiple uses cannot generally be concatenated, but it's fine if the input
// is always a multiple of 128 bytes in length.
func Encode(dest io.Writer, src io.Reader) (uint64, error) {

	history := make(map[uint16]uint32, 0)

	readEOF := false
	readN := 0
	var readTotal uint64
	var readErr error
	chunkbytes := make([]byte, 4)

	for {

		buf := make([]byte, 128) // 32 * sizeof(uint32) - maximum size we'll need
		buflen := 0

		var bitmap uint32
		var bitmapIdx uint

		for bitmapIdx = 0; bitmapIdx < 32; bitmapIdx++ {

			var chunk uint32

			readN, readErr = src.Read(chunkbytes)
			readTotal += uint64(readN)
			if readN == 4 {

				chunk = binary.LittleEndian.Uint32(chunkbytes)

				// Scan history to see if it's been found before
				found := false
				for hashedVal, hashedSrc := range history {

					if hashedSrc == chunk {
						binary.LittleEndian.PutUint16(buf[buflen:buflen+2], hashedVal)
						buflen += 2

						bitmap = bitmap | (1 << bitmapIdx)
						found = true
						break
					}

				}

				if !found {
					history[chameleonHash(chunk)] = chunk // Hash
					binary.LittleEndian.PutUint32(buf[buflen:buflen+4], chunk)
					buflen += 4
				}

			} else {
				// Read less than 4 bytes
				// Assume this was EOF (even if we didn't recieve io.EOF), and
				//   write the bytes out as trailer
				readErr = io.EOF
			}

			if readErr == io.EOF {
				// done
				readEOF = true
				break

			} else if readErr != nil {
				// unexpected error
				return readTotal, readErr
			}

		}

		err := binary.Write(dest, binary.LittleEndian, bitmap)
		if err != nil {
			return readTotal, err
		}
		bitmap = 0

		_, err = dest.Write(buf[:buflen])
		if err != nil {
			return readTotal, err
		}

		if readEOF {
			break
		}

	}

	// Write any trailing bytes as-is: zero bitmask decodes them as raw bytes

	if len(chunkbytes[:readN]) > 0 {
		_, err := dest.Write(chunkbytes[:readN])
		if err != nil {
			return readTotal, err
		}
	}

	// Done

	return readTotal, nil
}

// Decode copies from src to dest by way of the Chameleon decompression algorithm.
// It returns the number of bytes read from the input stream.
func Decode(dest io.Writer, src io.Reader) (uint64, error) {

	history := make(map[uint16]uint32, 0)

	var readTotal uint64

	for {

		// Read bitmap

		bitmapBytes := make([]byte, 4)
		n, err := src.Read(bitmapBytes)
		readTotal += uint64(n)
		if n == 0 && err == io.EOF {
			return readTotal, nil
		}
		if err != nil {
			return readTotal, err
		}
		if n < len(bitmapBytes) {
			return readTotal, errors.New("Corrupt stream, cannot decode") // There should always be a bitmap
		}

		bitmap := binary.LittleEndian.Uint32(bitmapBytes)

		// Read blocks

		for bitmapIdx := uint(0); bitmapIdx < 32; bitmapIdx++ {

			isUncompressed := bitmap&(1<<bitmapIdx) == 0

			expectByteCount := 2
			if isUncompressed {
				expectByteCount = 4
			}

			readBytes := make([]byte, expectByteCount)
			n, err := src.Read(readBytes)
			readTotal += uint64(n)
			if n == 0 && err == io.EOF {
				return readTotal, nil
			}
			if err != nil {
				return readTotal, err
			}
			if n < len(readBytes) { // If we couldn't read entirely, write as-is and then abandon
				_, err := dest.Write(readBytes[:n])
				return readTotal, err
			}

			var writeVal uint32
			if isUncompressed {
				writeVal = binary.LittleEndian.Uint32(readBytes)

				history[chameleonHash(writeVal)] = writeVal // Hash

			} else {
				encodedVal := binary.LittleEndian.Uint16(readBytes)
				lookupVal, found := history[encodedVal]
				if !found {
					return readTotal, errors.New("Corrupt stream, cannot decode")
				}

				writeVal = lookupVal

			}

			writeBuff := make([]byte, 4)
			binary.LittleEndian.PutUint32(writeBuff, writeVal)
			_, err = dest.Write(writeBuff)
			if err != nil {
				return readTotal, err
			}

		}

	}
}
