package io.openems.edge.simulator.ess.singlephase.reacting;

import static io.openems.edge.common.channel.ChannelUtils.setValue;

import java.io.IOException;

import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.osgi.service.event.propertytypes.EventTopics;
import org.osgi.service.metatype.annotations.Designate;

import io.openems.common.channel.AccessMode;
import io.openems.edge.common.component.AbstractOpenemsComponent;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.event.EdgeEventConstants;
import io.openems.edge.common.modbusslave.ModbusSlave;
import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
import io.openems.edge.common.modbusslave.ModbusSlaveTable;
import io.openems.edge.common.type.Phase.SinglePhase;
import io.openems.edge.ess.api.AsymmetricEss;
import io.openems.edge.ess.api.ManagedAsymmetricEss;
import io.openems.edge.ess.api.ManagedSinglePhaseEss;
import io.openems.edge.ess.api.ManagedSymmetricEss;
import io.openems.edge.ess.api.SinglePhaseEss;
import io.openems.edge.ess.api.SymmetricEss;
import io.openems.edge.ess.power.api.Power;
import io.openems.edge.simulator.datasource.api.SimulatorDatasource;
import io.openems.edge.timedata.api.Timedata;
import io.openems.edge.timedata.api.TimedataProvider;
import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower;

@Designate(ocd = Config.class, factory = true)
@Component(//
		name = "Simulator.EssSinglePhase.Reacting", //
		immediate = true, //
		configurationPolicy = ConfigurationPolicy.REQUIRE //
)
@EventTopics({ //
		EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE //
})
public class SimulatorEssSinglePhaseReactingImpl extends AbstractOpenemsComponent implements
		SimulatorEssSinglePhaseReacting, ManagedSinglePhaseEss, SinglePhaseEss, ManagedAsymmetricEss, AsymmetricEss,
		ManagedSymmetricEss, SymmetricEss, OpenemsComponent, TimedataProvider, EventHandler, ModbusSlave {

	private final CalculateEnergyFromPower calculateChargeEnergy = new CalculateEnergyFromPower(this,
			SymmetricEss.ChannelId.ACTIVE_CHARGE_ENERGY);
	private final CalculateEnergyFromPower calculateDischargeEnergy = new CalculateEnergyFromPower(this,
			SymmetricEss.ChannelId.ACTIVE_DISCHARGE_ENERGY);

	@Reference
	private Power power;

	@Reference(policy = ReferencePolicy.STATIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.MANDATORY)
	private SimulatorDatasource datasource;

	@Reference
	private ConfigurationAdmin cm;

	@Reference(policy = ReferencePolicy.DYNAMIC, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.OPTIONAL)
	private volatile Timedata timedata = null;

	/** Current state of charge. */
	private float soc = 0;
	private Config config;
	private SinglePhase phase;

	public SimulatorEssSinglePhaseReactingImpl() {
		super(//
				OpenemsComponent.ChannelId.values(), //
				SymmetricEss.ChannelId.values(), //
				ManagedSymmetricEss.ChannelId.values(), //
				AsymmetricEss.ChannelId.values(), //
				ManagedAsymmetricEss.ChannelId.values(), //
				SinglePhaseEss.ChannelId.values(), //
				ManagedSinglePhaseEss.ChannelId.values(), //
				SimulatorEssSinglePhaseReacting.ChannelId.values() //
		);
	}

	@Activate
	private void activate(ComponentContext context, Config config) throws IOException {
		super.activate(context, config.id(), config.alias(), config.enabled());
		this.phase = config.phase();
		SinglePhaseEss.initializeCopyPhaseChannel(this, this.phase);

		// update filter for 'datasource'
		if (OpenemsComponent.updateReferenceFilter(this.cm, this.servicePid(), "datasource", config.datasource_id())) {
			return;
		}

		this.config = config;
		this.soc = config.initialSoc();
		this._setSoc(config.initialSoc());
		this._setMaxApparentPower(config.maxApparentPower());
		setValue(this, ManagedSymmetricEss.ChannelId.ALLOWED_CHARGE_POWER,
				calculateAllowedChargePower(config.initialSoc(), config.maxApparentPower()) * -1);
		setValue(this, ManagedSymmetricEss.ChannelId.ALLOWED_DISCHARGE_POWER,
				calculateAllowedDischargePower(config.initialSoc(), config.maxApparentPower()));
		setValue(this, SymmetricEss.ChannelId.GRID_MODE, config.gridMode());
	}

	@Override
	@Deactivate
	protected void deactivate() {
		super.deactivate();
	}

	@Override
	public void handleEvent(Event event) {
		if (!this.isEnabled()) {
			return;
		}
		switch (event.getTopic()) {
		case EdgeEventConstants.TOPIC_CYCLE_AFTER_PROCESS_IMAGE:
			this.calculateEnergy();
			break;
		}
	}

	@Override
	public String debugLog() {
		return "SoC:" + this.getSoc().asString() //
				+ "|L:" + this.getActivePower().asString() //
				+ "|" + this.getGridModeChannel().value().asOptionString();
	}

	@Override
	public Power getPower() {
		return this.power;
	}

	@Override
	public void applyPower(int activePower, int reactivePower) {
		/*
		 * calculate State of charge
		 */
		var watthours = (float) activePower * this.datasource.getTimeDelta() / 3600;
		var socChange = watthours / this.config.capacity();
		this.soc -= socChange;
		if (this.soc > 100) {
			this.soc = 100;
		} else if (this.soc < 0) {
			this.soc = 0;
		}
		this._setSoc(Math.round(this.soc));
		/*
		 * Apply Active/Reactive power to simulated channels
		 */
		if (this.soc == 0 && activePower > 0) {
			activePower = 0;
		}
		if (this.soc == 100 && activePower < 0) {
			activePower = 0;
		}
		switch (this.getPhase()) {
		case L1:
			this._setActivePowerL1(activePower);
			break;
		case L2:
			this._setActivePowerL2(activePower);
			break;
		case L3:
			this._setActivePowerL3(activePower);
			break;
		}

		if (this.soc == 0 && reactivePower > 0) {
			reactivePower = 0;
		}
		if (this.soc == 100 && reactivePower < 0) {
			reactivePower = 0;
		}
		switch (this.getPhase()) {
		case L1:
			this._setReactivePowerL1(activePower);
			break;
		case L2:
			this._setReactivePowerL2(activePower);
			break;
		case L3:
			this._setReactivePowerL3(activePower);
			break;
		}

		/*
		 * Set AllowedCharge / Discharge based on SoC with derating zone: - Charge: full
		 * below 95%, linear derating 95–100%, hard 0 at 100% - Discharge: full above
		 * 5%, linear derating 0–5%, hard 0 at 0%
		 */
		setValue(this, ManagedSymmetricEss.ChannelId.ALLOWED_CHARGE_POWER,
				calculateAllowedChargePower(this.soc, this.config.maxApparentPower()) * -1);
		setValue(this, ManagedSymmetricEss.ChannelId.ALLOWED_DISCHARGE_POWER,
				calculateAllowedDischargePower(this.soc, this.config.maxApparentPower()));
	}

	@Override
	public int getPowerPrecision() {
		return 1;
	}

	@Override
	public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
		return new ModbusSlaveTable(//
				OpenemsComponent.getModbusSlaveNatureTable(accessMode), //
				SymmetricEss.getModbusSlaveNatureTable(accessMode), //
				ManagedSymmetricEss.getModbusSlaveNatureTable(accessMode), //
				ModbusSlaveNatureTable.of(SimulatorEssSinglePhaseReactingImpl.class, accessMode, 300) //
						.build());
	}

	@Override
	public SinglePhase getPhase() {
		return this.phase;
	}

	/**
	 * Calculate the Energy values from ActivePower.
	 */
	private void calculateEnergy() {
		// Calculate Energy
		var activePower = this.getActivePower().get();
		if (activePower == null) {
			// Not available
			this.calculateChargeEnergy.update(null);
			this.calculateDischargeEnergy.update(null);
		} else if (activePower > 0) {
			// Buy-From-Grid
			this.calculateChargeEnergy.update(0);
			this.calculateDischargeEnergy.update(activePower);
		} else {
			// Sell-To-Grid
			this.calculateChargeEnergy.update(activePower * -1);
			this.calculateDischargeEnergy.update(0);
		}
	}

	@Override
	public Timedata getTimedata() {
		return this.timedata;
	}

	/**
	 * Calculates allowed charge power with a linear derating from 95% to 100% SoC.
	 *
	 * @param soc            the current SoC [%]
	 * @param maxChargePower the maximum charge power [W], positive value
	 * @return allowed charge power as a positive value [W]
	 */
	private static int calculateAllowedChargePower(float soc, int maxChargePower) {
		if (soc >= 100) {
			return 0;
		}
		if (soc > 95) {
			return Math.round(maxChargePower * (100 - soc) / 5f);
		}
		return maxChargePower;
	}

	/**
	 * Calculates allowed discharge power with a linear derating from 5% to 0% SoC.
	 *
	 * @param soc               the current SoC [%]
	 * @param maxDischargePower the maximum discharge power [W], positive value
	 * @return allowed discharge power as a positive value [W]
	 */
	private static int calculateAllowedDischargePower(float soc, int maxDischargePower) {
		if (soc <= 0) {
			return 0;
		}
		if (soc < 5) {
			return Math.round(maxDischargePower * soc / 5f);
		}
		return maxDischargePower;
	}
}
