#!/usr/bin/env python

import copy
import time

from datetime import datetime as dt
from datetime import timedelta

import pytz

from aman.config.Airport import Airport
from aman.config.AirportSequencing import AirportSequencing
from aman.config.RHC import RHC
from aman.sys.aco.Node import Node
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.EnrouteArrivalTime
        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.EnrouteArrivalTime
                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.EnrouteArrivalTime
                    break
                lastWindowTime = self.Windows[-1].EndTime

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

    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
            plannedInbound.RequestedRunway = inbound.RequestedRunway
            # ingore fixed updates
            if True == plannedInbound.FixedSequence or index <= self.FreezedIndex:
                plannedInbound.FixedSequence = True
                return
            plannedInbound.WTC = inbound.WTC

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

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

    def resequenceInbound(self, inbound : Inbound):
        index = self.AssignedWindow[inbound.Callsign][0]
        sequenced = self.Windows[index].inbound(inbound.Callsign)
        if None == sequenced:
            return

        # resynchronized the planned information
        sequenced.PlannedRunway = inbound.PlannedRunway
        sequenced.PlannedStar = inbound.PlannedStar
        sequenced.PlannedArrivalRoute = inbound.PlannedArrivalRoute
        sequenced.PlannedArrivalTime = inbound.PlannedArrivalTime
        sequenced.InitialArrivalTime = inbound.InitialArrivalTime
        sequenced.PlannedTrackmiles = inbound.PlannedTrackmiles
        sequenced.AssignmentMode = inbound.AssignmentMode
        sequenced.ExpectedRunway = inbound.ExpectedRunway
        sequenced.HasValidSequence = True

        # resort the inbound
        if sequenced.PlannedArrivalTime < self.Windows[index].StartTime or sequenced.PlannedArrivalTime >= self.Windows[index].EndTime:
            self.Windows[index].remove(sequenced.Callsign)
            self.AssignedWindow.pop(sequenced.Callsign)
            self.insertInWindow(sequenced, True)
        else:
            sequenced.FixedSequence = index < self.FreezedIndex
            self.Windows[index].Inbounds.sort(key = lambda x: x.PlannedArrivalTime if None != x.PlannedArrivalTime else x.EnrouteArrivalTime)

    def latestFixedInbounds(self, configuration : Airport, sequenceConfiguration : AirportSequencing):
        if 0 == len(self.Windows):
            return None, None

        # create the runway tree
        runwayInbounds = {}
        for runway in sequenceConfiguration.ActiveArrivalRunways:
            runwayInbounds[runway.Runway.Name] = None

        # create the IAF tree
        iafInbounds = {}
        for star in configuration.ArrivalRouteConstraints:
            altitude = configuration.ArrivalRouteConstraints[star][0].Altitude
            iaf = configuration.ArrivalRouteConstraints[star][0].Name

            if iaf not in iafInbounds:
                iafInbounds[iaf] = { altitude : None }
            elif altitude not in iafInbounds[iaf]:
                iafInbounds[iaf][altitude] = None

        # associate the inbounds to the runways and the IAFs
        for i in range(min(self.FreezedIndex, len(self.Windows)), -1, -1):
            for inbound in self.Windows[i].Inbounds:
                if None == inbound.PlannedRunway or None == inbound.PlannedArrivalRoute:
                    continue

                node = Node(inbound, None, None, None, None)

                if inbound.PlannedRunway.Name in runwayInbounds:
                    if None == runwayInbounds[inbound.PlannedRunway.Name] or runwayInbounds[inbound.PlannedRunway.Name].Inbound.PlannedArrivalTime < node.Inbound.PlannedArrivalTime:
                        runwayInbounds[inbound.PlannedRunway.Name] = node

                if inbound.PlannedArrivalRoute[0].Waypoint.Name in iafInbounds:
                    delta = 100000.0
                    targetLevel = None
                    for level in iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name]:
                        difference = abs(level - inbound.PlannedArrivalRoute[0].Altitude)
                        if difference < delta:
                            delta = difference
                            targetLevel = level

                    if None == iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel]:
                        iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel] = node
                    elif iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel].Inbound.PlannedArrivalTime < node.Inbound.PlannedArrivalTime:
                        iafInbounds[inbound.PlannedArrivalRoute[0].Waypoint.Name][targetLevel] = node

        return runwayInbounds, iafInbounds

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

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

        # check the overlapping windows
        for i in range(self.FreezedIndex + 1, len(self.Windows)):
            for inbound in self.Windows[i].Inbounds:
                inbounds.append(copy.deepcopy(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.EnrouteArrivalTime)
            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:
                if True == inbound.HasValidSequence:
                    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)
