package com.rera.mss.measurement.computingBase

import com.rera.mss.exceptions.MonitoredAreaWithoutPvAreaException
import com.rera.mss.exceptions.PvAreaWithZeroAreaException
import com.rera.mss.exceptions.PvAreaWithoutPvPanelException
import com.rera.mss.model.Instrument
import com.rera.mss.model.MonitoredArea
import com.rera.mss.service.IrradianceAndEnergy
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.lang.Long.min

@Component
class ComputerImpl : Computer {
    private val logger = LoggerFactory.getLogger(javaClass)

    override fun computeEnergyFromIrradiance(monitoredArea: MonitoredArea, timeIrradianceData: List<Pair<Long, Float>>, energyOffset: List<Float>): List<Float> {
        if (monitoredArea.monitoredAreaPvPanelAreas.isEmpty()) throw MonitoredAreaWithoutPvAreaException("Could not compute generated energy for monitored area 'id: ${monitoredArea.id}'. It does not contain any pv areas.", monitoredArea.id)

        val sumOfPanelAreasTimesEfficiency = monitoredArea.monitoredAreaPvPanelAreas.map { pvPanelArea ->
            val pvPanel = pvPanelArea.pvPanel
                    ?: throw PvAreaWithoutPvPanelException("Could not compute generated energy for monitored area 'id: ${monitoredArea.id}' and corresponding Pv area 'id: ${pvPanelArea.id}',  without pv panel supplied.", monitoredArea.id, pvPanelArea.id)
            val area = pvPanelArea.area
            if (area == 0F) throw PvAreaWithZeroAreaException("Could not compute generated energy for monitored area 'id: ${monitoredArea.id}' and corresponding Pv area 'id: ${pvPanelArea.id}' which has 0 m2 area.", monitoredArea.id, pvPanelArea.id)
            area * pvPanel.efficiency / 100 // for efficiency in %
        }.reduce { acc, fl -> acc + fl }

        var previousEnergyValue = 0F
        return timeIrradianceData.mapIndexed { index, pair ->
            if (index == 0) {
                return@mapIndexed 0F
            }
            val dt: Float = (pair.first - timeIrradianceData[index - 1].first).toFloat() / 1000 / 3600 // from ms to s to hours
            val y = pair.second + timeIrradianceData[index - 1].second
            val energyIncrement = (dt * y) / (2 * 1000) * sumOfPanelAreasTimesEfficiency + previousEnergyValue // in kWh
            previousEnergyValue = energyIncrement

            return@mapIndexed if (energyOffset.isEmpty()) energyIncrement else energyOffset[index] + energyIncrement
        }
    }

    override fun computeBoundsOfTimeInterval(irradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>, realEnergyTimeBase: List<Long>?): Pair<Long, Long>? {
        val timeIntervals = irradianceAndEnergy.map { data ->
            if (data.value.time.isEmpty()) return null
            data.value.time.first() to data.value.time.last()
        }.toMutableList().also { if (realEnergyTimeBase != null) it.add(realEnergyTimeBase.first() to realEnergyTimeBase.last()) }
        val (lowerBound, upperBound) = timeIntervals.maxBy { it.first }?.first to timeIntervals.minBy { it.second }?.second
        return if (lowerBound == null || upperBound == null) {
            logger.error("Could not find intersected time interval, boundaries are missing. Boundaries <${lowerBound}; ${upperBound}>")
            null
        } else if (compareValues(lowerBound, upperBound) >= 0) {
            logger.error("Lower bound must be smaller than upper bound. Boundaries <${lowerBound}; ${upperBound}>")
            null
        } else {
            lowerBound to upperBound
        }
    }

    override fun computeInstrumentsEnergyWithUnifiedTimeBase(irradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>, timeBaseStep: Long, bounds: Pair<Long, Long>): Map<Long, List<Pair<Long, Float>>>? {
        val (lowerBound, upperBound) = bounds
        val instrumentSumEnergy: MutableMap<Long, MutableList<Pair<Long, Float>>> = mutableMapOf()
        if (irradianceAndEnergy.size == 1)
            return singleInstrumentEnergy(irradianceAndEnergy, lowerBound, upperBound)
        val instrumentsEnergyInIntervals: MutableMap<Long, List<Pair<Instrument, List<Pair<Long, Float>>>>> = divideEnergyMeasurementsIntoTimeIntervals(lowerBound, upperBound, timeBaseStep, irradianceAndEnergy)


        irradianceAndEnergy.keys.forEach { instrumentSumEnergy[it.id] = mutableListOf(lowerBound to 0F) }

        instrumentsEnergyInIntervals.forEach { interval ->
            val result = interval.value.map { ins ->
                val numberOfEnergyMeasurements = ins.second.size
                if (numberOfEnergyMeasurements == 0) {
                    return@map ins.first to null
                } else {
                    return@map computeAverageEnergyInTimeInterval(ins)
                }
            }
            val (instruments, values) = result.unzip()
            if (everyInstrumentContainsAtLeastOneEnergyMeasurementInTimeInterval(values)) {
                var previousInstrumentEnergy = 0F
                instruments.forEachIndexed { index, instrument ->
                    if (index == 0) {
                        previousInstrumentEnergy = 0F
                    }
                    instrumentSumEnergy[instrument.id].let { mapEntry ->
                        if (mapEntry == null) throw NoSuchElementException("Did not find instrument with id: ${instrument.id} in instrumentSumEnergy map")
                        val computedInstrumentEnergy = values[index]
                                ?: throw NoSuchElementException("Instrument with id: ${instrument.id} contained null energy.")

                        val computedEnergyCorrected = computedInstrumentEnergy + previousInstrumentEnergy
                        mapEntry.add(interval.key to computedEnergyCorrected.also { previousInstrumentEnergy = it })
                    }
                }
            }
        }
        return instrumentSumEnergy
    }

    override fun computePerformanceRatio(theoreticalEnergy: Map<Long, List<Pair<Long, Float>>>, realEnergy: List<Pair<Long, Float>>): List<Pair<Long, Float>> {
        val sumTheoreticalEnergy = theoreticalEnergy.values.last()
        return realEnergy.mapNotNull { re ->
            val x = re.first
            val (x1, y1) = sumTheoreticalEnergy.last { it.first <= x }
            val (x2, y2) = sumTheoreticalEnergy.find { it.first >= x } ?: (x1 to y1)
            val y = interpolateValueOfTheoreticalEnergy(x1, x2, y1, y2, x)
            return@mapNotNull if (y != 0F) {
                val pr = re.second / y * 100
                x to pr
            } else null
        }
    }

    private fun interpolateValueOfTheoreticalEnergy(x1: Long, x2: Long, y1: Float, y2: Float, x: Long): Float {
        val y = if (x1 == x2) {
            y1
        } else {
            y1 + (y2 - y1) * (x - x1) / (x2 - x1)
        }
        return y
    }

    private fun singleInstrumentEnergy(instrumentIrradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>, lowerBound: Long, upperBound: Long): Map<Long, List<Pair<Long, Float>>>? {
        val singleInstrumentEnergy: MutableMap<Long, List<Pair<Long, Float>>> = mutableMapOf()
        val dataInTimeRange = instrumentIrradianceAndEnergy.flatMap { data ->
            data.value.time.filter { it in lowerBound..upperBound }.zip(getInstrumentEnergyFromMonitoredAreasEnergies(data)
                    ?: return null)
        }
        val firstValue = dataInTimeRange.first().second
        singleInstrumentEnergy[instrumentIrradianceAndEnergy.keys.first().id] = dataInTimeRange.map { it.first to (it.second - firstValue) }
        return singleInstrumentEnergy
    }


    private fun everyInstrumentContainsAtLeastOneEnergyMeasurementInTimeInterval(result: List<Float?>): Boolean =
            result.all { r -> r != null }

    private fun computeAverageEnergyInTimeInterval(it: Pair<Instrument, List<Pair<Long, Float>>>): Pair<Instrument, Float> {
        val sumOfInverterEnergyInInterval = it.second.map { pair -> pair.first * pair.second }.reduce { acc, fl -> acc + fl }
        val sumOfEnergyIntervals = it.second.map { pair -> pair.first }.reduce { acc, l -> acc + l }
        return it.first to sumOfInverterEnergyInInterval / sumOfEnergyIntervals
    }

    private fun divideEnergyMeasurementsIntoTimeIntervals(lowerBound: Long, upperBound: Long, step: Long, irradianceAndEnergy: Map<Instrument, IrradianceAndEnergy>): MutableMap<Long, List<Pair<Instrument, List<Pair<Long, Float>>>>> {
        val instrumentsEnergyInIntervals: MutableMap<Long, List<Pair<Instrument, List<Pair<Long, Float>>>>> = mutableMapOf()
        val wholeTimeRange = (lowerBound + step / 2)..(upperBound + step / 2) step (step)
        for (t in wholeTimeRange) {
            val energyInTimeRange = irradianceAndEnergy.map { data ->
                val intervalTimeRange = computeIntervalTimeRange(t, wholeTimeRange, step, upperBound)
                val energyInRange = findEnergiesInTimeInterval(data, intervalTimeRange)
                data.key to energyInRange
            }
            instrumentsEnergyInIntervals[min(t, upperBound)] = energyInTimeRange.map { it }
        }
        return instrumentsEnergyInIntervals
    }

    private fun computeIntervalTimeRange(t: Long, wholeTimeRange: LongProgression, step: Long, upperBound: Long): LongRange {
        return if (t != wholeTimeRange.last) {
            (t - step / 2) until (t + step / 2)
        } else {
            (t - step / 2)..upperBound
        }
    }

    private fun findEnergiesInTimeInterval(data: Map.Entry<Instrument, IrradianceAndEnergy>, intervalTimeRange: LongRange): List<Pair<Long, Float>> {
        val instrumentEnergy = getInstrumentEnergyFromMonitoredAreasEnergies(data)
                ?: return emptyList() // TODO returning empty list is it all right?
        val filteredTimeEnergyValues = data.value.time.filter { time ->
            time in intervalTimeRange // TODO not needed to iterate throught boundaries would be enough
        }.map { filteredMeasurementTime ->
            val energyAtIndex = instrumentEnergy[data.value.time.indexOf(filteredMeasurementTime)]
            filteredMeasurementTime to energyAtIndex
        }.toMutableList().also {
            handleEnergyDataBetweenLastTimestampAndNextEnergyValue(data, it, intervalTimeRange, instrumentEnergy)
        }

        return mapEnergiesToTimeIntervals(filteredTimeEnergyValues, intervalTimeRange)
    }

    private fun handleEnergyDataBetweenLastTimestampAndNextEnergyValue(data: Map.Entry<Instrument, IrradianceAndEnergy>, it: MutableList<Pair<Long, Float>>, intervalTimeRange: LongRange, instrumentEnergy: List<Float>) {
        val time = data.value.time
        val timestampOfLastComputedEnergy = if (it.size == 0) {
            time.findLast { t -> t < intervalTimeRange.first }
        } else {
            it.last().first
        }
        val lastIndex = time.indexOf(timestampOfLastComputedEnergy)

        try {
            it.add(time[lastIndex + 1] to instrumentEnergy[lastIndex + 1])
        } catch (e: IndexOutOfBoundsException) {
        }
    }

    private fun mapEnergiesToTimeIntervals(filteredTimeEnergyValues: List<Pair<Long, Float>>, intervalTimeRange: LongRange): List<Pair<Long, Float>> {
        val size = filteredTimeEnergyValues.size
        return filteredTimeEnergyValues.mapIndexed { index, pair ->
            return@mapIndexed if (index == 0) {
                (pair.first - intervalTimeRange.first + 1) to pair.second
            } else if (index == size - 1) {
                (intervalTimeRange.last - filteredTimeEnergyValues[index - 1].first) to filteredTimeEnergyValues[index].second
            } else {
                (pair.first - filteredTimeEnergyValues[index - 1].first) to pair.second
            }
        }
    }

    private fun getInstrumentEnergyFromMonitoredAreasEnergies(data: Map.Entry<Instrument, IrradianceAndEnergy>): List<Float>? {
        return if (data.value.monitoredAreaEnergies.isEmpty()) null else data.value.monitoredAreaEnergies.values.last()
    }
}
