----------------------------------------------------------------------------------------------------
-- SeasonsWeather
----------------------------------------------------------------------------------------------------
-- Purpose:  Weather system for Seasons
--
-- Copyright (c) Realismus Modding, 2018
----------------------------------------------------------------------------------------------------

SeasonsWeather = {}

local SeasonsWeather_mt = Class(SeasonsWeather)

SeasonsWeather.FORECAST_SUN = 0
SeasonsWeather.FORECAST_PARTLY_CLOUDY = 1
SeasonsWeather.FORECAST_RAIN_SHOWERS = 2
SeasonsWeather.FORECAST_SNOW_SHOWERS = 3
SeasonsWeather.FORECAST_SLEET = 4
SeasonsWeather.FORECAST_CLOUDY = 5
SeasonsWeather.FORECAST_RAIN = 6
SeasonsWeather.FORECAST_SNOW = 7
SeasonsWeather.FORECAST_FOG = 8
SeasonsWeather.FORECAST_THUNDER = 9
SeasonsWeather.FORECAST_HAIL = 10

SeasonsWeather.WEATHERTYPE_SUN = 0
SeasonsWeather.WEATHERTYPE_CLOUDY = 1
SeasonsWeather.WEATHERTYPE_RAIN = 2
SeasonsWeather.WEATHERTYPE_SNOW = 3
SeasonsWeather.WEATHERTYPE_FOG = 4
SeasonsWeather.WEATHERTYPE_HAIL = 5
SeasonsWeather.WEATHERTYPE_THUNDER = 6

SeasonsWeather.WINDTYPE_CALM = 0
SeasonsWeather.WINDTYPE_GENTLE_BREEZE = 1
SeasonsWeather.WINDTYPE_STRONG_BREEZE = 2
SeasonsWeather.WINDTYPE_GALE = 3

SeasonsWeather.FOGSCALE = 10

SeasonsWeather.PRECIPITATION_TYPES = {nil, nil, 'rain', 'snow', nil, nil, 'rain'}
SeasonsWeather.MIN_CIRRUS_SCALE = {0, 0.1, 0.2, 0.2, 0, 0, 0.5}

---Change the HUD before it is created by the mission
function SeasonsWeather.onMissionWillLoad()
    SeasonsModUtil.overwrittenFunction(GameInfoDisplay, "getWeatherUVs", SeasonsWeather.inj_gameInfoDisplay_getWeatherUVs)
end

function SeasonsWeather:new(mission, environment, snowHandler, messageCenter, modDirectory, server)
    local self = setmetatable({}, SeasonsWeather_mt)

    self.mission = mission
    self.environment = environment
    self.snowHandler = snowHandler
    self.messageCenter = messageCenter
    self.isClient = mission:getIsClient()
    self.isServer = mission:getIsServer()
    self.server = server

    self.data = SeasonsWeatherData:new()
    self.model = SeasonsWeatherModel:new(self.data, self.environment, mission)
    self.forecast = SeasonsWeatherForecast:new(self.data, self.model, self.environment, server)
    self.handler = SeasonsWeatherHandler:new(mission, environment, self, messageCenter, modDirectory)

    self.handler:setSoilWetnessFunction(function ()
        return self:getSoilWetness()
    end)

    -- Replace the Weather system with our own
    SeasonsModUtil.overwrittenFunction(Environment, "new", SeasonsWeather.inj_environment_new)
    SeasonsModUtil.overwrittenFunction(GameInfoDisplay, "getWeatherStates", SeasonsWeather.inj_gameInfoDisplay_getWeatherStates)
    SeasonsModUtil.overwrittenFunction(WindTurbinePlaceable, "updateHeadRotation", SeasonsWeather.inj_windTurbinePlaceable_updateHeadRotation)
    SeasonsModUtil.overwrittenFunction(WindTurbinePlaceable, "update", SeasonsWeather.inj_windTurbinePlaceable_update)
    SeasonsModUtil.overwrittenFunction(WindTurbinePlaceable, "hourChanged", SeasonsWeather.inj_windTurbinePlaceable_hourChanged)

    addConsoleCommand("rmWeatherAddEvent", "Add a basic event", "consoleCommandAddEvent", self)
    addConsoleCommand("rmWeatherSetWindVelocity", "Set wind velocity", "consoleCommandSetWindVelocity", self)

    return self
end

function SeasonsWeather:delete()
    self.data:delete()
    self.model:delete()
    self.forecast:delete()
    -- Do not delete the handler, already deleted by vanilla environment

    self.messageCenter:unsubscribeAll(self)

    removeConsoleCommand("rmWeatherAddEvent")
    removeConsoleCommand("rmWeatherSetWindVelocity")
end

function SeasonsWeather:load()
    self.data:load()
    self.forecast:load()
    self.handler:load()

    self.soilTemp = self.data.startValues.soilTemp
    self.soilTempMax = self.soilTemp
    self.snowDepth = self.data.startValues.snowDepth

    self.cropMoistureContent = 15.0
    self.soilWaterContent =  0.1
    self.averageSoilWaterContent = self.soilWaterContent
    self.lowAirTemp = 0
    self.rotDryFactor = 0
    self.moistureEnabled = true
    self.meltedSnow = 0 -- won't change on clients
    self.airWasFrozen = false

    self.lastTemperatureUpdate = 0

    self.messageCenter:subscribe(SeasonsMessageType.HOUR_CHANGED_FIX, self.onHourChanged, self)
    self.messageCenter:subscribe(MessageType.DAY_CHANGED, self.onDayChanged, self)
    self.messageCenter:subscribe(SeasonsMessageType.PERIOD_CHANGED, self.onPeriodChanged, self)
    self.messageCenter:subscribe(SeasonsMessageType.SEASON_LENGTH_CHANGED, self.onSeasonLengthChanged, self)
end

function SeasonsWeather:onItemsLoaded()
    self.forecast:onItemsLoaded()

    if #self.handler.events <= 1 and self.isServer then
        self:build()
    end

    self:updateWeatherConditionals()

    self.cropDryingForecast = self:cropDryingSimulation(self.cropMoistureContent, self.environment.currentDay, self.mission.environment.currentHour)
end

function SeasonsWeather:loadFromSavegame(xmlFile)
    self.soilTemp = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.soilTemp"), self.soilTemp)
    self.soilTempMax = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.soilTempMax"), self.soilTempMax)
    self.cropMoistureContent = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.cropMoistureContent"), self.cropMoistureContent)
    self.soilWaterContent = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.soilWaterContent"), self.soilWaterContent)
    self.averageSoilWaterContent = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.averageSoilWaterContent"), self.averageSoilWaterContent)
    self.lowAirTemp = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.lowAirTemp"), self.lowAirTemp)
    self.snowDepth = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.snowDepth"), self.snowDepth)
    self.rotDryFactor = Utils.getNoNil(getXMLFloat(xmlFile, "seasons.weather.rotDryFactor"), self.rotDryFactor)
    self.moistureEnabled = Utils.getNoNil(getXMLBool(xmlFile, "seasons.weather.moistureEnabled"), self.moistureEnabled)

    self.handler:loadFromSavegame(xmlFile)
    self.forecast:loadFromSavegame(xmlFile)
end

function SeasonsWeather:saveToSavegame(xmlFile)
    setXMLFloat(xmlFile, "seasons.weather.soilTemp", self.soilTemp)
    setXMLFloat(xmlFile, "seasons.weather.soilTempMax", self.soilTempMax)
    setXMLFloat(xmlFile, "seasons.weather.cropMoistureContent", self.cropMoistureContent)
    setXMLFloat(xmlFile, "seasons.weather.soilWaterContent", self.soilWaterContent)
    setXMLFloat(xmlFile, "seasons.weather.lowAirTemp", self.lowAirTemp)
    setXMLFloat(xmlFile, "seasons.weather.snowDepth", self.snowDepth)
    setXMLFloat(xmlFile, "seasons.weather.rotDryFactor", self.rotDryFactor)
    setXMLFloat(xmlFile, "seasons.weather.averageSoilWaterContent", self.averageSoilWaterContent)
    setXMLBool(xmlFile, "seasons.weather.moistureEnabled", self.moistureEnabled)

    self.handler:saveToSavegame(xmlFile)
    self.forecast:saveToSavegame(xmlFile)
end

function SeasonsWeather:setDataPaths(paths)
    self.data:setDataPaths(paths)
end

function SeasonsWeather:writeStream(streamId, connection)
    streamWriteFloat32(streamId, self.soilTemp)
    streamWriteFloat32(streamId, self.cropMoistureContent)
    streamWriteFloat32(streamId, self.soilWaterContent)
    streamWriteBool(streamId, self.moistureEnabled)
end

function SeasonsWeather:readStream(streamId, connection)
    self.soilTemp = streamReadFloat32(streamId)
    self.yesterdaySoilTemp = self.soilTemp
    self.cropMoistureContent = streamReadFloat32(streamId)
    self.soilWaterContent = streamReadFloat32(streamId)
    self.moistureEnabled = streamReadBool(streamId)
end

----------------------
-- Events
----------------------

function SeasonsWeather:update(dt)
    -- Do not run weather handler update: already done by basegame

    -- Update the air temperature in the weather handler every 5 seconds. Changes downfall
    if self.lastTemperatureUpdate < g_time - 5000 then
        self.lastTemperatureUpdate = g_time
        self.handler:setAirTemperature(self:getCurrentAirTemperature())
    end
end

---Once game loaded, call the change handlers so everything has the correct state
function SeasonsWeather:onGameLoaded()
    self.messageCenter:publish(SeasonsMessageType.FREEZING_CHANGED)
    self.airWasFrozen = self:getIsFreezing()

    if self.mission:getIsServer() then
        if g_seasons.isNewSavegame and self.snowDepth > 0 then
            self.snowHandler:setSnowHeight(self.snowDepth)
        end
    end
end

function SeasonsWeather:onHourChanged()
    local deltaMoisture

    if self.isServer then
        self.meltedSnow = 0
        self:updateSnowDepth()

        local windSpeed = self.handler:getCurrentWindSpeed()
        local currentForecastItem = self.forecast:getCurrentItem()
        local cloudCoverage = self.mission.environment.weather.cloudUpdater.currentCloudCoverage
        local dropScale = self.mission.environment.weather.downfallUpdater.currentDropScale
        local fogScale = self.mission.environment.weather.fogUpdater.currentMieScale
        local timeSinceLastRain = self.mission.environment.weather.timeSinceLastRain
        local dayTime = self.mission.environment.dayTime / 60 / 60 / 1000 --current time in hours
        local julianDay = self.environment.daylight:getCurrentJulianDay()

        self.cropMoistureContent, deltaMoisture = self.model:updateCropMoistureContent(self.cropMoistureContent, julianDay, dayTime, self.latestCurrentTemp, currentForecastItem.lowTemp, windSpeed, cloudCoverage, dropScale, fogScale, timeSinceLastRain)
        self.rotDryFactor = self.rotDryFactor + deltaMoisture
        -- log(self.cropMoistureContent, deltaMoisture, self.rotDryFactor)

        if not self:isGroundFrozen() then
            self.soilWaterContent = self.model:calculateSoilWaterContent(self.soilWaterContent, self:getCurrentAirTemperature(), currentForecastItem.lowTemp, self.meltedSnow, self.snowDepth, self:isGroundFrozen())
            self:updateAverageSoilWaterContent()
        end
    end

    if self.isServer then
        g_server:broadcastEvent(SeasonsWeatherHourlyEvent:new(self.cropMoistureContent, self.snowDepth, self.soilWaterContent))
    end

    -- Update objects when freezing in air changes
    local isFreezing = self:getIsFreezing()
    if (self.airWasFrozen and not isFreezing) or (not self.airWasFrozen and isFreezing) then
        self.messageCenter:publish(SeasonsMessageType.FREEZING_CHANGED)
    end
    self.airWasFrozen = isFreezing

    local x, z, _, _ = self.handler.windUpdater:getCurrentValues()
    local angle = math.atan2(x, z)
    -- WindTurbinePlaceable:updateHeadRotation()
    --("Wind velocity ",self:getWindVelocity(), " | Wind direction ", angle)

    --todo: needs to be initiated after load
    self.cropDryingForecast = self:cropDryingSimulation(self.cropMoistureContent, self.environment.currentDay, self.mission.environment.currentHour)
end

function SeasonsWeather:onDayChanged()
    self:updateLowAirTemp()

    if self.isServer then
        self.forecast:generateNextDay()

        self.yesterdaySoilTemp = self.soilTemp
        local currentForecastItem = self.forecast:getCurrentItem()
        self.soilTemp, self.soilTempMax = self.model:calculateSoilTemp(self.soilTemp, self.soilTempMax, currentForecastItem.lowTemp, currentForecastItem.highTemp, self.snowDepth, self.environment.daysPerSeason, false)

        self:generateWeatherIfNeeded()

        -- Send new values so we don't deviate on clients
        g_server:broadcastEvent(SeasonsWeatherDailyEvent:new(self.yesterdaySoilTemp, self.soilTemp, self.soilTempMax))

        self:onTemperaturesChanged()
    end
end

---Temperatures changed (soil temp)
function SeasonsWeather:onTemperaturesChanged()
    -- Update objects when freezing in ground changes
    if (self.yesterdaySoilTemp <= 0 and self.soilTemp > 0) or (self.yesterdaySoilTemp > 0 and self.soilTemp <= 0) then
        self.messageCenter:publish(SeasonsMessageType.FREEZING_CHANGED)
    end

    self:updateWeatherConditionals()
end

function SeasonsWeather:onPeriodChanged()
    -- weather runs before growth?
    self.frostIndex = 4
end

---Regenerate the weather when the season length changed
function SeasonsWeather:onSeasonLengthChanged()
    if self.isServer then
        self.forecast:rebuild()
        self:rebuild()
        self.server:broadcastEvent(WeatherAddObjectEvent:new(self.handler.events, true))
    end
end

---Send any initial state. Called once a client joins
function SeasonsWeather:onClientJoined(connection)
    self.forecast:onClientJoined(connection)
end

---Data received from server
function SeasonsWeather:onHourlyDataReceived(cropMoistureContent, snowDepth, soilWaterContent)
    if not self.isServer then
        self.cropMoistureContent = cropMoistureContent
        self.snowDepth = snowDepth
        self.soilWaterContent = soilWaterContent
    end
end

---Data received from server to prevent deviation
function SeasonsWeather:onDailyDataReceived(yesterdaySoilTemp, soilTemp, soilTempMax)
    if not self.isServer then
        self.yesterdaySoilTemp = yesterdaySoilTemp
        self.soilTemp = soilTemp
        self.soilTempMax = soilTempMax

        self:onTemperaturesChanged()
    end
end

----------------------
-- Generating weather
----------------------

---Build actual weather and add it to the handler.
function SeasonsWeather:build()
    local day = self.environment.currentDay
    local lastDay = day + 3
    local event, prevEvent

    while day < lastDay do
        if prevEvent == nil then
            prevEvent = self:getFirstEvent()
            event = self:getWeatherEvent(self.forecast.items[day - lastDay + 4], prevEvent)
        else
            prevEvent = event
            event = self:getWeatherEvent(self.forecast.items[day - lastDay + 4], prevEvent)
        end

        self.handler:appendEvent(event)

        -- forecast days are in Seasons-time, while weather event days are in basegame-time
        day = event.endDay + self.environment.currentDayOffset
    end
end

---Rebuild weather from scratch
function SeasonsWeather:rebuild()
    self.handler:clearEvents()
    self:build()
end

function SeasonsWeather:generateWeatherIfNeeded()
    local prevEvent = self.handler.events[#self.handler.events]
    local day = prevEvent.endDay + self.environment.currentDayOffset
    local lastDay = self.environment.currentDay + 3

    local addedEvents = {}

    while day < lastDay do
        local event = self:getWeatherEvent(self.forecast.items[day - lastDay + 4], prevEvent)

        self.handler:appendEvent(event)

        table.insert(addedEvents, event)

        prevEvent = event
        day = event.endDay + self.environment.currentDayOffset
    end

    if #addedEvents > 0 then
        self.server:broadcastEvent(WeatherAddObjectEvent:new(addedEvents, false))
    end
end

--- function to keep track of snow accumulation
--- snowDepth in meters
function SeasonsWeather:updateSnowDepth()
    local seasonLengthFactor = math.max(9 / self.environment.daysPerSeason, 1.0)
    local currentTemp = self:getCurrentAirTemperature()
    local effectiveMeltTemp = math.max(currentTemp, 0) + math.max(self.soilTemp, 0)
    local windMeltFactor = 1 + math.max(self:getWindVelocity() - 5, 0) / 25
    local dropScale = self.handler.downfallUpdater.currentDropScale

    local julianDay = self.environment.daylight:getCurrentJulianDay()
    local dayTime = self.mission.environment.dayTime / 60 / 60 / 1000 --current time in hours
    local cloudCoverage = self.handler.cloudUpdater.currentCloudCoverage
    local solarRad = self.environment.daylight:getCurrentSolarRadiation(julianDay, dayTime, cloudCoverage) / 5

    local period = self.environment:periodAtDay(self.environment.currentDay, self.environment.daysPerSeason)
    local rainfall = self.data.rainfall[period]
    local rainProb = self.data.rainProbability[period]

    -- calculating snow melt as a function of radiation
    local snowMelt = math.max(0.001 * effectiveMeltTemp ) * (1 + (1 + solarRad) * seasonLengthFactor * windMeltFactor)

    -- melting snow
    if not self:getIsFreezing() then
        -- assume snow melts up to 50% faster if it rains
        self.meltedSnow = snowMelt * (1 + 0.5 * dropScale)
        self.snowDepth = self.snowDepth - self.meltedSnow

    -- accumulating snow
    elseif self:getIsFreezing() and dropScale > 0 then
        -- Initial value of 10 mm/hr accumulation rate. Higher rate when there is little snow to get the visual effect
        if self.snowDepth < 0 then
            self.snowDepth = 0
        elseif self.snowDepth > 0.06 then
            self.snowDepth = self.snowDepth + rainfall / 9 / rainProb / 1000 * seasonLengthFactor * dropScale
        else
            self.snowDepth = self.snowDepth + 31 / 1000
        end
    end

    -- We have seen games with extreme snow that never melts in time. To prevent this,
    -- we limit internal height to 60cm (visual height is limited to 48cm already)
    self.snowDepth = math.min(self.snowDepth, 0.60)

    self.snowHandler:setSnowHeight(self.snowDepth)
end

-- Weather event generator
function SeasonsWeather:getWeatherEvent(dayForecast, prevEvent)
    local event = SeasonsWeatherEvent:new()

    event.startDay = dayForecast.day - self.environment.currentDayOffset -- days in handler need to be basegame days so they are always continious
    event.endDay = event.startDay
    event.startTime = prevEvent.endTime
    event.duration = SeasonsMathUtil.triDist(2 , 4 , 6)
    event.endTime = event.startTime + event.duration
    event.temperatureIndication = self.forecast:diurnalTemp((event.startTime + event.endTime) / 2)
    event.windVelocity = dayForecast.windSpeed + (prevEvent.windVelocity - dayForecast.windSpeed) / 2 + SeasonsMathUtil.random(-0.25, 0.25) * self.forecast:getWindType(prevEvent.windVelocity)
    event.n = math.random() -- random number for weather type determination ( might change later so lower p gives lower n)

    -- correcting endTime if event ends next day
    if event.endTime > 24 then
        event.endTime = event.endTime - 24
        event.endDay = event.endDay + 1
    end

    local season = dayForecast.season
    local period = self.environment:periodAtDay(event.startDay, self.environment.daysPerSeason)
    local pRain = self.data.rainProbability[period]
    local avgHighTemp = self.data.temperature[period]

    local rainFreq = math.min(1 - dayForecast.p / pRain, 0.9)
    if dayForecast.forecastType == SeasonsWeather.FORECAST_RAIN_SHOWERS or dayForecast.forecastType == SeasonsWeather.FORECAST_SNOW_SHOWERS then
        rainFreq = rainFreq / 2
    end

    event.weatherType = self:getWeatherType(dayForecast.forecastType, event.temperatureIndication, avgHighTemp, event.n, rainFreq)
    event = self:updateWeatherEvent(event, prevEvent, dayForecast)

    event.stormIntensity = 0
    if event.weatherType == self.WEATHERTYPE_THUNDER then
        event.stormIntensity = SeasonsMathUtil.random(0.2, 1)
    end

    event.cirrusCloudSpeedFactor = 0
    if event.cirrusCloudDensityScale ~= 0 then
        event.cirrusCloudSpeedFactor = MathUtil.clamp((event.windVelocity - 3) / 10, 0, 1)
    end

    event.windDirX, event.windDirZ = self:calculateWindDirection(prevEvent.windDirX, prevEvent.windDirZ)

    return event
end

function SeasonsWeather:calculateWindDirection(dirX, dirZ)
    local angle = math.atan2(dirX, dirZ) + math.rad(SeasonsMathUtil.random(-15, 15))

    return math.cos(angle), math.sin(angle)
end

function SeasonsWeather:updateWeatherEvent(event, prevEvent, dayForecast)
    event.precipitationType = SeasonsWeather.PRECIPITATION_TYPES[event.weatherType + 1]
    event.precipitationIntensity = 0
    if event.precipitationType ~= nil then
        event.precipitationIntensity = math.random()
    end

    local minCirrusScale = SeasonsWeather.MIN_CIRRUS_SCALE[event.weatherType + 1]
    event.cirrusCloudDensityScale = MathUtil.clamp(prevEvent.cirrusCloudDensityScale + SeasonsMathUtil.random(-0.1, 0.1), minCirrusScale, 1)

    event.cloudCoverage = MathUtil.clamp(dayForecast.cloudCover +  SeasonsMathUtil.random(-0.1, 0.1), 0, 1)
    if event.precipitationIntensity > 0 then
        event.cloudCoverage = 1
    end

    event.cloudTypeFrom = MathUtil.clamp(prevEvent.cloudTypeFrom +  SeasonsMathUtil.random(-0.1, 0.1), 0, prevEvent.cloudTypeTo)
    event.cloudTypeTo = MathUtil.clamp(prevEvent.cloudTypeTo +  SeasonsMathUtil.random(-0.1, 0.1), prevEvent.cloudTypeFrom, 1)

    if event.weatherType == SeasonsWeather.WEATHERTYPE_SUN then
        event.cloudCoverage = 0
        event.cloudTypeFrom = 0
        event.cloudTypeTo = 0
    end

    event.fogScale = event.cloudCoverage * self.FOGSCALE

    return event
end

function SeasonsWeather:getFirstEvent()
    return SeasonsWeatherEvent:new()
end

function SeasonsWeather:getWeatherType(forecastType, temp, avgHighTemp, n, pRain)

    if forecastType == SeasonsWeather.FORECAST_SUN then
        return SeasonsWeather.WEATHERTYPE_SUN
    elseif forecastType == SeasonsWeather.FORECAST_RAIN_SHOWERS or forecastType == SeasonsWeather.FORECAST_SNOW_SHOWERS then
        -- always leave 10% probability for sunny weather
        if n > 0.9 then
            return SeasonsWeather.WEATHERTYPE_SUN
        elseif n < pRain then
            if temp < 1 then
                return SeasonsWeather.WEATHERTYPE_SNOW
            else
                return SeasonsWeather.WEATHERTYPE_RAIN
            end
        else
            return SeasonsWeather.WEATHERTYPE_CLOUDY
        end

    elseif forecastType == SeasonsWeather.FORECAST_PARTLY_CLOUDY or forecastType == SeasonsWeather.FORECAST_CLOUDY then
        return SeasonsWeather.WEATHERTYPE_CLOUDY

    elseif forecastType == SeasonsWeather.FORECAST_RAIN or forecastType == SeasonsWeather.FORECAST_SNOW or forecastType == SeasonsWeather.FORECAST_SLEET then
        if n > pRain then
            return SeasonsWeather.WEATHERTYPE_CLOUDY
        else
            if temp < 1 then
                return SeasonsWeather.WEATHERTYPE_SNOW
            elseif temp > avgHighTemp and n < 0.1 then
                return SeasonsWeather.WEATHERTYPE_THUNDER
            else
                return SeasonsWeather.WEATHERTYPE_RAIN
            end
        end
    end
end

----------------------
-- Crop weather damage functions
----------------------

function SeasonsWeather:updateAverageSoilWaterContent()
    local oldWaterContent = self.averageSoilWaterContent
    local duration = self.environment.daysPerSeason * 24

    self.averageSoilWaterContent = oldWaterContent * (duration - 1) / duration + self.soilWaterContent / duration
end

function SeasonsWeather:getDroughtSeverity()
    if self.averageSoilWaterContent < 0.05 then
        return 1 -- max damage
    elseif self.averageSoilWaterContent >= 0.05 and self.averageSoilWaterContent < 0.08 then
        return 2
    elseif self.averageSoilWaterContent >= 0.08 and self.averageSoilWaterContent < 0.12 then
        return 3
    else
        return 4 -- no damage
    end
end

-- low air temp for the day that passed
function SeasonsWeather:updateLowAirTemp()
    local lowTemp = self.forecast:getCurrentItem().lowTemp

    if lowTemp < self.lowAirTemp then
        self.lowAirTemp = lowTemp
    end
end

-- http://www.fao.org/docrep/008/y7223e/y7223e0a.htm
-- returns 4 (no damage, 0 to -1 deg Celsius) to 1 (max damage, below -6 deg Celsius)
function SeasonsWeather:getFrostSeverity()
    return 4 - math.floor(MathUtil.clamp(self.lowAirTemp * - 0.5, 0, 3))
end

----------------------
-- Updaters
----------------------

---Update things that depend on the weather state, like pedestrians
function SeasonsWeather:updateWeatherConditionals()
    if self.forecast:getCurrentItem().lowTemp < 1 then
        if self.mission.pedestrianSystem ~= nil then
            self.mission.pedestrianSystem:setNightTimeRange(0, 0)
        end
    end
end

----------------------
-- Getters
----------------------

---Set whether crop moisture is enabled
function SeasonsWeather:setCropMoistureEnabled(enabled)
    self.moistureEnabled = enabled
end

---Get whether crop moisture is enabled
function SeasonsWeather:getCropMoistureEnabled()
    return self.moistureEnabled
end

---Get whether the ground is currently frozen
function SeasonsWeather:isGroundFrozen()
    -- Can be nil when called during loading which is done by the field NPCs
    return self.soilTemp ~= nil and self.soilTemp < 0
end

---Get whether crops are likely to be wet
function SeasonsWeather:isCropWet()
    if self.moistureEnabled then
        return self.cropMoistureContent > 20 or self.handler:getTimeSinceLastRain() == 0
    else
        return self.handler:getTimeSinceLastRain() < 2 * 60
    end
end

---Get the current air temperature
function SeasonsWeather:getCurrentAirTemperature()
    local hour = self.mission.environment.currentHour
    local minute = self.mission.environment.currentMinute

    -- Caching
    if self.latestCurrentTempHour == hour and self.latestCurrentTempMinute == minute then
        return self.latestCurrentTemp
    end

    self.latestCurrentTempHour = hour
    self.latestCurrentTempMinute = minute
    self.latestCurrentTemp = self.forecast:diurnalTemp(hour)

    return self.latestCurrentTemp
end

---Get the current soil temperature
function SeasonsWeather:getCurrentSoilTemperature()
    return self.soilTemp
end

---Get the soil wetness. If the ground is frozen, wetness is always zero.
function SeasonsWeather:getSoilWetness()
    if self:isGroundFrozen() then
        return 0
    else
        return self.soilWaterContent
    end
end

---Generate a table with minimum soil temperatures over a year, by simulating the weather quickly.
-- If lowTemp < germTemp-1, it is too cold to germinate
function SeasonsWeather:getLowSoilTemperature()
    local lowSoilTemp = {}
    local soilTemp = {}

    local daysPerSeason = 9

    for i = 1,12 do
        lowSoilTemp[i] = -math.huge
    end

    -- run after loading data from xml so self.soilTemp will be initial value at this point
    soilTemp[1] = self.data.startValues.soilTemp

    -- building table with hard coded 9 day season
    for i = 2, 4 * daysPerSeason do
        local period = self.environment:periodAtDay(i, daysPerSeason)
        local periodPrevDay = self.environment:periodAtDay(i - 1, daysPerSeason)

        local averageDailyMaximum = self.data.temperature[period]

        local lowTemp, highTemp = self.model:calculateAirTemp(averageDailyMaximum, true)

        soilTemp[i], _ = self.model:calculateSoilTemp(soilTemp[i - 1], soilTemp[i - 1], lowTemp, highTemp, 0, daysPerSeason, true)
        if soilTemp[i] > lowSoilTemp[period] then
            lowSoilTemp[period] = soilTemp[i]
        end

        if period > periodPrevDay and soilTemp[i] > soilTemp[i - 1] then
            lowSoilTemp[period - 1] = soilTemp[i]
        end
    end

    return lowSoilTemp
end

---Get the maximum soil temperature from yesterday
function SeasonsWeather:getYesterdayMaxSoilTemp()
    return Utils.getNoNil(self.yesterdaySoilTemp, 0)
end

function SeasonsWeather:getForecast(day, time, duration)
    local daysUntil = day - self.environment.currentDay
    local std = 0.15 + 0.05 * daysUntil
    local uncertaintyFactor = std / 0.15

    if daysUntil > 1 or duration == 24 then
        -- use forecast
        local info = ListUtil.copyTable(self.forecast:getForecastForDay(day))

        info.lowTemp = info.lowTemp * (1 + info.tempUncertainty * uncertaintyFactor)
        info.highTemp = info.highTemp * (1 + info.tempUncertainty * uncertaintyFactor)
        info.averageTemp = (info.lowTemp * 15 + info.highTemp * 9) / 24

        info.windSpeed = info.windSpeed * (1 + info.windUncertainty * uncertaintyFactor)

        local temp = self.forecast:diurnalTemp(info.startTimeIndication, info.highTemp, info.lowTemp, info.highTemp, info.lowTemp)
        local period = self.environment:periodAtDay(day)
        local rainProb = self.data.rainProbability[period]

        info.p = MathUtil.clamp(info.p * (1 + info.weatherTypeUncertainty * uncertaintyFactor), 0, 1)
        info.forecastType, _ = self.forecast:getForecastType(day, info.p, temp, info.averageTemp, info.windSpeed)

        info.precipitationChance = MathUtil.clamp(MathUtil.round(SeasonsMathUtil.normCDF(rainProb, info.p, std), 1), 0, 0.9)

        local realPrecipitationAmount = self.model:getRainAmount(day, 0.5) * 24 * self:getRainScale(info.forecastType)
        info.precipitationAmount = realPrecipitationAmount + info.precipitationChance * uncertaintyFactor

        return info
    elseif duration > 24 or (duration == 24 and time ~= 0) then
        return nil

    else
        local hoursUntil = math.max(time - self.mission.environment.currentHour, 0)
        if daysUntil > 0 then
            hoursUntil = hoursUntil + daysUntil * 24
        end

        local dayForecast = self.forecast:getForecastForDay(day)
        local info = {}

        -- Find the events within the period
        local baseDay = day - self.environment.currentDayOffset -- basegame day we want in events
        local events = self:getEventsInPeriod(baseDay, time, duration)

        -- #events can't be nil normally, unless it is looking too far away.
        -- Force a forecast lookup instead
        if #events == 0 then
            return self:getForecast(day, 0, 24)
        end

        -- Find low temp
        local startTemp = self.forecast:diurnalTemp(time)
        local endTemp = self.forecast:diurnalTemp(time + duration)
        info.lowTemp = math.min(startTemp, endTemp) * (1 + dayForecast.tempUncertainty * uncertaintyFactor)

        -- Find high temp
        info.highTemp = math.max(startTemp, endTemp) * (1 + dayForecast.tempUncertainty * uncertaintyFactor)

        --info.averagePeriodTemp = luafp.reduce(events, function(accum, e) return accum + e.temperatureIndication end, 0) / #events
        info.averageTemp = (info.lowTemp + info.highTemp) / 2

        -- TODO
        local eventDuration = 3 --events[1].endTime > time and duration or events[1].endTime - time

        local n = MathUtil.clamp(events[1].n * (1 + dayForecast.weatherTypeUncertainty * uncertaintyFactor), 0, 1)
        local period = self.environment:periodAtDay(day)
        local avgHighTemp = self.data.temperature[period]
        local rainProb = self.data.rainProbability[period]
        local rainFreq = math.min(1 - dayForecast.p / rainProb, 0.9)
        if dayForecast.forecastType == SeasonsWeather.FORECAST_RAIN_SHOWERS or dayForecast.forecastType == SeasonsWeather.FORECAST_SNOW_SHOWERS then
            rainFreq = rainFreq / 2
        end

        info.weatherType = self:getWeatherType(dayForecast.forecastType, info.averageTemp, avgHighTemp, n, rainFreq)
        info.weatherType = events[1].weatherType

        info.precipitationChance = MathUtil.round(SeasonsMathUtil.normCDF(rainFreq, n, std), 1)

        local realPrecipitationAmount = self.model:getRainAmount(events[1].startDay, events[1].precipitationIntensity) * eventDuration
        info.precipitationAmount = realPrecipitationAmount + info.precipitationChance * uncertaintyFactor

        info.windSpeed = events[1].windVelocity * (1 + dayForecast.windUncertainty * uncertaintyFactor)

        info.dryingPotential = 0
        if self.cropDryingForecast ~= nil then
            for i = 1,duration do
                info.dryingPotential = info.dryingPotential + self.cropDryingForecast[hoursUntil + i]
                -- log(day, time, info.dryingPotential, self.cropDryingForecast[hoursUntil + i])
            end
        end

        return info
    end
end

function SeasonsWeather:cropDryingSimulation(cropMoistureContent, day, hour)
    local delta = 0
    local prevCropMoistureContent = cropMoistureContent
    local futureDryingPotential = {}

    -- simulate for the next 48 hours
    for i = 1, 48 do
        hour = hour + 1
        if hour >= 24 then
            hour = 0
            day = day + 1
        end

        local julianDay = self.environment:julianDay(day)
        local forecast = self.forecast:getForecastForDay(day)
        local event = self.handler:getEventAtTime(day - self.environment.currentDayOffset, hour)

        local timeSinceLastRain = 0

        if event == nil then
            log("EVENT IS NIL!", julianDay, day, hour, i)
            printCallstack()
            log("TIME", self.environment.currentDay, self.environment.currentOffset, self.environment.daysPerSeason)
            log("DATA")
            print_r(self.handler.events)
        end

        if event.precipitationIntensity == 0 then
            timeSinceLastRain = 1
        end

        cropMoistureContent, delta = self.model:updateCropMoistureContent(cropMoistureContent, julianDay, hour, event.temperatureIndication, forecast.lowTemp, event.windVelocity, event.cloudCoverage, event.precipitationIntensity, event.fogScale, timeSinceLastRain)
        table.insert( futureDryingPotential, delta )
    end

    return futureDryingPotential
end

function SeasonsWeather:getRainScale(forecastType)
    if forecastType == SeasonsWeather.FORECAST_RAIN_SHOWERS or forecastType == SeasonsWeather.FORECAST_SNOW_SHOWERS then
        return 0.3
    elseif forecastType == SeasonsWeather.FORECAST_RAIN or forecastType == SeasonsWeather.FORECAST_SNOW or forecastType == SeasonsWeather.FORECAST_SLEET then
        return 0.9
    else
        return 0
    end
end

---Get a list of events within given time slot
function SeasonsWeather:getEventsInPeriod(baseDay, time, duration)
    local events = {}
    local endTime = time + duration

    for _, event in ipairs(self.handler.events) do
        if event.startDay <= baseDay and event.endDay >= baseDay then
            -- We need to check for days a lot because the hours reset. An event can start and end at 6
            -- but that does not mean a time at 9 is not within the event: it could be a day-long event

            if baseDay == event.startDay and baseDay == event.endDay then -- Event is in 1 day only, and in same day
                if (time > event.startTime and time < event.endTime) or (endTime > event.startTime and endTime < event.endTime) then
                    table.insert(events, event)
                end
            elseif baseDay == event.startDay then -- In first day of event, start part only
                if time > event.startTime or endTime > event.startTime then
                    table.insert(events, event)
                end
            elseif baseDay == event.endDay then -- In last day of event, end part only
                if time < event.endTime or endTime < event.endTime then
                    table.insert(events, event)
                end
            else
                -- Covers whole event
                table.insert(events, event)
                break
            end
        end

        -- Won't find later
        if event.startDay > baseDay then
            break
        end
    end

    return events
end

---Convert meters per second to Beaufort scale
function SeasonsWeather:getBeaufortScale(ms)
    return MathUtil.round(math.pow(ms / 0.836, 0.6666), 0)
end

---Get current state of downfall
function SeasonsWeather:getDownfallState()
    return self.handler.downfallUpdater:getCurrentValues()
end

---Get current state of downfall including fading info (left and right plus fade info)
function SeasonsWeather:getDownfallFadeState()
    return self.handler.downfallUpdater:getCurrentFadeState()
end

---Get whether it is currently freezing outside
function SeasonsWeather:getIsFreezing()
    return math.floor(self:getCurrentAirTemperature()) <= 0
end

---Returns true when the current weatherType is snow, false otherwise
function SeasonsWeather:isSnowing()
    local event = self.handler:getCurrentEvent()
    if event == nil then
        return false
    end

    return event.weatherType == SeasonsWeather.WEATHERTYPE_SNOW
end

---Get current wind velocity. 0-27m/s
function SeasonsWeather:getWindVelocity()
    local _, _, velocity = self.handler.windUpdater:getCurrentValues()
    return velocity
end

---Get whether a storm is active
function SeasonsWeather:getIsStorming()
    return self.handler.stormUpdater:getCurrentValues() > 0
end

---Get the rot/dry factor
function SeasonsWeather:getRotDryFactor()
    return self.rotDryFactor
end

---Reset the rot/dry factor
function SeasonsWeather:resetRotDryFactor()
    self.rotDryFactor = 0
end

----------------------
-- Injections
----------------------

---Insert the new weather handling into the environment
function SeasonsWeather.inj_environment_new(environment, superFunc, xmlFilename)
    -- Disable initialization of the vanilla weather
    local old = Weather.new
    Weather.new = function() return { setIsRainAllowed = function() end, load = function() end } end

    local self = superFunc(environment, xmlFilename)

    -- Reset class
    Weather.new = old

    -- Use our own weather system
    self.weather = g_seasons.weather.handler

    return self
end

---Use Seasons weather types and more icons
function SeasonsWeather.inj_gameInfoDisplay_getWeatherUVs(gameInfoDisplay, superFunc)
    return {
        [SeasonsWeather.WEATHERTYPE_SUN]        = GameInfoDisplay.UV.WEATHER_ICON_CLEAR,
        [SeasonsWeather.WEATHERTYPE_CLOUDY]     = GameInfoDisplay.UV.WEATHER_ICON_CLOUDY,
        [SeasonsWeather.WEATHERTYPE_RAIN]       = GameInfoDisplay.UV.WEATHER_ICON_RAIN,
        [SeasonsWeather.WEATHERTYPE_SNOW]       = GameInfoDisplay.UV.WEATHER_ICON_SNOW,
        [SeasonsWeather.WEATHERTYPE_FOG]        = GameInfoDisplay.UV.WEATHER_ICON_FOG,
        [SeasonsWeather.WEATHERTYPE_THUNDER]    = GameInfoDisplay.UV.WEATHER_ICON_THUNDER,
        [SeasonsWeather.WEATHERTYPE_HAIL]       = GameInfoDisplay.UV.WEATHER_ICON_HAIL,
    }
end

function SeasonsWeather.inj_gameInfoDisplay_getWeatherStates(gameInfoDisplay, superFunc)
    return g_seasons.weather.handler:getHUDInfo()
end

----------------------
-- Wind turbine affected by wind speed and direction
----------------------

function SeasonsWeather:inj_windTurbinePlaceable_updateHeadRotation(superFunc)
    local x, z, _, _ = g_seasons.weather.handler.windUpdater:getCurrentValues()
    local angle = math.atan2(x, z)

    -- local eventAngle = math.atan2(g_seasons.weather.handler.events[1].windDirX, g_seasons.weather.handler.events[1].windDirZ)
    -- local _,y1,_ = getWorldRotation(self.headNode)
    -- log(eventAngle, angle, y1)

    local dx,_,dz = worldDirectionToLocal(self.nodeId, math.sin(angle),0,math.cos(angle))
    setDirection(self.headNode, dx,0,dz, 0,1,0)
end

function SeasonsWeather:inj_windTurbinePlaceable_update(superFunc, dt)
    local _, _, windSpeed, _ = g_seasons.weather.handler.windUpdater:getCurrentValues()
    -- not running at low wind speeds and shutting down at high wind speeds
    local speed = MathUtil.clamp(windSpeed - 3, 0, 8)
    if windSpeed > 25 then
        speed = 0
    end

    if self.rotationNode ~= 0 then
        if speed > 0 then
            rotate(self.rotationNode, 0, 0, -0.0005 * (speed + 3) * dt)
            self:updateHeadRotation()
        end

        self:raiseActive()
    end
end

function SeasonsWeather:inj_windTurbinePlaceable_hourChanged()
    local _, _, windSpeed, _ = g_seasons.weather.handler.windUpdater:getCurrentValues()
    local effectRatio = MathUtil.clamp(windSpeed - 3, 0, 8) / 8
    local income = self.incomePerHour

    if windSpeed < 25 then
        income = income * effectRatio
    else
        income = 0
    end

    if self.isServer and income > 0 then
        g_currentMission:addMoney(income, self:getOwnerFarmId(), MoneyType.PROPERTY_INCOME, true)
    end
end

----------------------
-- Console commands
----------------------

---Add a basic weather event for the next hour
function SeasonsWeather:consoleCommandAddEvent(typ, intensity, offset)
    if typ == nil or intensity == nil then
        return "Usage: rmWeatherAddEvent rain/snow/hail 0-1 [offset]"
    end

    local offset = tonumber(offset) or 0

    local currentEvent = self.handler:getCurrentEvent()
    local nextEvent = self.handler:getNextEvent()

    -- Create new event for 1 hour
    local newEvent = SeasonsWeatherEvent:new()
    newEvent.startDay = currentEvent.startDay
    newEvent.startTime = math.ceil(self.mission.environment.currentHour + 0.5 + offset)
    if newEvent.startTime == 24 then
        newEvent.startDay = newEvent.startDay + 1
        newEvent.startTime = 0
    end

    newEvent.endTime = newEvent.startTime + 1
    newEvent.endDay = newEvent.startDay
    if newEvent.endTime == 24 then
        newEvent.endTime = 0
        newEvent.endDay = newEvent.endDay + 1
    end

    if typ == "rain" then
        newEvent.precipitationIntensity = tonumber(intensity)
        newEvent.precipitationType = typ
        newEvent.weatherType = SeasonsWeather.WEATHERTYPE_RAIN
    elseif typ == "snow" then
        newEvent.precipitationIntensity = tonumber(intensity)
        newEvent.precipitationType = typ
        newEvent.weatherType = SeasonsWeather.WEATHERTYPE_SNOW
    elseif typ == "hail" then
        newEvent.precipitationIntensity = tonumber(intensity)
        newEvent.precipitationType = typ
        newEvent.weatherType = SeasonsWeather.WEATHERTYPE_HAIL
    end

    table.insert(self.handler.events, 2, newEvent)

    -- Cut the current event down to stop at end of the hour
    local originalEndDay, originalEndTime = currentEvent.endDay, currentEvent.endTime
    currentEvent.endDay = newEvent.startDay
    currentEvent.endTime = newEvent.startTime

    -- Always set next event start time to end of new event (easiest)
    nextEvent.startTime = newEvent.endTime
    nextEvent.startDay = newEvent.endDay

    if nextEvent.startTime == nextEvent.endTime and nextEvent.startDay == nextEvent.endDay then
        List.removeElement(self.handler.events, nextEvent)
    end

    return "Added new event from " .. tostring(newEvent.startTime) .. " to " .. tostring(newEvent.endTime)
end

function SeasonsWeather:consoleCommandSetWindVelocity(velocity, duration)
    if velocity == nil then
        return "Usage: velocity (in m/s, from 0-27) [switch duration]"
    end

    velocity = tonumber(velocity)
    duration = Utils.getNoNil(tonumber(duration), 1000 * 60 * 5)

    local x, z, _, cirrus = self.handler.windUpdater:getCurrentValues()
    self.handler.windUpdater:setTargetValues(x, z, velocity, cirrus, duration)

    return "Switching to " .. tostring(velocity) .. "m/s in " .. (duration / 1000) .. " ingame seconds"
end
