// Copyright 2019 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package ext

import (
	"io"
	"math"

	"gvisor.dev/gvisor/pkg/binary"
	"gvisor.dev/gvisor/pkg/syserror"
)

const (
	// numDirectBlks is the number of direct blocks in ext block map inodes.
	numDirectBlks = 12
)

// blockMapFile is a type of regular file which uses direct/indirect block
// addressing to store file data. This was deprecated in ext4.
type blockMapFile struct {
	regFile regularFile

	// directBlks are the direct blocks numbers. The physical blocks pointed by
	// these holds file data. Contains file blocks 0 to 11.
	directBlks [numDirectBlks]uint32

	// indirectBlk is the physical block which contains (blkSize/4) direct block
	// numbers (as uint32 integers).
	indirectBlk uint32

	// doubleIndirectBlk is the physical block which contains (blkSize/4) indirect
	// block numbers (as uint32 integers).
	doubleIndirectBlk uint32

	// tripleIndirectBlk is the physical block which contains (blkSize/4) doubly
	// indirect block numbers (as uint32 integers).
	tripleIndirectBlk uint32

	// coverage at (i)th index indicates the amount of file data a node at
	// height (i) covers. Height 0 is the direct block.
	coverage [4]uint64
}

// Compiles only if blockMapFile implements io.ReaderAt.
var _ io.ReaderAt = (*blockMapFile)(nil)

// newBlockMapFile is the blockMapFile constructor. It initializes the file to
// physical blocks map with (at most) the first 12 (direct) blocks.
func newBlockMapFile(regFile regularFile) (*blockMapFile, error) {
	file := &blockMapFile{regFile: regFile}
	file.regFile.impl = file

	for i := uint(0); i < 4; i++ {
		file.coverage[i] = getCoverage(regFile.inode.blkSize, i)
	}

	blkMap := regFile.inode.diskInode.Data()
	binary.Unmarshal(blkMap[:numDirectBlks*4], binary.LittleEndian, &file.directBlks)
	binary.Unmarshal(blkMap[numDirectBlks*4:(numDirectBlks+1)*4], binary.LittleEndian, &file.indirectBlk)
	binary.Unmarshal(blkMap[(numDirectBlks+1)*4:(numDirectBlks+2)*4], binary.LittleEndian, &file.doubleIndirectBlk)
	binary.Unmarshal(blkMap[(numDirectBlks+2)*4:(numDirectBlks+3)*4], binary.LittleEndian, &file.tripleIndirectBlk)
	return file, nil
}

// ReadAt implements io.ReaderAt.ReadAt.
func (f *blockMapFile) ReadAt(dst []byte, off int64) (int, error) {
	if len(dst) == 0 {
		return 0, nil
	}

	if off < 0 {
		return 0, syserror.EINVAL
	}

	offset := uint64(off)
	size := f.regFile.inode.diskInode.Size()
	if offset >= size {
		return 0, io.EOF
	}

	// dirBlksEnd is the file offset until which direct blocks cover file data.
	// Direct blocks cover 0 <= file offset < dirBlksEnd.
	dirBlksEnd := numDirectBlks * f.coverage[0]

	// indirBlkEnd is the file offset until which the indirect block covers file
	// data. The indirect block covers dirBlksEnd <= file offset < indirBlkEnd.
	indirBlkEnd := dirBlksEnd + f.coverage[1]

	// doubIndirBlkEnd is the file offset until which the double indirect block
	// covers file data. The double indirect block covers the range
	// indirBlkEnd <= file offset < doubIndirBlkEnd.
	doubIndirBlkEnd := indirBlkEnd + f.coverage[2]

	read := 0
	toRead := len(dst)
	if uint64(toRead)+offset > size {
		toRead = int(size - offset)
	}
	for read < toRead {
		var err error
		var curR int

		// Figure out which block to delegate the read to.
		switch {
		case offset < dirBlksEnd:
			// Direct block.
			curR, err = f.read(f.directBlks[offset/f.regFile.inode.blkSize], offset%f.regFile.inode.blkSize, 0, dst[read:])
		case offset < indirBlkEnd:
			// Indirect block.
			curR, err = f.read(f.indirectBlk, offset-dirBlksEnd, 1, dst[read:])
		case offset < doubIndirBlkEnd:
			// Doubly indirect block.
			curR, err = f.read(f.doubleIndirectBlk, offset-indirBlkEnd, 2, dst[read:])
		default:
			// Triply indirect block.
			curR, err = f.read(f.tripleIndirectBlk, offset-doubIndirBlkEnd, 3, dst[read:])
		}

		read += curR
		offset += uint64(curR)
		if err != nil {
			return read, err
		}
	}

	if read < len(dst) {
		return read, io.EOF
	}
	return read, nil
}

// read is the recursive step of the ReadAt function. It relies on knowing the
// current node's location on disk (curPhyBlk) and its height in the block map
// tree. A height of 0 shows that the current node is actually holding file
// data. relFileOff tells the offset from which we need to start to reading
// under the current node. It is completely relative to the current node.
func (f *blockMapFile) read(curPhyBlk uint32, relFileOff uint64, height uint, dst []byte) (int, error) {
	curPhyBlkOff := int64(curPhyBlk) * int64(f.regFile.inode.blkSize)
	if height == 0 {
		toRead := int(f.regFile.inode.blkSize - relFileOff)
		if len(dst) < toRead {
			toRead = len(dst)
		}

		n, _ := f.regFile.inode.dev.ReadAt(dst[:toRead], curPhyBlkOff+int64(relFileOff))
		if n < toRead {
			return n, syserror.EIO
		}
		return n, nil
	}

	childCov := f.coverage[height-1]
	startIdx := relFileOff / childCov
	endIdx := f.regFile.inode.blkSize / 4 // This is exclusive.
	wantEndIdx := (relFileOff + uint64(len(dst))) / childCov
	wantEndIdx++ // Make this exclusive.
	if wantEndIdx < endIdx {
		endIdx = wantEndIdx
	}

	read := 0
	curChildOff := relFileOff % childCov
	for i := startIdx; i < endIdx; i++ {
		var childPhyBlk uint32
		err := readFromDisk(f.regFile.inode.dev, curPhyBlkOff+int64(i*4), &childPhyBlk)
		if err != nil {
			return read, err
		}

		n, err := f.read(childPhyBlk, curChildOff, height-1, dst[read:])
		read += n
		if err != nil {
			return read, err
		}

		curChildOff = 0
	}

	return read, nil
}

// getCoverage returns the number of bytes a node at the given height covers.
// Height 0 is the file data block itself. Height 1 is the indirect block.
//
// Formula: blkSize * ((blkSize / 4)^height)
func getCoverage(blkSize uint64, height uint) uint64 {
	return blkSize * uint64(math.Pow(float64(blkSize/4), float64(height)))
}