#!/usr/bin/env python

import time

from datetime import datetime as dt
from datetime import timedelta

import pytz

from aman.config.RHC import RHC
from aman.sys.RecedingHorizonWindow import RecedingHorizonWindow
from aman.types.Inbound import Inbound

class RecedingHorizonControl:
    def __init__(self, config : RHC):
        self.Windows = []
        # contains the current index and the missed update counter
        self.AssignedWindow = {}
        self.Configuration = config
        self.FreezedIndex = int(self.Configuration.FixedBeforeArrival.seconds / self.Configuration.WindowSize)

    def insertInWindow(self, inbound : Inbound, usePTA : bool):
        if False == usePTA:
            referenceTime = inbound.InitialArrivalTime
        else:
            referenceTime = inbound.PlannedArrivalTime

        inserted = False
        for i in range(0, len(self.Windows)):
            window = self.Windows[i]

            # find the correct window
            if window.StartTime <= referenceTime and window.EndTime > referenceTime:
                self.AssignedWindow[inbound.Callsign] = [ i, 0 ]
                inbound.FixedSequence = i < self.FreezedIndex
                if True == inbound.FixedSequence and None == inbound.PlannedArrivalTime:
                    inbound.PlannedArrivalTime = inbound.InitialArrivalTime
                window.insert(inbound)
                inserted = True
                break

        # create the new window
        if False == inserted:
            if 0 != len(self.Windows):
                lastWindowTime = self.Windows[-1].EndTime
            else:
                lastWindowTime = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
            timestep = timedelta(seconds = self.Configuration.WindowSize)

            while True:
                self.Windows.append(RecedingHorizonWindow(lastWindowTime, lastWindowTime + timestep))
                if self.Windows[-1].EndTime > referenceTime:
                    window = self.Windows[-1]
                    window.insert(inbound)

                    self.AssignedWindow[inbound.Callsign] = [ len(self.Windows) - 1, 0 ]
                    inbound.FixedSequence = len(self.Windows) < self.FreezedIndex
                    if True == inbound.FixedSequence and None == inbound.PlannedArrivalTime:
                        inbound.PlannedArrivalTime = inbound.InitialArrivalTime
                    break
                lastWindowTime = self.Windows[-1].EndTime

        window.Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.InitialArrivalTime)

    def updateReport(self, inbound : Inbound):
        # check if we need to update
        if inbound.Callsign in self.AssignedWindow:
            index = self.AssignedWindow[inbound.Callsign][0]
            self.AssignedWindow[inbound.Callsign][1] = 0

            plannedInbound = self.Windows[index].inbound(inbound.Callsign)
            plannedInbound.Report = inbound.Report
            plannedInbound.ReportTime = inbound.ReportTime
            plannedInbound.CurrentPosition = inbound.CurrentPosition
            # ingore fixed updates
            if True == plannedInbound.FixedSequence or index < self.FreezedIndex:
                return
            plannedInbound.WTC = inbound.WTC

            # check if we need to update the inbound
            if None == plannedInbound.PlannedStar:
                reference = inbound.InitialArrivalTime
                if plannedInbound.InitialArrivalTime > reference:
                    reference = plannedInbound.InitialArrivalTime

                if reference < self.Windows[index].StartTime or reference >= self.Windows[index].EndTime:
                    self.Windows[index].remove(inbound.Callsign)
                    self.AssignedWindow.pop(inbound.Callsign)
                    inbound.InitialArrivalTime = reference
                    self.updateReport(inbound)
                else:
                    plannedInbound.InitialArrivalTime = reference
                    self.Windows[index].Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.InitialArrivalTime)
        else:
            self.insertInWindow(inbound, False)

    def resequenceInbound(self, inbound : Inbound):
        index = self.AssignedWindow[inbound.Callsign][0]
        if inbound.PlannedArrivalTime < self.Windows[index].StartTime or inbound.PlannedArrivalTime >= self.Windows[index].EndTime:
            self.Windows[index].remove(inbound.Callsign)
            self.AssignedWindow.pop(inbound.Callsign)
            self.insertInWindow(inbound, True)
        else:
            inbound.FixedSequence = index < self.FreezedIndex
            self.Windows[index].Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.InitialArrivalTime)

    def lastFixedInboundOnRunway(self, runway : str):
        # no inbounds available
        if 0 == len(self.Windows):
            return None

        # search from the back to the front to find the last inbound
        for i in range(min(self.FreezedIndex, len(self.Windows)), -1, -1):
            for inbound in self.Windows[i].Inbounds:
                if runway == inbound.PlannedRunway.Name:
                    return inbound

        # no inbound found
        return None

    def optimizationRelevantInbounds(self):
        # no new inbounds
        if len(self.Windows) <= self.FreezedIndex:
            return None, None

        inbounds = []
        earliestArrivalTime = self.Windows[self.FreezedIndex].StartTime

        # check the overlapping windows
        for i in range(self.FreezedIndex + 1, len(self.Windows)):
            for inbound in self.Windows[i].Inbounds:
                inbounds.append(inbound)

            if 20 <= len(inbounds):
                break

        # check if we found relevant inbounds
        if 0 != len(inbounds):
            inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.InitialArrivalTime)
            return inbounds, earliestArrivalTime
        else:
            return None, None

    def sequence(self):
        inbounds = []

        for i in range(0, len(self.Windows)):
            for inbound in self.Windows[i].Inbounds:
                inbounds.append(inbound)

        return inbounds

    def cleanupWindows(self):
        currentUtc = dt.utcfromtimestamp(int(time.time())).replace(tzinfo = pytz.UTC)
        offsetCorrection = 0

        # delete the non-required windows
        while 0 != len(self.Windows) and currentUtc > self.Windows[0].EndTime:
            # cleanup the association table
            for inbound in self.Windows[0].Inbounds:
                self.AssignedWindow.pop(inbound.Callsign)

            offsetCorrection += 1
            self.Windows.pop(0)

        # correct the association table
        if 0 != offsetCorrection:
            for callsign in self.AssignedWindow:
                self.AssignedWindow[callsign][0] -= offsetCorrection
                if self.AssignedWindow[callsign][0] < self.FreezedIndex:
                    self.Windows[self.AssignedWindow[callsign][0]].inbound(callsign).FixedSequence = True

        # delete the non-updated aircrafts and increase the missed-counter for later runs
        callsigns = []
        for callsign in self.AssignedWindow:
            if 2 < self.AssignedWindow[callsign][1]:
                self.Windows[self.AssignedWindow[callsign][0]].remove(callsign)
                callsigns.append(callsign)
            self.AssignedWindow[callsign][1] += 1

        for callsign in callsigns:
            self.AssignedWindow.pop(callsign)
