package ru.dbotthepony.mc.otm.core.multiblock

import com.google.common.collect.ImmutableList
import com.google.common.collect.ImmutableSet
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.SectionPos
import net.minecraft.core.Vec3i
import net.minecraft.tags.TagKey
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.chunk.status.ChunkStatus
import ru.dbotthepony.mc.otm.core.collect.collect
import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.getBlockStateNow
import ru.dbotthepony.mc.otm.core.math.RelativeSide
import ru.dbotthepony.mc.otm.core.math.plus
import java.util.function.Predicate

inline fun shapedMultiblock(configurator: ShapedMultiblockBuilder.Node.() -> Unit): ShapedMultiblockFactory {
	val builder = ShapedMultiblockBuilder()
	configurator.invoke(builder.root())
	return builder.build()
}

class ShapedMultiblockBuilder {
	private val nodes = Object2ObjectOpenHashMap<BlockPos, Node>()
	private val customChecks = ArrayList<Predicate<IMultiblockAccess>>()

	/**
	 * Adds custom [predicate] which determines whenever selected multiblock configuration is valid
	 *
	 * Code inside predicate can safely call [ShapedMultiblock.blocks], [ShapedMultiblock.blockEntities], etc to determine whenever
	 * all demands are met
	 */
	fun customCheck(predicate: Predicate<IMultiblockAccess>): ShapedMultiblockBuilder {
		customChecks.add(predicate)
		return this
	}

	@Suppress("unchecked_cast")
	abstract inner class Part<T : Part<T>> {
		val predicates = ObjectArraySet<BlockPredicate>()
		val children = ObjectArraySet<Part<*>>()

		val builder: ShapedMultiblockBuilder
			get() = this@ShapedMultiblockBuilder

		val blockStateTags = ObjectArraySet<Any>()
		val blockTags = ObjectArraySet<Any>()
		val blockEntityTags = ObjectArraySet<BlockEntityTag<*>>()
		val displayBlocks = HashSet<BlockState>()

		/**
		 * Marks this node report its block in [ShapedMultiblock.blockStates] method when called with specified [value]
		 *
		 * [value] is searched using "value" semantics (`==` / `equals`)
		 */
		fun tagBlockState(value: Any): T {
			blockStateTags.add(value)
			return this as T
		}

		/**
		 * Marks this node report its block in [ShapedMultiblock.blockStates] method when called without arguments
		 */
		fun tagBlockState(): T {
			blockStateTags.add(IMultiblockAccess.GLOBAL_BLOCK_TAG)
			return this as T
		}

		/**
		 * Marks this node report its block in [ShapedMultiblock.blocks] method when called with specified [value]
		 *
		 * [value] is searched for using "value" semantics (`==` / `equals`)
		 */
		fun tagBlock(value: Any): T {
			blockTags.add(value)
			return this as T
		}

		/**
		 * Marks this node report its block in [ShapedMultiblock.blocks] method when called without arguments
		 */
		fun tagBlock(): T {
			blockTags.add(IMultiblockAccess.GLOBAL_BLOCK_TAG)
			return this as T
		}

		/**
		 * Marks this node to report block entities put at its position when called [ShapedMultiblock.blockEntities] with specified [value]
		 *
		 * [value] is searched for using "identity" semantics (`===` / Java's `==`)
		 */
		fun tag(value: BlockEntityTag<*>): T {
			blockEntityTags.add(value)
			return this as T
		}

		/**
		 * Removes all predicates, custom and prebuilt alike
		 */
		fun clear(): T {
			predicates.clear()
			return this as T
		}

		/**
		 * Adds a custom predicate on this node's position
		 */
		fun predicate(predicate: BlockPredicate): T {
			predicates.add(predicate)
			return this as T
		}

		/**
		 * Adds a block predicate, matching any valid blockstate of [block] on this node's position
		 */
		fun block(block: Block): T {
			predicates.add { pos, access, blockState, blockEntity ->
				blockState.block === block
			}

			return this as T
		}

		/**
		 * Adds a block tag predicate on this node's position
		 */
		fun block(block: TagKey<Block>): T {
			predicates.add { pos, access, blockState, blockEntity ->
				blockState.`is`(block)
			}

			return this as T
		}

		/**
		 * Adds "nothing" predicate (no block should be present) on this node's position
		 */
		fun air(): T {
			predicate(BlockPredicate.Air)
			return this as T
		}

		/**
		 * Adds a blockstate predicate, matching exactly provided [state] on this node's position
		 */
		fun block(state: BlockState): T {
			predicates.add { pos, access, blockState, blockEntity ->
				blockState === state
			}

			return this as T
		}

		/**
		 * Creates "and" subnode, which requires all its children to test true
		 *
		 * @see And
		 */
		fun and(): SubNode<T> {
			return SubNode(this as T, Strategy.AND)
		}

		/**
		 * Creates "or" subnode, which requires at least one of its children to test true
		 */
		fun or(): SubNode<T> {
			return SubNode(this as T, Strategy.OR_EITHER)
		}

		/**
		 * Creates "and" subnode, which requires all its children to test true
		 */
		fun and(configurator: SubNode<T>.() -> Unit): SubNode<T> {
			return SubNode(this as T, Strategy.AND).also(configurator)
		}

		/**
		 * Creates "or" subnode, which requires at least one of its children to test true
		 */
		fun or(configurator: SubNode<T>.() -> Unit): SubNode<T> {
			return SubNode(this as T, Strategy.OR_EITHER).also(configurator)
		}

		protected fun build(pos: BlockPos): ShapedMultiblockFactory.Part {
			return ShapedMultiblockFactory.Part(
				pos, strategy, ImmutableList.copyOf(predicates),
				children.stream().map { it.build(pos) }.collect(ImmutableList.toImmutableList()),
				ImmutableSet.copyOf(blockStateTags),
				ImmutableSet.copyOf(blockTags),
				ImmutableSet.copyOf(blockEntityTags),
				ImmutableSet.copyOf(displayBlocks)
			)
		}

		abstract val strategy: Strategy
	}

	/**
	 * Logical subnode, which combines all children using [strategy]
	 */
	inner class SubNode<P : Part<P>>(val parent: P, override val strategy: Strategy) : Part<SubNode<P>>() {
		init {
			parent.children.add(this)
		}

		fun end() = parent
	}

	/**
	 * By default, tests children using [Strategy.OR_BOTH] strategy
	 */
	inner class Node(val pos: BlockPos) : Part<Node>() {
		init {
			check(nodes.put(pos, this) == null) { "Trying to create new node at $pos while already having one" }
		}

		/**
		 * Creates new node relative to this node at [diff], which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(diff: Vec3i): Node {
			return node(pos + diff)
		}

		/**
		 * Creates new node relative to this node at [dir] side, which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(dir: Direction): Node {
			return relative(dir.normal)
		}

		/**
		 * Creates new node relative to this node at [dir] side, which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(dir: RelativeSide): Node {
			return relative(dir.default)
		}

		/**
		 * Creates new node relative to this node at [diff], and configures it in-place using provided [configurator] expression, which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(diff: Vec3i, configurator: Node.() -> Unit): Node {
			return node(pos + diff).also(configurator)
		}

		/**
		 * Creates new node relative to this node at [dir] side, and configures it in-place using provided [configurator] expression, which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(dir: Direction, configurator: Node.() -> Unit): Node {
			return relative(dir.normal).also(configurator)
		}

		/**
		 * Creates new node relative to this node at [dir] side, and configures it in-place using provided [configurator] expression, which behaves by default as [Strategy.OR_EITHER]
		 */
		fun relative(dir: RelativeSide, configurator: Node.() -> Unit): Node {
			return relative(dir.default).also(configurator)
		}

		fun front() = relative(RelativeSide.FRONT)
		fun back() = relative(RelativeSide.BACK)
		fun left() = relative(RelativeSide.LEFT)
		fun right() = relative(RelativeSide.RIGHT)
		fun top() = relative(RelativeSide.TOP)
		fun bottom() = relative(RelativeSide.BOTTOM)

		fun front(block: Block) = relative(RelativeSide.FRONT).also { it.block(block) }
		fun back(block: Block) = relative(RelativeSide.BACK).also { it.block(block) }
		fun left(block: Block) = relative(RelativeSide.LEFT).also { it.block(block) }
		fun right(block: Block) = relative(RelativeSide.RIGHT).also { it.block(block) }
		fun top(block: Block) = relative(RelativeSide.TOP).also { it.block(block) }
		fun bottom(block: Block) = relative(RelativeSide.BOTTOM).also { it.block(block) }

		fun front(block: BlockState) = relative(RelativeSide.FRONT).also { it.block(block) }
		fun back(block: BlockState) = relative(RelativeSide.BACK).also { it.block(block) }
		fun left(block: BlockState) = relative(RelativeSide.LEFT).also { it.block(block) }
		fun right(block: BlockState) = relative(RelativeSide.RIGHT).also { it.block(block) }
		fun top(block: BlockState) = relative(RelativeSide.TOP).also { it.block(block) }
		fun bottom(block: BlockState) = relative(RelativeSide.BOTTOM).also { it.block(block) }

		fun front(predicate: BlockPredicate) = relative(RelativeSide.FRONT).also { it.predicate(predicate) }
		fun back(predicate: BlockPredicate) = relative(RelativeSide.BACK).also { it.predicate(predicate) }
		fun left(predicate: BlockPredicate) = relative(RelativeSide.LEFT).also { it.predicate(predicate) }
		fun right(predicate: BlockPredicate) = relative(RelativeSide.RIGHT).also { it.predicate(predicate) }
		fun top(predicate: BlockPredicate) = relative(RelativeSide.TOP).also { it.predicate(predicate) }
		fun bottom(predicate: BlockPredicate) = relative(RelativeSide.BOTTOM).also { it.predicate(predicate) }

		fun front(configurator: Node.() -> Unit) = relative(RelativeSide.FRONT).also(configurator)
		fun back(configurator: Node.() -> Unit) = relative(RelativeSide.BACK).also(configurator)
		fun left(configurator: Node.() -> Unit) = relative(RelativeSide.LEFT).also(configurator)
		fun right(configurator: Node.() -> Unit) = relative(RelativeSide.RIGHT).also(configurator)
		fun top(configurator: Node.() -> Unit) = relative(RelativeSide.TOP).also(configurator)
		fun bottom(configurator: Node.() -> Unit) = relative(RelativeSide.BOTTOM).also(configurator)

		fun front(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.FRONT).also { it.block(block) }.also(configurator)
		fun back(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.BACK).also { it.block(block) }.also(configurator)
		fun left(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.LEFT).also { it.block(block) }.also(configurator)
		fun right(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.RIGHT).also { it.block(block) }.also(configurator)
		fun top(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.TOP).also { it.block(block) }.also(configurator)
		fun bottom(block: Block, configurator: Node.() -> Unit) = relative(RelativeSide.BOTTOM).also { it.block(block) }.also(configurator)

		fun front(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.FRONT).also { it.block(block) }.also(configurator)
		fun back(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.BACK).also { it.block(block) }.also(configurator)
		fun left(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.LEFT).also { it.block(block) }.also(configurator)
		fun right(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.RIGHT).also { it.block(block) }.also(configurator)
		fun top(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.TOP).also { it.block(block) }.also(configurator)
		fun bottom(block: BlockState, configurator: Node.() -> Unit) = relative(RelativeSide.BOTTOM).also { it.block(block) }.also(configurator)

		fun front(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.FRONT).also { it.predicate(predicate) }.also(configurator)
		fun back(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.BACK).also { it.predicate(predicate) }.also(configurator)
		fun left(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.LEFT).also { it.predicate(predicate) }.also(configurator)
		fun right(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.RIGHT).also { it.predicate(predicate) }.also(configurator)
		fun top(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.TOP).also { it.predicate(predicate) }.also(configurator)
		fun bottom(predicate: BlockPredicate, configurator: Node.() -> Unit) = relative(RelativeSide.BOTTOM).also { it.predicate(predicate) }.also(configurator)

		fun build(): ShapedMultiblockFactory.Part {
			return build(pos)
		}

		override var strategy: Strategy = Strategy.OR_EITHER
	}

	/**
	 * Creates new (or returns existing one) node at specified position and returns it
	 */
	fun node(at: BlockPos): Node {
		return nodes[at] ?: Node(at)
	}

	/**
	 * Returns node which represents multiblock "root" position (relative position of 0 0 0)
	 */
	fun root() = node(BlockPos.ZERO)

	/**
	 * Returns node which represents multiblock "root" position (relative position of 0 0 0), and configures it using provided [configurator] expression
	 */
	fun root(configurator: Node.() -> Unit) = node(BlockPos.ZERO).also(configurator)

	/**
	 * Makes a deep copy of this [ShapedMultiblockBuilder], which can be modified independently of this builder
	 */
	fun copy(): ShapedMultiblockBuilder {
		val copied = ShapedMultiblockBuilder()

		for ((k, v) in nodes) {
			val node = copied.Node(k)
			copied.nodes[k] = node
			node.predicates.addAll(v.predicates)
		}

		return copied
	}

	/**
	 * Creates [ShapedMultiblockFactory] out of this [ShapedMultiblockBuilder].
	 *
	 * Created factory does not share reference(s) to this builder, and this builder can be mutated further without consequences.
	 */
	fun build(): ShapedMultiblockFactory {
		return ShapedMultiblockFactory(nodes.values.iterator().map { it.build() }.collect(ImmutableSet.toImmutableSet()), ImmutableList.copyOf(customChecks))
	}
}
