package rardecode

import (
	"bytes"
	"crypto/sha1"
	"errors"
	"hash/crc32"
	"io"
	"slices"
	"strconv"
	"strings"
	"time"
	"unicode/utf16"
)

const (
	// block types
	blockArc     = 0x73
	blockFile    = 0x74
	blockComment = 0x75
	blockService = 0x7a
	blockEnd     = 0x7b

	// block flags
	blockHasData = 0x8000

	// archive block flags
	arcVolume    = 0x0001
	arcComment   = 0x0002
	arcSolid     = 0x0008
	arcNewNaming = 0x0010
	arcEncrypted = 0x0080

	// file block flags
	fileSplitBefore = 0x0001
	fileSplitAfter  = 0x0002
	fileEncrypted   = 0x0004
	fileSolid       = 0x0010
	fileWindowMask  = 0x00e0
	fileLargeData   = 0x0100
	fileUnicode     = 0x0200
	fileSalt        = 0x0400
	fileVersion     = 0x0800
	fileExtTime     = 0x1000

	// end block flags
	endArcNotLast = 0x0001

	saltSize    = 8 // size of salt for calculating AES keys
	cacheSize30 = 4 // number of AES keys to cache
	hashRounds  = 0x40000
)

var (
	ErrUnsupportedDecoder = errors.New("rardecode: unsupported decoder version")
)

type blockHeader15 struct {
	htype    byte // block header type
	flags    uint16
	data     readBuf // header data
	dataSize int64   // size of extra block data
}

// archive15 implements archiveBlockReader for RAR 1.5 file format archives
type archive15 struct {
	multi     bool // archive is multi-volume
	solid     bool // archive is a solid archive
	encrypted bool
	oldNaming bool
	pass      []uint16              // password in UTF-16
	keyCache  [cacheSize30]struct { // cache of previously calculated decryption keys
		salt []byte
		key  []byte
		iv   []byte
	}
}

func (a *archive15) useOldNaming() bool {
	return a.oldNaming
}

// Calculates the key and iv for AES decryption given a password and salt.
func calcAes30Params(pass []uint16, salt []byte) (key, iv []byte) {
	p := make([]byte, 0, len(pass)*2+len(salt))
	for _, v := range pass {
		p = append(p, byte(v), byte(v>>8))
	}
	p = append(p, salt...)

	hash := sha1.New()
	iv = make([]byte, 16)
	s := make([]byte, hash.Size())
	b := s[:3]
	for i := 0; i < hashRounds; i++ {
		// ignore hash Write errors, should always succeed
		_, _ = hash.Write(p)
		b[0], b[1], b[2] = byte(i), byte(i>>8), byte(i>>16)
		_, _ = hash.Write(b)
		if i%(hashRounds/16) == 0 {
			s = hash.Sum(s[:0])
			iv[i/(hashRounds/16)] = s[4*4+3]
		}
	}
	key = hash.Sum(s[:0])
	key = key[:16]

	for k := key; len(k) >= 4; k = k[4:] {
		k[0], k[1], k[2], k[3] = k[3], k[2], k[1], k[0]
	}
	return key, iv
}

// parseDosTime converts a 32bit DOS time value to time.Time
func parseDosTime(t uint32) time.Time {
	n := int(t)
	sec := n & 0x1f << 1
	min := n >> 5 & 0x3f
	hr := n >> 11 & 0x1f
	day := n >> 16 & 0x1f
	mon := time.Month(n >> 21 & 0x0f)
	yr := n>>25&0x7f + 1980
	return time.Date(yr, mon, day, hr, min, sec, 0, time.Local)
}

// decodeName decodes a non-unicode filename from a file header.
func decodeName(buf []byte) string {
	i := bytes.IndexByte(buf, 0)
	if i < 0 {
		return string(buf) // filename is UTF-8
	}

	name := buf[:i]
	encName := readBuf(buf[i+1:])
	if len(encName) < 2 {
		return "" // invalid encoding
	}
	highByte := uint16(encName.byte()) << 8
	flags := encName.byte()
	flagBits := 8
	var wchars []uint16 // decoded characters are UTF-16
	for len(wchars) < len(name) && len(encName) > 0 {
		if flagBits == 0 {
			flags = encName.byte()
			flagBits = 8
			if len(encName) == 0 {
				break
			}
		}
		switch flags >> 6 {
		case 0:
			wchars = append(wchars, uint16(encName.byte()))
		case 1:
			wchars = append(wchars, uint16(encName.byte())|highByte)
		case 2:
			if len(encName) < 2 {
				break
			}
			wchars = append(wchars, encName.uint16())
		case 3:
			n := encName.byte()
			b := name[len(wchars):]
			if l := int(n&0x7f) + 2; l < len(b) {
				b = b[:l]
			}
			if n&0x80 > 0 {
				if len(encName) < 1 {
					break
				}
				ec := encName.byte()
				for _, c := range b {
					wchars = append(wchars, uint16(c+ec)|highByte)
				}
			} else {
				for _, c := range b {
					wchars = append(wchars, uint16(c))
				}
			}
		}
		flags <<= 2
		flagBits -= 2
	}
	return string(utf16.Decode(wchars))
}

// readExtTimes reads and parses the optional extra time field from the file header.
func readExtTimes(f *fileBlockHeader, b *readBuf) {
	if len(*b) < 2 {
		return // invalid, not enough data
	}
	flags := b.uint16()

	ts := []*time.Time{&f.ModificationTime, &f.CreationTime, &f.AccessTime}

	for i, t := range ts {
		n := flags >> uint((3-i)*4)
		if n&0x8 == 0 {
			continue
		}
		if i != 0 { // ModificationTime already read so skip
			if len(*b) < 4 {
				return // invalid, not enough data
			}
			*t = parseDosTime(b.uint32())
		}
		if n&0x4 > 0 {
			*t = t.Add(time.Second)
		}
		n &= 0x3
		if n == 0 {
			continue
		}
		if len(*b) < int(n) {
			return // invalid, not enough data
		}
		// add extra time data in 100's of nanoseconds
		d := time.Duration(0)
		for j := 3 - n; j < n; j++ {
			d |= time.Duration(b.byte()) << (j * 8)
		}
		d *= 100
		*t = t.Add(d)
	}
}

func (a *archive15) getKeys(salt []byte) (key, iv []byte) {
	// check cache of keys
	for _, v := range a.keyCache {
		if bytes.Equal(v.salt[:], salt) {
			return v.key, v.iv
		}
	}
	key, iv = calcAes30Params(a.pass, salt)

	// save a copy in the cache
	copy(a.keyCache[1:], a.keyCache[:])
	a.keyCache[0].salt = slices.Clone(salt) // copy so byte slice can be reused
	a.keyCache[0].key = key
	a.keyCache[0].iv = iv

	return key, iv
}

func (a *archive15) parseFileHeader(h *blockHeader15) (*fileBlockHeader, error) {
	f := new(fileBlockHeader)

	f.first = h.flags&fileSplitBefore == 0
	f.last = h.flags&fileSplitAfter == 0

	f.Solid = h.flags&fileSolid > 0
	f.arcSolid = a.solid
	f.Encrypted = h.flags&fileEncrypted > 0
	f.HeaderEncrypted = a.encrypted
	f.IsDir = h.flags&fileWindowMask == fileWindowMask
	if !f.IsDir {
		f.winSize = 0x10000 << ((h.flags & fileWindowMask) >> 5)
	}

	b := h.data
	if len(b) < 21 {
		return nil, ErrCorruptFileHeader
	}

	f.PackedSize = h.dataSize
	f.UnPackedSize = int64(b.uint32())
	f.HostOS = b.byte() + 1
	if f.HostOS > HostOSBeOS {
		f.HostOS = HostOSUnknown
	}
	f.sum = slices.Clone(b.bytes(4))

	f.ModificationTime = parseDosTime(b.uint32())
	unpackver := b.byte()     // decoder version
	method := b.byte() - 0x30 // decryption method
	namesize := int(b.uint16())
	f.Attributes = int64(b.uint32())
	if h.flags&fileLargeData > 0 {
		if len(b) < 8 {
			return nil, ErrCorruptFileHeader
		}
		_ = b.uint32() // already read large PackedSize in readBlockHeader
		f.UnPackedSize |= int64(b.uint32()) << 32
		f.UnKnownSize = f.UnPackedSize == -1
	} else if int32(f.UnPackedSize) == -1 {
		f.UnKnownSize = true
		f.UnPackedSize = -1
	}
	if len(b) < namesize {
		return nil, ErrCorruptFileHeader
	}
	name := b.bytes(namesize)
	if h.flags&fileUnicode == 0 {
		f.Name = string(name)
	} else {
		f.Name = decodeName(name)
	}
	// Rar 4.x uses '\' as file separator
	f.Name = strings.Replace(f.Name, "\\", "/", -1)

	if h.flags&fileVersion > 0 {
		// file version is stored as ';n' appended to file name
		i := strings.LastIndex(f.Name, ";")
		if i > 0 {
			j, err := strconv.Atoi(f.Name[i+1:])
			if err == nil && j >= 0 {
				f.Version = j
				f.Name = f.Name[:i]
			}
		}
	}

	var salt []byte
	if h.flags&fileSalt > 0 {
		if len(b) < saltSize {
			return nil, ErrCorruptFileHeader
		}
		salt = slices.Clone(b.bytes(saltSize))
	}
	if h.flags&fileExtTime > 0 {
		readExtTimes(f, &b)
	}

	if !f.first {
		return f, nil
	}
	// fields only needed for first block in a file
	if f.Encrypted && len(salt) == saltSize && a.pass != nil {
		f.key, f.iv = a.getKeys(salt)
	}
	f.hash = newLittleEndianCRC32
	if method != 0 {
		switch unpackver {
		case 15:
			return nil, ErrUnsupportedDecoder
		case 20, 26:
			f.decVer = decode20Ver
		case 29:
			f.decVer = decode29Ver
		default:
			return nil, ErrUnknownDecoder
		}
	}
	return f, nil
}

func (a *archive15) parseArcBlock(h *blockHeader15) error {
	a.encrypted = h.flags&arcEncrypted > 0
	a.multi = h.flags&arcVolume > 0
	a.oldNaming = h.flags&arcNewNaming == 0
	a.solid = h.flags&arcSolid > 0
	if a.encrypted && a.pass == nil {
		return ErrArchiveEncrypted
	}
	return nil
}

// readBlockHeader returns the next block header in the archive.
// It will return io.EOF if there were no bytes read.
func (a *archive15) readBlockHeader(r byteReader) (*blockHeader15, error) {
	if a.encrypted {
		if a.pass == nil {
			return nil, ErrArchiveEncrypted
		}
		salt := make([]byte, saltSize)
		_, err := io.ReadFull(r, salt)
		if err != nil {
			return nil, err
		}
		key, iv := a.getKeys(salt)
		r, err = newAesDecryptReader(r, key, iv)
		if err != nil {
			return nil, err
		}
	}
	sizeBuf := make([]byte, 7)
	_, err := io.ReadFull(r, sizeBuf)
	if err != nil {
		if err == io.EOF && a.encrypted {
			err = io.ErrUnexpectedEOF
		}
		return nil, err
	}
	b := readBuf(sizeBuf)
	crc := b.uint16()
	h := new(blockHeader15)
	h.htype = b.byte()
	h.flags = b.uint16()
	size := int(b.uint16())
	if h.htype == blockArc && h.flags&arcComment > 0 {
		// comment block embedded into archive block
		if size < 13 {
			return nil, ErrCorruptBlockHeader
		}
		size = 13
	} else if size < 7 {
		return nil, ErrCorruptBlockHeader
	}

	h.data = make([]byte, size)
	copy(h.data, sizeBuf)
	_, err = io.ReadFull(r, h.data[7:])
	if err != nil {
		if err == io.EOF {
			err = io.ErrUnexpectedEOF
		}
		return nil, err
	}
	hash := crc32.NewIEEE()
	if h.htype == blockComment {
		if size < 13 {
			return nil, ErrCorruptBlockHeader
		}
		_, _ = hash.Write(h.data[2:13])
	} else {
		_, _ = hash.Write(h.data[2:])
	}
	if crc != uint16(hash.Sum32()) {
		return nil, ErrBadHeaderCRC
	}
	h.data = h.data[7:]
	if h.flags&blockHasData > 0 {
		if len(h.data) < 4 {
			return nil, ErrCorruptBlockHeader
		}
		h.dataSize = int64(h.data.uint32())
	}
	if (h.htype == blockService || h.htype == blockFile) && h.flags&fileLargeData > 0 {
		if len(h.data) < 25 {
			return nil, ErrCorruptBlockHeader
		}
		b := h.data[21:25]
		h.dataSize |= int64(b.uint32()) << 32
	}
	return h, nil
}

func (a *archive15) init(br *bufVolumeReader) (int, error) {
	a.encrypted = false // reset encryption when opening new volume file
	h, err := a.readBlockHeader(br)
	if err != nil {
		if err == io.EOF {
			err = io.ErrUnexpectedEOF
		}
	} else if h.htype != blockArc {
		err = ErrNoArchiveBlock
	} else {
		err = a.parseArcBlock(h)
	}
	return -1, err
}

// nextBlock advances to the next file block in the archive
func (a *archive15) nextBlock(br *bufVolumeReader) (*fileBlockHeader, error) {
	for {
		// could return an io.EOF here as 1.5 archives may not have an end block.
		h, err := a.readBlockHeader(br)
		if err != nil {
			if err == io.EOF {
				return nil, errVolumeOrArchiveEnd
			}
			return nil, err
		}
		switch h.htype {
		case blockFile:
			return a.parseFileHeader(h)
		case blockEnd:
			if h.flags&endArcNotLast == 0 || !a.multi {
				return nil, io.EOF
			}
			return nil, ErrMultiVolume
		default:
			if h.dataSize > 0 {
				err = br.Discard(h.dataSize) // skip over block data
				if err != nil {
					return nil, err
				}
			}
		}
	}
}

// newArchive15 creates a new archiveBlockReader for a Version 1.5 archive
func newArchive15(password *string) *archive15 {
	a := &archive15{}
	if password != nil {
		a.pass = utf16.Encode([]rune(*password)) // convert to UTF-16
	}
	return a
}
