import { Random } from 'random'
import type { TestConfig } from './framework-types'
import type { Computed, ReactiveFramework, Signal } from './reactive-framework'

export interface Graph {
	sources: Signal<number>[]
	layers: Computed<number>[][]
}

/**
 * Make a rectangular dependency graph, with an equal number of source elements
 * and computation elements at every layer.
 *
 * @param width number of source elements and number of computed elements per layer
 * @param totalLayers total number of source and computed layers
 * @param staticFraction every nth computed node is static (1 = all static, 3 = 2/3rd are dynamic)
 * @returns the graph
 */
export function makeGraph(
	framework: ReactiveFramework,
	config: TestConfig,
	counter: Counter,
): Graph {
	const { width, totalLayers, staticFraction, nSources } = config

	return framework.withBuild(() => {
		const sources = new Array(width)
			.fill(0)
			.map((_, i) => framework.signal(i))
		const rows = makeDependentRows(
			sources,
			totalLayers - 1,
			counter,
			staticFraction,
			nSources,
			framework,
		)
		const graph = { sources, layers: rows }
		return graph
	})
}

/**
 * Execute the graph by writing one of the sources and reading some or all of the leaves.
 *
 * @return the sum of all leaf values
 */
export function runGraph(
	graph: Graph,
	iterations: number,
	readFraction: number,
	framework: ReactiveFramework,
): number {
	const rand = new Random('seed')
	const { sources, layers } = graph
	// biome-ignore lint/style/noNonNullAssertion: test
	const leaves = layers[layers.length - 1]!
	const skipCount = Math.round(leaves.length * (1 - readFraction))
	const readLeaves = removeElems(leaves, skipCount, rand)
	const frameworkName = framework.name.toLowerCase()
	// const start = Date.now();
	let sum = 0

	if (frameworkName === 's-js' || frameworkName === 'solidjs') {
		// [S.js freeze](https://github.com/adamhaile/S#sdatavalue) doesn't allow different values to be set during a single batch, so special case it.
		for (let i = 0; i < iterations; i++) {
			framework.withBatch(() => {
				const sourceDex = i % sources.length
				// biome-ignore lint/style/noNonNullAssertion: test
				sources[sourceDex]!.write(i + sourceDex)
			})

			for (const leaf of readLeaves) {
				leaf.read()
			}
		}

		sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
	} else {
		framework.withBatch(() => {
			for (let i = 0; i < iterations; i++) {
				// Useful for debugging edge cases for some frameworks that experience
				// dramatic slow downs for certain test configurations. These are generally
				// due to `computed` effects not being cached efficiently, and as the number
				// of layers increases, the uncached `computed` effects are re-evaluated in
				// an `O(n^2)` manner where `n` is the number of layers.
				/* if (i % 100 === 0) {
           console.log("iteration:", i, "delta:", Date.now() - start);
        } */

				const sourceDex = i % sources.length
				// biome-ignore lint/style/noNonNullAssertion: test
				sources[sourceDex]!.write(i + sourceDex)

				for (const leaf of readLeaves) {
					leaf.read()
				}
			}

			sum = readLeaves.reduce((total, leaf) => leaf.read() + total, 0)
		})
	}

	return sum
}

function removeElems<T>(src: T[], rmCount: number, rand: Random): T[] {
	const copy = src.slice()
	for (let i = 0; i < rmCount; i++) {
		const rmDex = rand.int(0, copy.length - 1)
		copy.splice(rmDex, 1)
	}
	return copy
}

export class Counter {
	count = 0
}

function makeDependentRows(
	sources: Computed<number>[],
	numRows: number,
	counter: Counter,
	staticFraction: number,
	nSources: number,
	framework: ReactiveFramework,
): Computed<number>[][] {
	let prevRow = sources
	const rand = new Random('seed')
	const rows: Computed<number>[][] = []
	for (let l = 0; l < numRows; l++) {
		const row = makeRow(
			prevRow,
			counter,
			staticFraction,
			nSources,
			framework,
			l,
			rand,
		)
		rows.push(row)
		prevRow = row
	}
	return rows
}

function makeRow(
	sources: Computed<number>[],
	counter: Counter,
	staticFraction: number,
	nSources: number,
	framework: ReactiveFramework,
	_layer: number,
	random: Random,
): Computed<number>[] {
	return sources.map((_, myDex) => {
		const mySources: Computed<number>[] = []
		for (let sourceDex = 0; sourceDex < nSources; sourceDex++) {
			// biome-ignore lint/style/noNonNullAssertion: test
			mySources.push(sources[(myDex + sourceDex) % sources.length]!)
		}

		const staticNode = random.float() < staticFraction
		if (staticNode) {
			// static node, always reference sources
			return framework.computed(() => {
				counter.count++

				let sum = 0
				for (const src of mySources) {
					sum += src.read()
				}
				return sum
			})
		} else {
			// dynamic node, drops one of the sources depending on the value of the first element
			// biome-ignore lint/style/noNonNullAssertion: test
			const first = mySources[0]!
			const tail = mySources.slice(1)
			const node = framework.computed(() => {
				counter.count++
				let sum = first.read()
				const shouldDrop = sum & 0x1
				const dropDex = sum % tail.length

				for (let i = 0; i < tail.length; i++) {
					if (shouldDrop && i === dropDex) continue
					// biome-ignore lint/style/noNonNullAssertion: test
					sum += tail[i]!.read()
				}

				return sum
			})
			return node
		}
	})
}
