package ru.dbotthepony.kstarbound

import com.google.common.collect.ImmutableMap
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonElement
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import ru.dbotthepony.kommons.util.KOptional
import java.util.Arrays
import java.util.concurrent.Callable
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.ForkJoinTask
import java.util.stream.Stream
import kotlin.reflect.KProperty

inline fun <reified T> GsonBuilder.registerTypeAdapter(adapter: TypeAdapter<T>): GsonBuilder {
	return registerTypeAdapter(T::class.java, adapter)
}

inline fun <reified T> GsonBuilder.registerTypeAdapter(noinline factory: (Gson) -> TypeAdapter<T>): GsonBuilder {
	val token = TypeToken.get(T::class.java)

	return registerTypeAdapterFactory(object : TypeAdapterFactory {
		override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
			if (type == token) {
				return factory(gson) as TypeAdapter<T>
			}

			return null
		}
	})
}

fun <T> Array<T>.stream(): Stream<T> = Arrays.stream(this)

operator fun <T> ThreadLocal<T>.getValue(thisRef: Any, property: KProperty<*>): T {
	return get()
}

operator fun <T> ThreadLocal<T>.setValue(thisRef: Any, property: KProperty<*>, value: T) {
	set(value)
}

operator fun <K : Any, V : Any> ImmutableMap.Builder<K, V>.set(key: K, value: V): ImmutableMap.Builder<K, V> = put(key, value)

fun String.sintern(): String = Starbound.STRINGS.intern(this)

inline fun <reified T> Gson.fromJson(reader: JsonReader): T? = fromJson<T>(reader, T::class.java)
inline fun <reified T> Gson.fromJson(reader: JsonElement): T? = fromJson<T>(reader, T::class.java)

/**
 * guarantees even distribution of tasks while also preserving encountered order of elements
 */
fun <T> Collection<IStarboundFile>.batch(executor: ForkJoinPool, batchSize: Int = 16, mapper: (IStarboundFile) -> KOptional<T>): Stream<T> {
	require(batchSize >= 1) { "Invalid batch size: $batchSize" }

	if (batchSize == 1 || size <= batchSize) {
		val tasks = ArrayList<ForkJoinTask<KOptional<T>>>()

		for (listedFile in this) {
			tasks.add(executor.submit(Callable { mapper.invoke(listedFile) }))
		}

		return tasks.stream().map { it.join() }.filter { it.isPresent }.map { it.value }
	}

	val batches = ArrayList<ForkJoinTask<List<KOptional<T>>>>()
	var batch = ArrayList<IStarboundFile>(batchSize)

	for (listedFile in this) {
		batch.add(listedFile)

		if (batch.size >= batchSize) {
			val mbatch = batch
			batches.add(executor.submit(Callable { mbatch.map { mapper.invoke(it) } }))
			batch = ArrayList(batchSize)
		}
	}

	if (batch.isNotEmpty())
		batches.add(executor.submit(Callable { batch.map { mapper.invoke(it) } }))

	return batches.stream().flatMap { it.join().stream() }.filter { it.isPresent }.map { it.value }
}
