package io.openems.edge.evse.chargepoint.bender;

import static io.openems.edge.common.channel.ChannelUtils.setValue;
import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE;
import static io.openems.edge.common.type.Phase.SinglePhase.L1;
import static io.openems.edge.common.type.Phase.SinglePhase.L2;
import static io.openems.edge.common.type.Phase.SinglePhase.L3;
import static io.openems.edge.meter.api.PhaseRotation.mapLongToPhaseRotatedActivePowerChannel;

import java.util.function.Supplier;

import org.osgi.service.event.Event;

import io.openems.common.types.SemanticVersion;
import io.openems.edge.bridge.modbus.api.AbstractOpenemsModbusComponent;
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.BitsWordElement;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.StringWordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
import io.openems.edge.common.channel.value.Value;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.evse.api.chargepoint.EvseChargePoint;
import io.openems.edge.meter.api.ElectricityMeter;

public abstract class AbstractEvseChargePointBender extends AbstractOpenemsModbusComponent
		implements ElectricityMeter, EvseChargePointBender {

	protected AbstractEvseChargePointBender(io.openems.edge.common.channel.ChannelId[] firstInitialChannelIds,
			io.openems.edge.common.channel.ChannelId[][] furtherInitialChannelIds) {
		super(firstInitialChannelIds, furtherInitialChannelIds);
	}

	@Override
	protected ModbusProtocol defineModbusProtocol() {
		final var phaseRotated = this.getPhaseRotation();
		return new ModbusProtocol(this,
				new FC3ReadRegistersTask(104, Priority.HIGH,
						m(EvseChargePointBender.ChannelId.OCPP_CP_STATUS, new UnsignedWordElement(104))),
				new FC3ReadRegistersTask(111, Priority.LOW, //
						m(new BitsWordElement(111, this)) //
								.bit(0, EvseChargePointBender.ChannelId.ERR_ACTUATOR_UNLOCKED_WHILE_CHARGING) //
								.bit(1, EvseChargePointBender.ChannelId.ERR_TILT_PREVENT_CHARGING_UNTIL_REBOOT) //
								.bit(2, EvseChargePointBender.ChannelId.ERR_PIC24) //
								.bit(3, EvseChargePointBender.ChannelId.ERR_USB_STICK_HANDLING) //
								.bit(4, EvseChargePointBender.ChannelId.ERR_INCORRECT_PHASE_INSTALLATION) //
								.bit(5, EvseChargePointBender.ChannelId.ERR_NO_POWER),
						m(new BitsWordElement(112, this)) //
								.bit(0, EvseChargePointBender.ChannelId.ERR_RCMB_TRIGGERED) //
								.bit(1, EvseChargePointBender.ChannelId.ERR_VEHICLE_STATE_E) //
								.bit(2, EvseChargePointBender.ChannelId.ERR_MODE3_DIODE_CHECK) //
								.bit(3, EvseChargePointBender.ChannelId.ERR_MCB_TYPE2_TRIGGERED) //
								.bit(4, EvseChargePointBender.ChannelId.ERR_MCB_SCHUKO_TRIGGERED) //
								.bit(5, EvseChargePointBender.ChannelId.ERR_RCD_TRIGGERED) //
								.bit(6, EvseChargePointBender.ChannelId.ERR_CONTACTOR_WELD) //
								.bit(7, EvseChargePointBender.ChannelId.ERR_BACKEND_DISCONNECTED) //
								.bit(8, EvseChargePointBender.ChannelId.ERR_ACTUATOR_LOCKING_FAILED) //
								.bit(9, EvseChargePointBender.ChannelId.ERR_ACTUATOR_LOCKING_WITHOUT_PLUG_FAILED) //
								.bit(10, EvseChargePointBender.ChannelId.ERR_ACTUATOR_STUCK) //
								.bit(11, EvseChargePointBender.ChannelId.ERR_ACTUATOR_DETECTION_FAILED) //
								.bit(12, EvseChargePointBender.ChannelId.ERR_FW_UPDATE_RUNNING) //
								.bit(13, EvseChargePointBender.ChannelId.ERR_TILT) //
								.bit(14, EvseChargePointBender.ChannelId.ERR_WRONG_CP_PR_WIRING) //
								.bit(15, EvseChargePointBender.ChannelId.ERR_TYPE2_OVERLOAD_THR_2)),
				new FC3ReadRegistersTask(122, Priority.HIGH,
						m(EvseChargePointBender.ChannelId.VEHICLE_STATE, new UnsignedWordElement(122))),
				new FC3ReadRegistersTask(141, Priority.LOW,
						m(EvseChargePointBender.ChannelId.RAW_DEVICE_ID, new UnsignedWordElement(141)),
						m(EvseChargePointBender.ChannelId.CHARGE_POINT_MODEL, new StringWordElement(142, 10))),
				new FC3ReadRegistersTask(153, Priority.LOW,
						m(EvseChargePointBender.ChannelId.SOFTWARE_VERSION_MAJOR, new UnsignedWordElement(153)),
						m(EvseChargePointBender.ChannelId.SOFTWARE_VERSION_MINOR, new UnsignedWordElement(154)),
						m(EvseChargePointBender.ChannelId.SOFTWARE_VERSION_PATCH, new UnsignedWordElement(155))),
				new FC3ReadRegistersTask(200, Priority.HIGH,
						m(ElectricityMeter.ChannelId.ACTIVE_PRODUCTION_ENERGY, new UnsignedDoublewordElement(200))),
				new FC3ReadRegistersTask(206, Priority.HIGH, //
						m(new UnsignedDoublewordElement(206)).build() //
								.onUpdateCallback(mapLongToPhaseRotatedActivePowerChannel(this, L1)), //
						m(new UnsignedDoublewordElement(208)).build() //
								.onUpdateCallback(mapLongToPhaseRotatedActivePowerChannel(this, L2)), //
						m(new UnsignedDoublewordElement(210)).build() //
								.onUpdateCallback(mapLongToPhaseRotatedActivePowerChannel(this, L3)), //
						m(phaseRotated.channelCurrentL1(), new UnsignedDoublewordElement(212)), //
						m(phaseRotated.channelCurrentL2(), new UnsignedDoublewordElement(214)), //
						m(phaseRotated.channelCurrentL3(), new UnsignedDoublewordElement(216)), //
						new DummyRegisterElement(218, 221),
						m(phaseRotated.channelVoltageL1(), new UnsignedDoublewordElement(222),
								ElementToChannelConverter.SCALE_FACTOR_3), //
						m(phaseRotated.channelVoltageL2(), new UnsignedDoublewordElement(224),
								ElementToChannelConverter.SCALE_FACTOR_3), //
						m(phaseRotated.channelVoltageL3(), new UnsignedDoublewordElement(226),
								ElementToChannelConverter.SCALE_FACTOR_3)));
	}

	/**
	 * Calculates if ev is connected from the {@link VehicleState}.
	 * 
	 * @return is the vehicle connected
	 */
	public boolean isEvConnected() {
		return this.getVehicleState().isEvConnected;
	}

	/**
	 * Calculates if the cp isReadyForCharging.
	 * 
	 * @return is the vehicle connected
	 */
	protected boolean isReadyForCharging() {
		var rawState = this.getOcppStatus();
		return rawState.isReadyForCharging;
	}

	protected void benderHandleEvent(Event event) {
		if (!this.isEnabled()) {
			return;
		}
		switch (event.getTopic()) {
		case TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> {
			var state = this.isReadyForCharging();
			setValue(this, EvseChargePoint.ChannelId.IS_READY_FOR_CHARGING, state);
		}
		}
	}

	/**
	 * Handles updates of standard register 141 (device id/model id).
	 *
	 * <p>
	 * Default implementation is a no-op; concrete charge points can override this
	 * and map the raw integer to their own channels.
	 *
	 * @param deviceId raw value from register 141
	 */
	protected void onDeviceIdUpdate(Value<Integer> deviceId) {
		// Intentionally left blank.
	}

	/**
	 * Helper Method that handles a Softwareversion check. Sets
	 * {@link EvseChargePointBender.ChannelId#FIRMWARE_OUTDATED}
	 *
	 */
	public void updateSoftwareVersionOutdated() {
		setValue(this, EvseChargePointBender.ChannelId.FIRMWARE_OUTDATED, isSoftwareOutdated(//
				this::getSoftwareVersionMajor, //
				this::getSoftwareVersionMinor, //
				this::getSoftwareVersionPatch, //
				this));
	}

	/**
	 * Checks if the Software Version provided by Suppliers is Outdated.
	 *
	 * @param valMajor {@link Supplier} for major Software Version
	 * @param valMinor {@link Supplier} for minor Software Version
	 * @param valPatch {@link Supplier} for patch Software Version
	 * @param cp       the {@link AbstractEvseChargePointBender}
	 * @return is software outdated
	 */
	public static boolean isSoftwareOutdated(Supplier<Value<Integer>> valMajor, Supplier<Value<Integer>> valMinor,
			Supplier<Value<Integer>> valPatch, AbstractEvseChargePointBender cp) {
		return SemanticVersion.fromNullable(valMajor.get().get(), valMinor.get().get(), valPatch.get().get())
				.map(version -> {
					setValue(cp, EvseChargePointBender.ChannelId.FIRMWARE_VERSION, version.toString());
					return !version.isAtLeast(cp.getOutdatedVersion());
				}) //
				.orElse(false);
	}

}
