/*
 * EliasDB
 *
 * Copyright 2016 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

/*
Package slotting contains managers which deal with slots on pages.

FreeLogicalSlotManager

FreeLogicalSlotManager is a list manager for free logical slots. This manager
object is used by the LogicalSlotManager.

FreePhysicalSlotManager

FreePhysicalSlotManager is a list manager for free physical slots. This manager
object is used by the PhysicalSlotManager.

LogicalSlotManager

LogicalSlotManager is a list manager for logical slots. Logical slots are stored
on translation pages which store just pointers to physical slots.

PhysicalSlotManager

PhysicalSlotManager is a list manager for physical slots.
*/
package slotting

import (
	"fmt"

	"devt.de/krotik/eliasdb/storage/file"
	"devt.de/krotik/eliasdb/storage/paging"
	"devt.de/krotik/eliasdb/storage/paging/view"
	"devt.de/krotik/eliasdb/storage/slotting/pageview"
	"devt.de/krotik/eliasdb/storage/util"
)

/*
FreeLogicalSlotManager data structure
*/
type FreeLogicalSlotManager struct {
	storagefile *file.StorageFile        // StorageFile which is wrapped
	pager       *paging.PagedStorageFile // Pager for StorageFile
	slots       []uint64                 // List of free slots
}

/*
NewFreeLogicalSlotManager creates a new object to manage free logical slots.
*/
func NewFreeLogicalSlotManager(psf *paging.PagedStorageFile) *FreeLogicalSlotManager {
	return &FreeLogicalSlotManager{psf.StorageFile(), psf, make([]uint64, 0)}
}

/*
Get gets a free slot.
*/
func (flsm *FreeLogicalSlotManager) Get() (uint64, error) {

	// Try to get entry from the slots list

	if len(flsm.slots) > 0 {
		freeSlot := flsm.slots[len(flsm.slots)-1]
		flsm.slots = flsm.slots[:len(flsm.slots)-1]
		return freeSlot, nil
	}

	cursor := paging.NewPageCursor(flsm.pager, view.TypeFreeLogicalSlotPage, 0)

	// No need for error checking on cursor next since all pages will be opened
	// via Get calls in the loop.

	page, _ := cursor.Next()
	for page != 0 {

		record, err := flsm.storagefile.Get(page)

		if err != nil {
			return 0, err
		}

		flsp := pageview.NewFreeLogicalSlotPage(record)

		slot := flsp.FirstAllocatedSlotInfo()

		if slot != -1 {

			// Return a found slot and free the free page if necessary

			loc := flsp.SlotInfoLocation(uint16(slot))

			// Release the slot

			flsp.ReleaseSlotInfo(uint16(slot))

			if flsp.FreeSlotCount() == 0 {

				// Free the page if no free row id slot is left

				flsm.storagefile.ReleaseInUseID(page, false)

				flsm.pager.FreePage(page)

			} else {

				flsm.storagefile.ReleaseInUseID(page, true)
			}

			return loc, nil
		}

		flsm.storagefile.ReleaseInUseID(page, false)

		page, _ = cursor.Next()
	}

	return 0, nil
}

/*
Add adds a slot to the free slot set.
*/
func (flsm *FreeLogicalSlotManager) Add(loc uint64) {
	if loc == 0 {

		// The bit pattern for the 0 location is used to mark free slots

		panic("Illigal free logical slot pattern: 0x0")
	}

	flsm.slots = append(flsm.slots, loc)
}

/*
Flush writes all added slotinfos to FreeLogicalSlotPages.
*/
func (flsm *FreeLogicalSlotManager) Flush() error {
	cursor := paging.NewPageCursor(flsm.pager, view.TypeFreeLogicalSlotPage, 0)
	index := 0

	// Go through all free logical slot pages

	// No need for error checking on cursor next since all pages will be opened
	// via Get calls in the loop.

	page, _ := cursor.Next()

	for page != 0 {

		// Need to declare err here otherwise index becomes a local for
		// the "for" block

		var err error

		index, err = flsm.doFlush(page, index)
		if err != nil {
			return err
		}

		if index >= len(flsm.slots) {
			break
		}

		page, _ = cursor.Next()
	}

	// Allocate new free logical slot pages if all present ones are full
	// and we have still slots to process

	for index < len(flsm.slots) {

		allocPage, err := flsm.pager.AllocatePage(view.TypeFreeLogicalSlotPage)
		if err != nil {
			return err
		}

		index, err = flsm.doFlush(allocPage, index)
		if err != nil {

			// Try to free the allocated page if there was an error
			// ignore any error of the FreePage call

			flsm.pager.FreePage(allocPage)

			return err
		}
	}

	// Clear lists after all slots information have been written

	flsm.slots = make([]uint64, 0)

	return nil
}

/*
doFlush writes all added slotinfos to a FreeLogicalSlotPage. Stop if the page is full.
*/
func (flsm *FreeLogicalSlotManager) doFlush(page uint64, index int) (int, error) {
	r, err := flsm.storagefile.Get(page)

	if err != nil {
		return index, err
	}

	flsp := pageview.NewFreeLogicalSlotPage(r)

	// Iterate all page slots (stop if the page has no more available slots
	// or we reached the end of the page)

	slot := flsp.FirstFreeSlotInfo()

	for ; slot != -1 && index < len(flsm.slots); index++ {

		loc := flsm.slots[index]

		offset := flsp.AllocateSlotInfo(uint16(slot))

		flsp.SetSlotInfo(offset, util.LocationRecord(loc), util.LocationOffset(loc))

		slot = flsp.FirstFreeSlotInfo()
	}

	flsm.storagefile.ReleaseInUseID(page, true)

	return index, nil
}

/*
Returns a string representation of this FreeLogicalSlotManager.
*/
func (flsm *FreeLogicalSlotManager) String() string {
	return fmt.Sprintf("FreeLogicalSlotManager: %v\nIds  :%v\n",
		flsm.storagefile.Name(), flsm.slots)
}
