//
// Copyright (c) SAS Institute Inc.
//
// 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 comdoc

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

// Convert a sector ID to an absolute file position
func (r *ComDoc) sectorToOffset(sector SecID) int64 {
	if sector < 0 {
		return -1
	}
	return r.FirstSector + int64(sector)*int64(r.SectorSize)
}

// Read the specified sector
func (r *ComDoc) readSector(sector SecID, buf []byte) error {
	_, err := r.File.ReadAt(buf, r.sectorToOffset(sector))
	return err
}

// Read the specified sector into a binary structure. It must be exactly 1
// sector in size already.
func (r *ComDoc) readSectorStruct(sector SecID, v interface{}) error {
	if err := r.readSector(sector, r.sectorBuf); err != nil {
		return err
	}
	return binary.Read(bytes.NewReader(r.sectorBuf), binary.LittleEndian, v)
}

// Write to a specified sector. Content will be padded with zeroes if it is
// less than a full sector in length.
func (r *ComDoc) writeSector(sector SecID, content []byte) error {
	if len(content) > r.SectorSize {
		panic("excessive write")
	} else if len(content) < r.SectorSize {
		buf := r.sectorBuf
		copy(buf, content)
		for i := len(content); i < r.SectorSize; i++ {
			buf[i] = 0
		}
		content = buf[:r.SectorSize]
	}
	_, err := r.writer.WriteAt(content, r.sectorToOffset(sector))
	return err
}

// Mark a chain of sectors as free
func freeSectors(sat []SecID, sector SecID) {
	for {
		nextSector := sat[sector]
		sat[sector] = SecIDFree
		if nextSector < 0 {
			break
		}
		sector = nextSector
	}
}

// Return a list of "count" free sectors or short sectors, extending the table if needed
func (r *ComDoc) makeFreeSectors(count int, short bool) []SecID {
	if count <= 0 {
		return nil
	}
	freeList := make([]SecID, 0, count)
	var sat []SecID
	if short {
		sat = r.SSAT
	} else {
		sat = r.SAT
	}
	// scan for existing free sectors
	for i, j := range sat {
		if j != SecIDFree {
			continue
		}
		freeList = append(freeList, SecID(i))
		count--
		if count == 0 {
			return freeList
		}
	}
	// extend the sector table
	sectorsPerBlock := r.SectorSize / 4
	needBlocks := (count + sectorsPerBlock - 1) / sectorsPerBlock
	oldCount := len(sat)
	newSAT := append(sat, make([]SecID, needBlocks*sectorsPerBlock)...)
	for i := oldCount; i < len(newSAT); i++ {
		newSAT[i] = SecIDFree
		if count > 0 {
			freeList = append(freeList, SecID(i))
			count--
		}
	}
	if short {
		r.SSAT = newSAT
	} else {
		r.SAT = newSAT
	}
	return freeList
}

// Read the sector allocation table.
//
// Each index within the table corresponds to a sector within the file itself.
// The value held at that index in the SAT either points to the ID of the
// sector holding the next chunk of data for that stream, or -2 to indicate
// there are no more sectors.
func (r *ComDoc) readSAT() error {
	count := r.SectorSize / 4
	sat := make([]SecID, count*int(r.Header.SATSectors))
	position := 0
	for _, sector := range r.MSAT {
		if sector < 0 {
			continue
		}
		if position >= len(sat) {
			return errors.New("msat has more sectors than indicated")
		}
		if err := r.readSectorStruct(sector, sat[position:position+count]); err != nil {
			return err
		}
		position += count
	}
	r.SAT = sat
	return nil
}

// Write the new sector allocation table to the listed sector IDs
func (r *ComDoc) writeSAT(freeList []SecID) error {
	satPerSector := r.SectorSize / 4
	buf := bytes.NewBuffer(r.sectorBuf)
	for i, sector := range freeList {
		j := i * satPerSector
		buf.Reset()
		binary.Write(buf, binary.LittleEndian, r.SAT[j:j+satPerSector])
		if err := r.writeSector(sector, buf.Bytes()); err != nil {
			return err
		}
	}
	return nil
}
