package com.simibubi.create.content.logistics.trains.management.schedule;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL30;

import com.google.common.collect.ImmutableList;
import com.jozufozu.flywheel.util.transform.TransformStack;
import com.mojang.blaze3d.platform.InputConstants;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Matrix4f;
import com.simibubi.create.Create;
import com.simibubi.create.content.logistics.trains.GlobalRailwayManager;
import com.simibubi.create.content.logistics.trains.TrackGraph;
import com.simibubi.create.content.logistics.trains.management.edgePoint.EdgePointType;
import com.simibubi.create.content.logistics.trains.management.edgePoint.station.GlobalStation;
import com.simibubi.create.content.logistics.trains.management.schedule.condition.ScheduleWaitCondition;
import com.simibubi.create.content.logistics.trains.management.schedule.condition.ScheduledDelay;
import com.simibubi.create.content.logistics.trains.management.schedule.destination.DestinationInstruction;
import com.simibubi.create.content.logistics.trains.management.schedule.destination.ScheduleInstruction;
import com.simibubi.create.foundation.gui.AllGuiTextures;
import com.simibubi.create.foundation.gui.AllIcons;
import com.simibubi.create.foundation.gui.ModularGuiLine;
import com.simibubi.create.foundation.gui.ModularGuiLineBuilder;
import com.simibubi.create.foundation.gui.UIRenderHelper;
import com.simibubi.create.foundation.gui.element.GuiGameElement;
import com.simibubi.create.foundation.gui.menu.AbstractSimiContainerScreen;
import com.simibubi.create.foundation.gui.menu.GhostItemSubmitPacket;
import com.simibubi.create.foundation.gui.widget.IconButton;
import com.simibubi.create.foundation.gui.widget.Indicator;
import com.simibubi.create.foundation.gui.widget.Indicator.State;
import com.simibubi.create.foundation.gui.widget.Label;
import com.simibubi.create.foundation.gui.widget.SelectionScrollInput;
import com.simibubi.create.foundation.networking.AllPackets;
import com.simibubi.create.foundation.utility.Components;
import com.simibubi.create.foundation.utility.IntAttached;
import com.simibubi.create.foundation.utility.Lang;
import com.simibubi.create.foundation.utility.Pair;
import com.simibubi.create.foundation.utility.animation.LerpedFloat;
import com.simibubi.create.foundation.utility.animation.LerpedFloat.Chaser;

import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.Widget;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.client.gui.GuiUtils;

public class ScheduleScreen extends AbstractSimiContainerScreen<ScheduleMenu> {

	private static final int CARD_HEADER = 22;
	private static final int CARD_WIDTH = 195;

	private List<Rect2i> extraAreas = Collections.emptyList();

	private List<LerpedFloat> horizontalScrolls = new ArrayList<>();
	private LerpedFloat scroll = LerpedFloat.linear()
		.startWithValue(0);

	private Schedule schedule;

	private IconButton confirmButton;
	private IconButton cyclicButton;
	private Indicator cyclicIndicator;

	private IconButton resetProgress;
	private IconButton skipProgress;

	private ScheduleInstruction editingDestination;
	private ScheduleWaitCondition editingCondition;
	private SelectionScrollInput scrollInput;
	private Label scrollInputLabel;
	private IconButton editorConfirm, editorDelete;
	private ModularGuiLine editorSubWidgets;
	private Consumer<Boolean> onEditorClose;

	private DestinationSuggestions destinationSuggestions;

	public ScheduleScreen(ScheduleMenu menu, Inventory inv, Component title) {
		super(menu, inv, title);
		schedule = new Schedule();
		CompoundTag tag = menu.contentHolder.getOrCreateTag()
			.getCompound("Schedule");
		if (!tag.isEmpty())
			schedule = Schedule.fromTag(tag);
		menu.slotsActive = false;
		editorSubWidgets = new ModularGuiLine();
	}

	@Override
	protected void init() {
		AllGuiTextures bg = AllGuiTextures.SCHEDULE;
		setWindowSize(bg.width, bg.height);
		super.init();
		clearWidgets();

		confirmButton = new IconButton(leftPos + bg.width - 42, topPos + bg.height - 24, AllIcons.I_CONFIRM);
		confirmButton.withCallback(() -> minecraft.player.closeContainer());
		addRenderableWidget(confirmButton);

		cyclicIndicator = new Indicator(leftPos + 21, topPos + 196, Components.immutableEmpty());
		cyclicIndicator.state = schedule.cyclic ? State.ON : State.OFF;
		addRenderableWidget(cyclicIndicator);

		cyclicButton = new IconButton(leftPos + 21, topPos + 202, AllIcons.I_REFRESH);
		cyclicButton.withCallback(() -> {
			schedule.cyclic = !schedule.cyclic;
			cyclicIndicator.state = schedule.cyclic ? State.ON : State.OFF;
		});

		List<Component> tip = cyclicButton.getToolTip();
		tip.add(Lang.translateDirect("schedule.loop"));
		tip.add(Lang.translateDirect("schedule.loop1")
			.withStyle(ChatFormatting.GRAY));
		tip.add(Lang.translateDirect("schedule.loop2")
			.withStyle(ChatFormatting.GRAY));

		addRenderableWidget(cyclicButton);

		resetProgress = new IconButton(leftPos + 45, topPos + 202, AllIcons.I_PRIORITY_VERY_HIGH);
		resetProgress.withCallback(() -> {
			schedule.savedProgress = 0;
			resetProgress.active = false;
		});
		resetProgress.active = schedule.savedProgress > 0 && !schedule.entries.isEmpty();
		resetProgress.setToolTip(Lang.translateDirect("schedule.reset"));
		addRenderableWidget(resetProgress);

		skipProgress = new IconButton(leftPos + 63, topPos + 202, AllIcons.I_PRIORITY_LOW);
		skipProgress.withCallback(() -> {
			schedule.savedProgress++;
			schedule.savedProgress %= schedule.entries.size();
			resetProgress.active = schedule.savedProgress > 0;
		});
		skipProgress.active = schedule.entries.size() > 1;
		skipProgress.setToolTip(Lang.translateDirect("schedule.skip"));
		addRenderableWidget(skipProgress);

		stopEditing();
		extraAreas = ImmutableList.of(new Rect2i(leftPos + bg.width, topPos + bg.height - 56, 48, 48));
		horizontalScrolls.clear();
		for (int i = 0; i < schedule.entries.size(); i++)
			horizontalScrolls.add(LerpedFloat.linear()
				.startWithValue(0));
	}

	protected void startEditing(IScheduleInput field, Consumer<Boolean> onClose, boolean allowDeletion) {
		onEditorClose = onClose;
		confirmButton.visible = false;
		cyclicButton.visible = false;
		cyclicIndicator.visible = false;
		skipProgress.visible = false;
		resetProgress.visible = false;

		scrollInput = new SelectionScrollInput(leftPos + 56, topPos + 65, 143, 16);
		scrollInputLabel = new Label(leftPos + 59, topPos + 69, Components.immutableEmpty()).withShadow();
		editorConfirm = new IconButton(leftPos + 56 + 168, topPos + 65 + 22, AllIcons.I_CONFIRM);
		if (allowDeletion)
			editorDelete = new IconButton(leftPos + 56 - 45, topPos + 65 + 22, AllIcons.I_TRASH);
		menu.slotsActive = true;
		menu.targetSlotsActive = field.slotsTargeted();

		for (int i = 0; i < field.slotsTargeted(); i++) {
			ItemStack item = field.getItem(i);
			menu.ghostInventory.setStackInSlot(i, item);
			AllPackets.getChannel().sendToServer(new GhostItemSubmitPacket(item, i));
		}

		if (field instanceof ScheduleInstruction instruction) {
			int startIndex = 0;
			for (int i = 0; i < Schedule.INSTRUCTION_TYPES.size(); i++)
				if (Schedule.INSTRUCTION_TYPES.get(i)
					.getFirst()
					.equals(instruction.getId()))
					startIndex = i;
			editingDestination = instruction;
			updateEditorSubwidgets(editingDestination);
			scrollInput.forOptions(Schedule.getTypeOptions(Schedule.INSTRUCTION_TYPES))
				.titled(Lang.translateDirect("schedule.instruction_type"))
				.writingTo(scrollInputLabel)
				.calling(index -> {
					ScheduleInstruction newlyCreated = Schedule.INSTRUCTION_TYPES.get(index)
						.getSecond()
						.get();
					if (editingDestination.getId()
						.equals(newlyCreated.getId()))
						return;
					editingDestination = newlyCreated;
					updateEditorSubwidgets(editingDestination);
				})
				.setState(startIndex);
		}

		if (field instanceof ScheduleWaitCondition cond) {
			int startIndex = 0;
			for (int i = 0; i < Schedule.CONDITION_TYPES.size(); i++)
				if (Schedule.CONDITION_TYPES.get(i)
					.getFirst()
					.equals(cond.getId()))
					startIndex = i;
			editingCondition = cond;
			updateEditorSubwidgets(editingCondition);
			scrollInput.forOptions(Schedule.getTypeOptions(Schedule.CONDITION_TYPES))
				.titled(Lang.translateDirect("schedule.condition_type"))
				.writingTo(scrollInputLabel)
				.calling(index -> {
					ScheduleWaitCondition newlyCreated = Schedule.CONDITION_TYPES.get(index)
						.getSecond()
						.get();
					if (editingCondition.getId()
						.equals(newlyCreated.getId()))
						return;
					editingCondition = newlyCreated;
					updateEditorSubwidgets(editingCondition);
				})
				.setState(startIndex);
		}

		addRenderableWidget(scrollInput);
		addRenderableWidget(scrollInputLabel);
		addRenderableWidget(editorConfirm);
		if (allowDeletion)
			addRenderableWidget(editorDelete);
	}

	private void onDestinationEdited(String text) {
		if (destinationSuggestions != null)
			destinationSuggestions.updateCommandInfo();
	}

	protected void stopEditing() {
		confirmButton.visible = true;
		cyclicButton.visible = true;
		cyclicIndicator.visible = true;
		skipProgress.visible = true;
		resetProgress.visible = true;

		if (editingCondition == null && editingDestination == null)
			return;

		destinationSuggestions = null;

		removeWidget(scrollInput);
		removeWidget(scrollInputLabel);
		removeWidget(editorConfirm);
		removeWidget(editorDelete);

		IScheduleInput editing = editingCondition == null ? editingDestination : editingCondition;
		for (int i = 0; i < editing.slotsTargeted(); i++) {
			editing.setItem(i, menu.ghostInventory.getStackInSlot(i));
			AllPackets.getChannel().sendToServer(new GhostItemSubmitPacket(ItemStack.EMPTY, i));
		}

		editorSubWidgets.saveValues(editing.getData());
		editorSubWidgets.forEach(this::removeWidget);
		editorSubWidgets.clear();

		editingCondition = null;
		editingDestination = null;
		editorConfirm = null;
		editorDelete = null;
		menu.slotsActive = false;
		init();
	}

	protected void updateEditorSubwidgets(IScheduleInput field) {
		destinationSuggestions = null;
		menu.targetSlotsActive = field.slotsTargeted();

		editorSubWidgets.forEach(this::removeWidget);
		editorSubWidgets.clear();
		field.initConfigurationWidgets(
			new ModularGuiLineBuilder(font, editorSubWidgets, getGuiLeft() + 77, getGuiTop() + 92).speechBubble());
		editorSubWidgets.loadValues(field.getData(), this::addRenderableWidget, this::addRenderableOnly);

		if (!(field instanceof DestinationInstruction))
			return;

		editorSubWidgets.forEach(e -> {
			if (!(e instanceof EditBox destinationBox))
				return;
			destinationSuggestions = new DestinationSuggestions(this.minecraft, this, destinationBox, this.font,
				getViableStations(field), topPos + 33);
			destinationSuggestions.setAllowSuggestions(true);
			destinationSuggestions.updateCommandInfo();
			destinationBox.setResponder(this::onDestinationEdited);
		});
	}

	private List<IntAttached<String>> getViableStations(IScheduleInput field) {
		GlobalRailwayManager railwayManager = Create.RAILWAYS.sided(null);
		Set<TrackGraph> viableGraphs = new HashSet<>(railwayManager.trackNetworks.values());

		for (ScheduleEntry entry : schedule.entries) {
			if (!(entry.instruction instanceof DestinationInstruction destination))
				continue;
			if (destination == field)
				continue;
			String filter = destination.getFilterForRegex();
			if (filter.isBlank())
				continue;
			Graphs: for (Iterator<TrackGraph> iterator = viableGraphs.iterator(); iterator.hasNext();) {
				TrackGraph trackGraph = iterator.next();
				for (GlobalStation station : trackGraph.getPoints(EdgePointType.STATION)) {
					if (station.name.matches(filter))
						continue Graphs;
				}
				iterator.remove();
			}
		}

		if (viableGraphs.isEmpty())
			viableGraphs = new HashSet<>(railwayManager.trackNetworks.values());

		Vec3 position = minecraft.player.position();
		Set<String> visited = new HashSet<>();

		return viableGraphs.stream()
			.flatMap(g -> g.getPoints(EdgePointType.STATION)
				.stream())
			.filter(station -> station.blockEntityPos != null)
			.filter(station -> visited.add(station.name))
			.map(station -> IntAttached.with((int) Vec3.atBottomCenterOf(station.blockEntityPos)
				.distanceTo(position), station.name))
			.toList();
	}

	@Override
	protected void containerTick() {
		super.containerTick();
		scroll.tickChaser();
		for (LerpedFloat lerpedFloat : horizontalScrolls)
			lerpedFloat.tickChaser();

		if (destinationSuggestions != null)
			destinationSuggestions.tick();

		schedule.savedProgress =
			schedule.entries.isEmpty() ? 0 : Mth.clamp(schedule.savedProgress, 0, schedule.entries.size() - 1);
		resetProgress.active = schedule.savedProgress > 0;
		skipProgress.active = schedule.entries.size() > 1;
	}

	@Override
	public void render(PoseStack matrixStack, int mouseX, int mouseY, float partialTicks) {
		partialTicks = minecraft.getFrameTime();

		if (menu.slotsActive)
			super.render(matrixStack, mouseX, mouseY, partialTicks);
		else {
			renderBackground(matrixStack);
			renderBg(matrixStack, partialTicks, mouseX, mouseY);
			for (Widget widget : this.renderables)
				widget.render(matrixStack, mouseX, mouseY, partialTicks);
			renderForeground(matrixStack, mouseX, mouseY, partialTicks);
		}
	}

	protected void renderSchedule(PoseStack matrixStack, int mouseX, int mouseY, float partialTicks) {
		UIRenderHelper.swapAndBlitColor(minecraft.getMainRenderTarget(), UIRenderHelper.framebuffer);
		UIRenderHelper.drawStretched(matrixStack, leftPos + 33, topPos + 16, 3, 173, -100,
			AllGuiTextures.SCHEDULE_STRIP_DARK);

		int yOffset = 25;
		List<ScheduleEntry> entries = schedule.entries;
		float scrollOffset = -scroll.getValue(partialTicks);

		for (int i = 0; i <= entries.size(); i++) {

			if (schedule.savedProgress == i && !schedule.entries.isEmpty()) {
				matrixStack.pushPose();
				float expectedY = scrollOffset + topPos + yOffset + 4;
				float actualY = Mth.clamp(expectedY, topPos + 18, topPos + 170);
				matrixStack.translate(0, actualY, 0);
				(expectedY == actualY ? AllGuiTextures.SCHEDULE_POINTER : AllGuiTextures.SCHEDULE_POINTER_OFFSCREEN)
					.render(matrixStack, leftPos, 0);
				matrixStack.popPose();
			}

			startStencil(matrixStack, leftPos + 16, topPos + 16, 220, 173);
			matrixStack.pushPose();
			matrixStack.translate(0, scrollOffset, 0);
			if (i == 0 || entries.size() == 0)
				UIRenderHelper.drawStretched(matrixStack, leftPos + 33, topPos + 16, 3, 10, -100,
					AllGuiTextures.SCHEDULE_STRIP_LIGHT);

			if (i == entries.size()) {
				if (i > 0)
					yOffset += 9;
				AllGuiTextures.SCHEDULE_STRIP_END.render(matrixStack, leftPos + 29, topPos + yOffset);
				AllGuiTextures.SCHEDULE_CARD_NEW.render(matrixStack, leftPos + 43, topPos + yOffset);
				matrixStack.popPose();
				endStencil();
				break;
			}

			ScheduleEntry scheduleEntry = entries.get(i);
			int cardY = yOffset;
			int cardHeight = renderScheduleEntry(matrixStack, scheduleEntry, cardY, mouseX, mouseY, partialTicks);
			yOffset += cardHeight;

			if (i + 1 < entries.size()) {
				AllGuiTextures.SCHEDULE_STRIP_DOTTED.render(matrixStack, leftPos + 29, topPos + yOffset - 3);
				yOffset += 10;
			}

			matrixStack.popPose();
			endStencil();

			if (!scheduleEntry.instruction.supportsConditions())
				continue;

			float h = cardHeight - 26;
			float y1 = cardY + 24 + scrollOffset;
			float y2 = y1 + h;
			if (y2 > 189)
				h -= y2 - 189;
			if (y1 < 16) {
				float correction = 16 - y1;
				y1 += correction;
				h -= correction;
			}

			if (h <= 0)
				continue;

			startStencil(matrixStack, leftPos + 43, topPos + y1, 161, h);
			matrixStack.pushPose();
			matrixStack.translate(0, scrollOffset, 0);
			renderScheduleConditions(matrixStack, scheduleEntry, cardY, mouseX, mouseY, partialTicks, cardHeight, i);
			matrixStack.popPose();
			endStencil();

			if (isConditionAreaScrollable(scheduleEntry)) {
				startStencil(matrixStack, leftPos + 16, topPos + 16, 220, 173);
				matrixStack.pushPose();
				matrixStack.translate(0, scrollOffset, 0);
				int center = (cardHeight - 8 + CARD_HEADER) / 2;
				float chaseTarget = horizontalScrolls.get(i)
					.getChaseTarget();
				if (!Mth.equal(chaseTarget, 0))
					AllGuiTextures.SCHEDULE_SCROLL_LEFT.render(matrixStack, leftPos + 40, topPos + cardY + center);
				if (!Mth.equal(chaseTarget, scheduleEntry.conditions.size() - 1))
					AllGuiTextures.SCHEDULE_SCROLL_RIGHT.render(matrixStack, leftPos + 203, topPos + cardY + center);
				matrixStack.popPose();
				endStencil();
			}
		}

		int zLevel = 200;
		Matrix4f mat = matrixStack.last()
			.pose();
		GuiUtils.drawGradientRect(mat, zLevel, leftPos + 16, topPos + 16, leftPos + 16 + 220, topPos + 16 + 10,
			0x77000000, 0x00000000);
		GuiUtils.drawGradientRect(mat, zLevel, leftPos + 16, topPos + 179, leftPos + 16 + 220, topPos + 179 + 10,
			0x00000000, 0x77000000);
		UIRenderHelper.swapAndBlitColor(UIRenderHelper.framebuffer, minecraft.getMainRenderTarget());
	}

	public int renderScheduleEntry(PoseStack matrixStack, ScheduleEntry entry, int yOffset, int mouseX, int mouseY,
		float partialTicks) {
		int zLevel = -100;

		AllGuiTextures light = AllGuiTextures.SCHEDULE_CARD_LIGHT;
		AllGuiTextures medium = AllGuiTextures.SCHEDULE_CARD_MEDIUM;
		AllGuiTextures dark = AllGuiTextures.SCHEDULE_CARD_DARK;

		int cardWidth = CARD_WIDTH;
		int cardHeader = CARD_HEADER;
		int maxRows = 0;
		for (List<ScheduleWaitCondition> list : entry.conditions)
			maxRows = Math.max(maxRows, list.size());
		boolean supportsConditions = entry.instruction.supportsConditions();
		int cardHeight = cardHeader + (supportsConditions ? 24 + maxRows * 18 : 4);

		matrixStack.pushPose();
		matrixStack.translate(leftPos + 25, topPos + yOffset, 0);

		UIRenderHelper.drawStretched(matrixStack, 0, 1, cardWidth, cardHeight - 2, zLevel, light);
		UIRenderHelper.drawStretched(matrixStack, 1, 0, cardWidth - 2, cardHeight, zLevel, light);
		UIRenderHelper.drawStretched(matrixStack, 1, 1, cardWidth - 2, cardHeight - 2, zLevel, dark);
		UIRenderHelper.drawStretched(matrixStack, 2, 2, cardWidth - 4, cardHeight - 4, zLevel, medium);
		UIRenderHelper.drawStretched(matrixStack, 2, 2, cardWidth - 4, cardHeader, zLevel,
			supportsConditions ? light : medium);

		AllGuiTextures.SCHEDULE_CARD_REMOVE.render(matrixStack, cardWidth - 14, 2);
		AllGuiTextures.SCHEDULE_CARD_DUPLICATE.render(matrixStack, cardWidth - 14, cardHeight - 14);

		int i = schedule.entries.indexOf(entry);
		if (i > 0)
			AllGuiTextures.SCHEDULE_CARD_MOVE_UP.render(matrixStack, cardWidth, cardHeader - 14);
		if (i < schedule.entries.size() - 1)
			AllGuiTextures.SCHEDULE_CARD_MOVE_DOWN.render(matrixStack, cardWidth, cardHeader);

		UIRenderHelper.drawStretched(matrixStack, 8, 0, 3, cardHeight + 10, zLevel,
			AllGuiTextures.SCHEDULE_STRIP_LIGHT);
		(supportsConditions ? AllGuiTextures.SCHEDULE_STRIP_TRAVEL : AllGuiTextures.SCHEDULE_STRIP_ACTION)
			.render(matrixStack, 4, 6);

		if (supportsConditions)
			AllGuiTextures.SCHEDULE_STRIP_WAIT.render(matrixStack, 4, 28);

		Pair<ItemStack, Component> destination = entry.instruction.getSummary();
		renderInput(matrixStack, destination, 26, 5, false, 100);
		entry.instruction.renderSpecialIcon(matrixStack, 30, 5);

		matrixStack.popPose();

		return cardHeight;
	}

	public void renderScheduleConditions(PoseStack matrixStack, ScheduleEntry entry, int yOffset, int mouseX,
		int mouseY, float partialTicks, int cardHeight, int entryIndex) {
		int cardWidth = CARD_WIDTH;
		int cardHeader = CARD_HEADER;

		matrixStack.pushPose();
		matrixStack.translate(leftPos + 25, topPos + yOffset, 0);
		int xOffset = 26;
		float scrollOffset = getConditionScroll(entry, partialTicks, entryIndex);

		matrixStack.pushPose();
		matrixStack.translate(-scrollOffset, 0, 0);

		for (List<ScheduleWaitCondition> list : entry.conditions) {
			int maxWidth = getConditionColumnWidth(list);
			for (int i = 0; i < list.size(); i++) {
				ScheduleWaitCondition scheduleWaitCondition = list.get(i);
				Math.max(maxWidth, renderInput(matrixStack, scheduleWaitCondition.getSummary(), xOffset, 29 + i * 18,
					i != 0, maxWidth));
				scheduleWaitCondition.renderSpecialIcon(matrixStack, xOffset + 4, 29 + i * 18);
			}

			AllGuiTextures.SCHEDULE_CONDITION_APPEND.render(matrixStack, xOffset + (maxWidth - 10) / 2,
				29 + list.size() * 18);
			xOffset += maxWidth + 10;
		}

		AllGuiTextures.SCHEDULE_CONDITION_NEW.render(matrixStack, xOffset - 3, 29);
		matrixStack.popPose();

		if (xOffset + 16 > cardWidth - 26) {
			TransformStack.cast(matrixStack)
				.rotateZ(-90);
			Matrix4f m = matrixStack.last()
				.pose();
			GuiUtils.drawGradientRect(m, 200, -cardHeight + 2, 18, -2 - cardHeader, 28, 0x44000000, 0x00000000);
			GuiUtils.drawGradientRect(m, 200, -cardHeight + 2, cardWidth - 26, -2 - cardHeader, cardWidth - 16,
				0x00000000, 0x44000000);
		}

		matrixStack.popPose();
	}

	private boolean isConditionAreaScrollable(ScheduleEntry entry) {
		int xOffset = 26;
		for (List<ScheduleWaitCondition> list : entry.conditions)
			xOffset += getConditionColumnWidth(list) + 10;
		return xOffset + 16 > CARD_WIDTH - 26;
	}

	private float getConditionScroll(ScheduleEntry entry, float partialTicks, int entryIndex) {
		float scrollOffset = 0;
		float scrollIndex = horizontalScrolls.get(entryIndex)
			.getValue(partialTicks);
		for (List<ScheduleWaitCondition> list : entry.conditions) {
			int maxWidth = getConditionColumnWidth(list);
			float partialOfThisColumn = Math.min(1, scrollIndex);
			scrollOffset += (maxWidth + 10) * partialOfThisColumn;
			scrollIndex -= partialOfThisColumn;
		}
		return scrollOffset;
	}

	private int getConditionColumnWidth(List<ScheduleWaitCondition> list) {
		int maxWidth = 0;
		for (ScheduleWaitCondition scheduleWaitCondition : list)
			maxWidth = Math.max(maxWidth, getFieldSize(32, scheduleWaitCondition.getSummary()));
		return maxWidth;
	}

	protected int renderInput(PoseStack matrixStack, Pair<ItemStack, Component> pair, int x, int y, boolean clean,
		int minSize) {
		ItemStack stack = pair.getFirst();
		Component text = pair.getSecond();
		boolean hasItem = !stack.isEmpty();
		int fieldSize = Math.min(getFieldSize(minSize, pair), 150);
		matrixStack.pushPose();

		AllGuiTextures left =
			clean ? AllGuiTextures.SCHEDULE_CONDITION_LEFT_CLEAN : AllGuiTextures.SCHEDULE_CONDITION_LEFT;
		AllGuiTextures middle = AllGuiTextures.SCHEDULE_CONDITION_MIDDLE;
		AllGuiTextures item = AllGuiTextures.SCHEDULE_CONDITION_ITEM;
		AllGuiTextures right = AllGuiTextures.SCHEDULE_CONDITION_RIGHT;

		matrixStack.translate(x, y, 0);
		UIRenderHelper.drawStretched(matrixStack, 0, 0, fieldSize, 16, -100, middle);
		left.render(matrixStack, clean ? 0 : -3, 0);
		right.render(matrixStack, fieldSize - 2, 0);
		if (hasItem)
			item.render(matrixStack, 3, 0);
		if (hasItem) {
			item.render(matrixStack, 3, 0);
			if (stack.getItem() != Items.STRUCTURE_VOID)
				GuiGameElement.of(stack)
					.at(4, 0)
					.render(matrixStack);
		}

		if (text != null)
			font.drawShadow(matrixStack, font.substrByWidth(text, 120)
				.getString(), hasItem ? 28 : 8, 4, 0xff_f2f2ee);

		matrixStack.popPose();
		return fieldSize;
	}

	private Component clickToEdit = Lang.translateDirect("gui.schedule.lmb_edit")
		.withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC);
	private Component rClickToDelete = Lang.translateDirect("gui.schedule.rmb_remove")
		.withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC);

	public boolean action(PoseStack ms, double mouseX, double mouseY, int click) {
		if (editingCondition != null || editingDestination != null)
			return false;

		Component empty = Components.immutableEmpty();

		int mx = (int) mouseX;
		int my = (int) mouseY;
		int x = mx - leftPos - 25;
		int y = my - topPos - 25;
		if (x < 0 || x >= 205)
			return false;
		if (y < 0 || y >= 173)
			return false;
		y += scroll.getValue(0);

		List<ScheduleEntry> entries = schedule.entries;
		for (int i = 0; i < entries.size(); i++) {
			ScheduleEntry entry = entries.get(i);
			int maxRows = 0;
			for (List<ScheduleWaitCondition> list : entry.conditions)
				maxRows = Math.max(maxRows, list.size());
			int cardHeight = CARD_HEADER + (entry.instruction.supportsConditions() ? 24 + maxRows * 18 : 4);

			if (y >= cardHeight + 5) {
				y -= cardHeight + 10;
				if (y < 0)
					return false;
				continue;
			}

			int fieldSize = getFieldSize(100, entry.instruction.getSummary());
			if (x > 25 && x <= 25 + fieldSize && y > 4 && y <= 20) {
				List<Component> components = new ArrayList<>();
				components.addAll(entry.instruction.getTitleAs("instruction"));
				components.add(empty);
				components.add(clickToEdit);
				renderTooltip(ms, components, Optional.empty(), mx, my);
				if (click == 0)
					startEditing(entry.instruction, confirmed -> {
						if (confirmed)
							entry.instruction = editingDestination;
					}, false);
				return true;
			}

			if (x > 180 && x <= 192) {
				if (y > 0 && y <= 14) {
					renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.remove_entry")), Optional.empty(),
						mx, my);
					if (click == 0) {
						entries.remove(entry);
						init();
					}
					return true;
				}
				if (y > cardHeight - 14) {
					renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.duplicate")), Optional.empty(), mx,
						my);
					if (click == 0) {
						entries.add(entries.indexOf(entry), entry.clone());
						init();
					}
					return true;
				}
			}

			if (x > 194) {
				if (y > 7 && y <= 20 && i > 0) {
					renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.move_up")), Optional.empty(), mx,
						my);
					if (click == 0) {
						entries.remove(entry);
						entries.add(i - 1, entry);
						init();
					}
					return true;
				}
				if (y > 20 && y <= 33 && i < entries.size() - 1) {
					renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.move_down")), Optional.empty(), mx,
						my);
					if (click == 0) {
						entries.remove(entry);
						entries.add(i + 1, entry);
						init();
					}
					return true;
				}
			}

			int center = (cardHeight - 8 + CARD_HEADER) / 2;
			if (y > center - 1 && y <= center + 7 && isConditionAreaScrollable(entry)) {
				float chaseTarget = horizontalScrolls.get(i)
					.getChaseTarget();
				if (x > 12 && x <= 19 && !Mth.equal(chaseTarget, 0)) {
					if (click == 0)
						horizontalScrolls.get(i)
							.chase(chaseTarget - 1, 0.5f, Chaser.EXP);
					return true;
				}
				if (x > 177 && x <= 184 && !Mth.equal(chaseTarget, entry.conditions.size() - 1)) {
					if (click == 0)
						horizontalScrolls.get(i)
							.chase(chaseTarget + 1, 0.5f, Chaser.EXP);
					return true;
				}
			}

			x -= 18;
			y -= 28;
			if (x < 0 || y < 0 || x > 160)
				return false;
			x += getConditionScroll(entry, 0, i) - 8;

			List<List<ScheduleWaitCondition>> columns = entry.conditions;
			for (int j = 0; j < columns.size(); j++) {
				List<ScheduleWaitCondition> conditions = columns.get(j);
				if (x < 0)
					return false;
				int w = getConditionColumnWidth(conditions);
				if (x >= w) {
					x -= w + 10;
					continue;
				}

				int row = y / 18;
				if (row < conditions.size() && row >= 0) {
					boolean canRemove = conditions.size() > 1 || columns.size() > 1;
					List<Component> components = new ArrayList<>();
					components.add(Lang.translateDirect("schedule.condition_type")
						.withStyle(ChatFormatting.GRAY));
					ScheduleWaitCondition condition = conditions.get(row);
					components.addAll(condition.getTitleAs("condition"));
					components.add(empty);
					components.add(clickToEdit);
					if (canRemove)
						components.add(rClickToDelete);
					renderTooltip(ms, components, Optional.empty(), mx, my);
					if (canRemove && click == 1) {
						conditions.remove(row);
						if (conditions.isEmpty())
							columns.remove(conditions);
					}
					if (click == 0)
						startEditing(condition, confirmed -> {
							conditions.remove(row);
							if (confirmed) {
								conditions.add(row, editingCondition);
								return;
							}
							if (conditions.isEmpty())
								columns.remove(conditions);
						}, canRemove);
					return true;
				}

				if (y > 18 * conditions.size() && y <= 18 * conditions.size() + 10 && x >= w / 2 - 5 && x < w / 2 + 5) {
					renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.add_condition")), Optional.empty(),
						mx, my);
					if (click == 0)
						startEditing(new ScheduledDelay(), confirmed -> {
							if (confirmed)
								conditions.add(editingCondition);
						}, true);
					return true;
				}

				return false;
			}

			if (x < 0 || x > 15 || y > 20)
				return false;

			renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.alternative_condition")), Optional.empty(),
				mx, my);
			if (click == 0)
				startEditing(new ScheduledDelay(), confirmed -> {
					if (!confirmed)
						return;
					ArrayList<ScheduleWaitCondition> conditions = new ArrayList<>();
					conditions.add(editingCondition);
					columns.add(conditions);
				}, true);
			return true;
		}

		if (x < 18 || x > 33 || y > 14)
			return false;

		renderTooltip(ms, ImmutableList.of(Lang.translateDirect("gui.schedule.add_entry")), Optional.empty(), mx, my);
		if (click == 0)
			startEditing(new DestinationInstruction(), confirmed -> {
				if (!confirmed)
					return;

				ScheduleEntry entry = new ScheduleEntry();
				ScheduledDelay delay = new ScheduledDelay();
				ArrayList<ScheduleWaitCondition> initialConditions = new ArrayList<>();
				initialConditions.add(delay);
				entry.instruction = editingDestination;
				entry.conditions.add(initialConditions);
				schedule.entries.add(entry);
			}, true);
		return true;
	}

	private int getFieldSize(int minSize, Pair<ItemStack, Component> pair) {
		ItemStack stack = pair.getFirst();
		Component text = pair.getSecond();
		boolean hasItem = !stack.isEmpty();
		return Math.max((text == null ? 0 : font.width(text)) + (hasItem ? 20 : 0) + 16, minSize);
	}

	protected void startStencil(PoseStack matrixStack, float x, float y, float w, float h) {
		RenderSystem.clear(GL30.GL_STENCIL_BUFFER_BIT | GL30.GL_DEPTH_BUFFER_BIT, Minecraft.ON_OSX);

		GL11.glDisable(GL11.GL_STENCIL_TEST);
		RenderSystem.stencilMask(~0);
		RenderSystem.clear(GL11.GL_STENCIL_BUFFER_BIT, Minecraft.ON_OSX);
		GL11.glEnable(GL11.GL_STENCIL_TEST);
		RenderSystem.stencilOp(GL11.GL_REPLACE, GL11.GL_KEEP, GL11.GL_KEEP);
		RenderSystem.stencilMask(0xFF);
		RenderSystem.stencilFunc(GL11.GL_NEVER, 1, 0xFF);

		matrixStack.pushPose();
		matrixStack.translate(x, y, 0);
		matrixStack.scale(w, h, 1);
		GuiUtils.drawGradientRect(matrixStack.last()
			.pose(), -100, 0, 0, 1, 1, 0xff000000, 0xff000000);
		matrixStack.popPose();

		GL11.glEnable(GL11.GL_STENCIL_TEST);
		RenderSystem.stencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
		RenderSystem.stencilFunc(GL11.GL_EQUAL, 1, 0xFF);
	}

	protected void endStencil() {
		GL11.glDisable(GL11.GL_STENCIL_TEST);
	}

	@Override
	public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) {
		if (destinationSuggestions != null
			&& destinationSuggestions.mouseClicked((int) pMouseX, (int) pMouseY, pButton))
			return true;
		if (editorConfirm != null && editorConfirm.isMouseOver(pMouseX, pMouseY) && onEditorClose != null) {
			onEditorClose.accept(true);
			stopEditing();
			return true;
		}
		if (editorDelete != null && editorDelete.isMouseOver(pMouseX, pMouseY) && onEditorClose != null) {
			onEditorClose.accept(false);
			stopEditing();
			return true;
		}
		if (action(new PoseStack(), pMouseX, pMouseY, pButton))
			return true;

		return super.mouseClicked(pMouseX, pMouseY, pButton);
	}

	@Override
	public boolean keyPressed(int pKeyCode, int pScanCode, int pModifiers) {
		if (destinationSuggestions != null && destinationSuggestions.keyPressed(pKeyCode, pScanCode, pModifiers))
			return true;
		if (editingCondition == null && editingDestination == null)
			return super.keyPressed(pKeyCode, pScanCode, pModifiers);
		InputConstants.Key mouseKey = InputConstants.getKey(pKeyCode, pScanCode);
		boolean hitEnter = getFocused() instanceof EditBox && (pKeyCode == 257 || pKeyCode == 335);
		boolean hitE = getFocused() == null && minecraft.options.keyInventory.isActiveAndMatches(mouseKey);
		if (hitE || hitEnter) {
			onEditorClose.accept(true);
			stopEditing();
			return true;
		}
		return super.keyPressed(pKeyCode, pScanCode, pModifiers);
	}

	@Override
	public boolean mouseScrolled(double pMouseX, double pMouseY, double pDelta) {
		if (destinationSuggestions != null && destinationSuggestions.mouseScrolled(Mth.clamp(pDelta, -1.0D, 1.0D)))
			return true;
		if (editingCondition != null || editingDestination != null)
			return super.mouseScrolled(pMouseX, pMouseY, pDelta);

		if (hasShiftDown()) {
			List<ScheduleEntry> entries = schedule.entries;
			int y = (int) (pMouseY - topPos - 25 + scroll.getValue());
			for (int i = 0; i < entries.size(); i++) {
				ScheduleEntry entry = entries.get(i);
				int maxRows = 0;
				for (List<ScheduleWaitCondition> list : entry.conditions)
					maxRows = Math.max(maxRows, list.size());
				int cardHeight = CARD_HEADER + 24 + maxRows * 18;

				if (y >= cardHeight) {
					y -= cardHeight + 9;
					if (y < 0)
						break;
					continue;
				}

				if (!isConditionAreaScrollable(entry))
					break;
				if (y < 24)
					break;
				if (pMouseX < leftPos + 25)
					break;
				if (pMouseX > leftPos + 205)
					break;
				float chaseTarget = horizontalScrolls.get(i)
					.getChaseTarget();
				if (pDelta > 0 && !Mth.equal(chaseTarget, 0)) {
					horizontalScrolls.get(i)
						.chase(chaseTarget - 1, 0.5f, Chaser.EXP);
					return true;
				}
				if (pDelta < 0 && !Mth.equal(chaseTarget, entry.conditions.size() - 1)) {
					horizontalScrolls.get(i)
						.chase(chaseTarget + 1, 0.5f, Chaser.EXP);
					return true;
				}
				return false;
			}
		}

		float chaseTarget = scroll.getChaseTarget();
		float max = 40 - 173;
		for (ScheduleEntry scheduleEntry : schedule.entries) {
			int maxRows = 0;
			for (List<ScheduleWaitCondition> list : scheduleEntry.conditions)
				maxRows = Math.max(maxRows, list.size());
			max += CARD_HEADER + 24 + maxRows * 18 + 10;
		}
		if (max > 0) {
			chaseTarget -= pDelta * 12;
			chaseTarget = Mth.clamp(chaseTarget, 0, max);
			scroll.chase((int) chaseTarget, 0.7f, Chaser.EXP);
		} else
			scroll.chase(0, 0.7f, Chaser.EXP);

		return super.mouseScrolled(pMouseX, pMouseY, pDelta);
	}

	@Override
	protected void renderForeground(PoseStack matrixStack, int mouseX, int mouseY, float partialTicks) {
		if (destinationSuggestions != null) {
			matrixStack.pushPose();
			matrixStack.translate(0, 0, 500);
			destinationSuggestions.render(matrixStack, mouseX, mouseY);
			matrixStack.popPose();
		}

		super.renderForeground(matrixStack, mouseX, mouseY, partialTicks);

		GuiGameElement.of(menu.contentHolder).<GuiGameElement
			.GuiRenderBuilder>at(leftPos + AllGuiTextures.SCHEDULE.width, topPos + AllGuiTextures.SCHEDULE.height - 56,
				-200)
			.scale(3)
			.render(matrixStack);
		action(matrixStack, mouseX, mouseY, -1);

		if (editingCondition == null && editingDestination == null)
			return;

		int x = leftPos + 53;
		int y = topPos + 87;
		if (mouseX < x || mouseY < y || mouseX >= x + 120 || mouseY >= y + 18)
			return;

		IScheduleInput rendered = editingCondition == null ? editingDestination : editingCondition;

		for (int i = 0; i < Math.max(1, rendered.slotsTargeted()); i++) {
			List<Component> secondLineTooltip = rendered.getSecondLineTooltip(i);
			if (secondLineTooltip == null || (hoveredSlot != menu.getSlot(36 + i) || !hoveredSlot.getItem()
				.isEmpty()))
				continue;
			renderTooltip(matrixStack, secondLineTooltip, Optional.empty(), mouseX, mouseY);
		}
	}

	@Override
	protected void renderBg(PoseStack pPoseStack, float pPartialTick, int pMouseX, int pMouseY) {
		AllGuiTextures.SCHEDULE.render(pPoseStack, leftPos, topPos);
		FormattedCharSequence formattedcharsequence = title.getVisualOrderText();
		int center = leftPos + (AllGuiTextures.SCHEDULE.width - 8) / 2;
		font.draw(pPoseStack, formattedcharsequence, (float) (center - font.width(formattedcharsequence) / 2),
			(float) topPos + 4, 0x505050);
		renderSchedule(pPoseStack, pMouseX, pMouseY, pPartialTick);

		if (editingCondition == null && editingDestination == null)
			return;

		this.fillGradient(pPoseStack, 0, 0, this.width, this.height, -1072689136, -804253680);
		AllGuiTextures.SCHEDULE_EDITOR.render(pPoseStack, leftPos - 2, topPos + 40);
		AllGuiTextures.PLAYER_INVENTORY.render(pPoseStack, leftPos + 38, topPos + 122);
		font.draw(pPoseStack, playerInventoryTitle, leftPos + 46, topPos + 128, 0x505050);

		formattedcharsequence = editingCondition == null ? Lang.translateDirect("schedule.instruction.editor")
			.getVisualOrderText()
			: Lang.translateDirect("schedule.condition.editor")
				.getVisualOrderText();
		font.draw(pPoseStack, formattedcharsequence, (float) (center - font.width(formattedcharsequence) / 2),
			(float) topPos + 44, 0x505050);

		IScheduleInput rendered = editingCondition == null ? editingDestination : editingCondition;

		for (int i = 0; i < rendered.slotsTargeted(); i++)
			AllGuiTextures.SCHEDULE_EDITOR_ADDITIONAL_SLOT.render(pPoseStack, leftPos + 53 + 20 * i, topPos + 87);

		if (rendered.slotsTargeted() == 0 && !rendered.renderSpecialIcon(pPoseStack, leftPos + 54, topPos + 88)) {
			Pair<ItemStack, Component> summary = rendered.getSummary();
			ItemStack icon = summary.getFirst();
			if (icon.isEmpty())
				icon = rendered.getSecondLineIcon();
			if (icon.isEmpty())
				AllGuiTextures.SCHEDULE_EDITOR_INACTIVE_SLOT.render(pPoseStack, leftPos + 53, topPos + 87);
			else
				GuiGameElement.of(icon)
					.at(leftPos + 54, topPos + 88)
					.render(pPoseStack);
		}

		pPoseStack.pushPose();
		pPoseStack.translate(0, getGuiTop() + 87, 0);
		editorSubWidgets.renderWidgetBG(getGuiLeft() + 77, pPoseStack);
		pPoseStack.popPose();
	}

	@Override
	public void removed() {
		super.removed();
		AllPackets.getChannel().sendToServer(new ScheduleEditPacket(schedule));
	}

	@Override
	public List<Rect2i> getExtraAreas() {
		return extraAreas;
	}

	public Font getFont() {
		return font;
	}

}
