/*
 * SPDX-FileCopyrightText: © 2017-2025 Istari Digital, Inc.
 * SPDX-License-Identifier: Apache-2.0
 */

package sim

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

var (
	// ErrDone is returned when the underlying file has ran out of lines.
	ErrDone = errors.New("no more values in the Simulator")
	// ErrBadLine is returned when the trace file line is unrecognizable to
	// the Parser.
	ErrBadLine = errors.New("bad line for trace format")
)

// Simulator is the central type of the `sim` package. It is a function
// returning a key from some source (composed from the other functions in this
// package, either generated or parsed). You can use these Simulators to
// approximate access distributions.
type Simulator func() (uint64, error)

// NewZipfian creates a Simulator returning numbers following a Zipfian [1]
// distribution infinitely. Zipfian distributions are useful for simulating real
// workloads.
//
// [1]: https://en.wikipedia.org/wiki/Zipf%27s_law
func NewZipfian(s, v float64, n uint64) Simulator {
	z := rand.NewZipf(rand.New(rand.NewSource(time.Now().UnixNano())), s, v, n)
	return func() (uint64, error) {
		return z.Uint64(), nil
	}
}

// NewUniform creates a Simulator returning uniformly distributed [1] (random)
// numbers [0, max) infinitely.
//
// [1]: https://en.wikipedia.org/wiki/Uniform_distribution_(continuous)
func NewUniform(max uint64) Simulator {
	m := int64(max)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	return func() (uint64, error) {
		return uint64(r.Int63n(m)), nil
	}
}

// Parser is used as a parameter to NewReader so we can create Simulators from
// varying trace file formats easily.
type Parser func(string, error) ([]uint64, error)

// NewReader creates a Simulator from two components: the Parser, which is a
// filetype specific function for parsing lines, and the file itself, which will
// be read from.
//
// When every line in the file has been read, ErrDone will be returned. For some
// trace formats (LIRS) there is one item per line. For others (ARC) there is a
// range of items on each line. Thus, the true number of items in each file
// is hard to determine, so it's up to the user to handle ErrDone accordingly.
func NewReader(parser Parser, file io.Reader) Simulator {
	b := bufio.NewReader(file)
	s := make([]uint64, 0)
	i := -1
	var err error
	return func() (uint64, error) {
		// only parse a new line when we've run out of items
		if i++; i == len(s) {
			// parse sequence from line
			if s, err = parser(b.ReadString('\n')); err != nil {
				s = []uint64{0}
			}
			i = 0
		}
		return s[i], err
	}
}

// ParseLIRS takes a single line of input from a LIRS trace file as described in
// multiple papers [1] and returns a slice containing one number. A nice
// collection of LIRS trace files can be found in Ben Manes' repo [2].
//
// [1]: https://en.wikipedia.org/wiki/LIRS_caching_algorithm
// [2]: https://git.io/fj9gU
func ParseLIRS(line string, err error) ([]uint64, error) {
	if line = strings.TrimSpace(line); line != "" {
		// example: "1\r\n"
		key, err := strconv.ParseUint(line, 10, 64)
		return []uint64{key}, err
	}
	return nil, ErrDone
}

// ParseARC takes a single line of input from an ARC trace file as described in
// "ARC: a self-tuning, low overhead replacement cache" [1] by Nimrod Megiddo
// and Dharmendra S. Modha [1] and returns a sequence of numbers generated from
// the line and any error. For use with NewReader.
//
// [1]: https://scinapse.io/papers/1860107648
func ParseARC(line string, err error) ([]uint64, error) {
	if line != "" {
		// example: "0 5 0 0\n"
		//
		// -  first block: starting number in sequence
		// - second block: number of items in sequence
		// -  third block: ignore
		// - fourth block: global line number (not used)
		cols := strings.Fields(line)
		if len(cols) != 4 {
			return nil, ErrBadLine
		}
		start, err := strconv.ParseUint(cols[0], 10, 64)
		if err != nil {
			return nil, err
		}
		count, err := strconv.ParseUint(cols[1], 10, 64)
		if err != nil {
			return nil, err
		}
		// populate sequence from start to start + count
		seq := make([]uint64, count)
		for i := range seq {
			seq[i] = start + uint64(i)
		}
		return seq, nil
	}
	return nil, ErrDone
}

// Collection evaluates the Simulator size times and saves each item to the
// returned slice.
func Collection(simulator Simulator, size uint64) []uint64 {
	collection := make([]uint64, size)
	for i := range collection {
		collection[i], _ = simulator()
	}
	return collection
}

// StringCollection evaluates the Simulator size times and saves each item to
// the returned slice, after converting it to a string.
func StringCollection(simulator Simulator, size uint64) []string {
	collection := make([]string, size)
	for i := range collection {
		n, _ := simulator()
		collection[i] = fmt.Sprintf("%d", n)
	}
	return collection
}
