content: package com.amnesica.belugaproject.services.aircraft;
  
  import com.amnesica.belugaproject.config.Configuration;
  import com.amnesica.belugaproject.config.Feeder;
  import com.amnesica.belugaproject.config.StaticValues;
  import com.amnesica.belugaproject.entities.aircraft.Aircraft;
  import com.amnesica.belugaproject.entities.data.AirportData;
  import com.amnesica.belugaproject.repositories.aircraft.AircraftRepository;
  import com.amnesica.belugaproject.services.data.AircraftDataService;
  import com.amnesica.belugaproject.services.data.AirportDataService;
  import com.amnesica.belugaproject.services.network.NetworkHandlerService;
  import com.amnesica.belugaproject.services.trails.AircraftTrailService;
  import lombok.extern.slf4j.Slf4j;
  import org.json.JSONArray;
  import org.json.JSONObject;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.scheduling.annotation.EnableScheduling;
  import org.springframework.scheduling.annotation.Scheduled;
  import org.springframework.stereotype.Service;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.List;
  import java.util.stream.Collectors;
  
  @Slf4j
  @EnableScheduling
  @Service
  public class LocalFeederService {
    @Autowired
    private AircraftService aircraftService;
  
    @Autowired
    private NetworkHandlerService networkHandler;
  
    @Autowired
    private AircraftDataService aircraftDataService;
    @Autowired
    private HistoryAircraftService historyAircraftService;
    @Autowired
    private AircraftTrailService aircraftTrailService;
    @Autowired
    private AirportDataService airportDataService;
  
    @Autowired
    private AircraftRepository aircraftRepository;
  
    @Autowired
    private Configuration configuration;
  
    // Sets mit hex der Flugzeuge, um Flugzeuge temporär zu speichern
    // (nötig um feederList und sourceList jeweils zu füllen)
    private final HashSet<String> currentIterationSet = new HashSet<>();
    private final HashSet<String> previousIterationSet = new HashSet<>();
  
    /**
     * Methode fragt Flugzeuge von den lokalen Feedern ab und speichert diese in der
     * Tabelle aircraft. Methode wird alle INTERVAL_UPDATE_LOCAL_FEEDER Sekunden
     * aufgerufen
     */
    @Scheduled(fixedRate = StaticValues.INTERVAL_UPDATE_LOCAL_FEEDER)
    public void getPlanesFromFeeder() {
      currentIterationSet.clear();
  
      for (Feeder feeder : configuration.getListFeeder()) {
        if (feeder == null) return;
  
        JSONArray jsonArrayPlanesFeeder = getAircraftJSONFromLocalFeeder(feeder);
        if (jsonArrayPlanesFeeder == null) continue;
  
        for (int i = 0; i < jsonArrayPlanesFeeder.length(); i++) {
          final JSONObject element = jsonArrayPlanesFeeder.getJSONObject(i);
          Aircraft aircraftNew = null;
          if (isValidAircraft(element, feeder)) aircraftNew = aircraftService.createNewAircraft(element, feeder);
          if (aircraftNew == null) continue;
  
          processAircraft(aircraftNew, feeder);
        }
      }
  
      previousIterationSet.clear();
      previousIterationSet.addAll(currentIterationSet);
    }
  
    private void processAircraft(Aircraft aircraftNew, Feeder feeder) {
      // Füge hex mit feeder zu currentIterationSet hinzu
      currentIterationSet.add(aircraftNew.getHex());
  
      // Aktualisiere Flugzeug aus Datenbank oder
      // füge neues Flugzeug zur Datenbank hinzu
      if (aircraftRepository.existsById(aircraftNew.getHex())) {
        updateExistingAircraft(aircraftNew, feeder);
      } else {
        addNewAircraft(aircraftNew, feeder);
      }
    }
  
    private void addNewAircraft(Aircraft aircraftNew, Feeder feeder) {
      // Füge Informationen aus aircraftData hinzu
      aircraftDataService.addAircraftData(aircraftNew);
  
      // Setze Boolean, dass Flugzeug nicht von Opensky ist
      aircraftNew.setIsFromRemote(null);
  
      // Füge Timestamp als Zeitpunkt des letzten Updates an
      aircraftNew.setLastUpdate(System.currentTimeMillis());
  
      // Speichere Trail
      aircraftTrailService.addTrail(aircraftNew, feeder.getName());
  
      try {
        // Schreibe Flugzeug in aircraft-Tabelle
        aircraftRepository.save(aircraftNew);
      } catch (Exception e) {
        log.error("Server - DB error when writing aircraftNew for hex " + aircraftNew.getHex()
            + ": Exception = " + e);
      }
    }
  
    private void updateExistingAircraft(Aircraft aircraftNew, Feeder feeder) {
      // Prüfe, ob Flugzeug in Datenbank-Tabelle enthalten ist
      Aircraft aircraftInDb = aircraftRepository.findByHex(aircraftNew.getHex());
  
      // Wenn Flugzeug in der letzten Iteration vorhanden war, lösche Feeder-Liste und Source-Liste,
      // damit nach der Iteration nur die Feeder und die Sources in der Liste stehen,
      // welche das Flugzeug geupdated haben (nur der erste Feeder pro Iteration soll die Listen löschen)
      if (previousIterationSet.contains(aircraftNew.getHex())) {
        previousIterationSet.remove(aircraftNew.getHex());
        aircraftInDb.clearFeederList();
        aircraftInDb.clearSourceList();
      }
  
      // Update Werte des Flugzeugs mit Werten von aircraftNew
      aircraftService.updateValuesOfAircraft(aircraftInDb, aircraftNew, feeder.getName(), true);
  
      try {
        // Schreibe Flugzeug in aircraft-Tabelle
        aircraftRepository.save(aircraftInDb);
      } catch (Exception e) {
        log.error("Server - DB error when writing aircraftInDb for hex " + aircraftInDb.getHex()
            + ": Exception = " + e);
      }
    }
  
    private boolean isValidAircraft(JSONObject element, Feeder feeder) {
      // Icao is hex in vrs feeder
      return element != null && (element.has("hex") || (feeder.getType().equals("vrs") && element.has("Icao")));
    }
  
    /**
     * Methode kopiert alle Flugzeuge,
     * die länger als RETENTION_TIME_AIRCRAFT_LOCAL
     * Millisekunden (Default = 3600000 = 1 Stunde) nicht geupdatet wurden,
     * in die History-Tabelle und löscht die betroffenen Flugzeuge
     * aus der aircraft-Tabelle.
     * Methode wird alle INTERVAL_LOCAL_PLANES_TO_HISTORY
     * Millisekunden aufgerufen
     */
    @Scheduled(fixedRate = StaticValues.INTERVAL_LOCAL_PLANES_TO_HISTORY)
    private void putOldPlanesInHistoryTable() {
      // Berechne timestamp vor RETENTION_TIME_AIRCRAFT_LOCAL Millisekunden,
      // damit nur die Flugzeuge kopiert werden, die nicht mehr
      // aktualisiert werden
  
      long startTime = System.currentTimeMillis() - StaticValues.RETENTION_TIME_AIRCRAFT_LOCAL;
  
      // Hole Flugzeuge der aktuellen Iteration
      List<Aircraft> listPlanesNotUpdated = aircraftRepository
          .findAllByLastUpdateLessThanEqual(startTime);
  
      if (listPlanesNotUpdated != null) {
        // Lösche Daten der planespotters.net API, damit diese
        // nicht länger als 24h in der Db gespeichert werden
        for (Aircraft aircraft : listPlanesNotUpdated) {
          aircraft.setUrlPhotoDirect(null);
          aircraft.setUrlPhotoWebsite(null);
          aircraft.setPhotoPhotographer(null);
        }
  
        // Packe nicht geupdatete Flugzeuge in History-Tabelle
        boolean successful = historyAircraftService.putListPlanesInHistoryTable(listPlanesNotUpdated);
  
        // Wenn Speicherung in History-Tabelle erfolgreich war, lösche alle betroffenen
        // Flugzeuge aus der Aircraft-Tabelle
        if (successful) {
          aircraftRepository.deleteAll(listPlanesNotUpdated);
        }
      }
    }
  
    /**
     * Wandelt die Flugzeuge von einer url eines Feeder in ein Aircraft-Objekt um
     * und fügt dieses der Liste aircraftList hinzu. Dopplungen (selber hex) werden
     * nicht hinzugefügt.
     *
     * @param feeder Feeder
     */
    private JSONArray getAircraftJSONFromLocalFeeder(Feeder feeder) {
      if (feeder.getIpAddress() != null && !feeder.getIpAddress().isEmpty()) {
        // Anfrage an Feeder mit url
        String jsonStr = networkHandler.makeServiceCallLocalFeeder(feeder.getIpAddress());
  
        try {
          if (jsonStr != null) {
            JSONArray jsonArray;
  
            // Erstelle jsonArray aus jsonStr vom Feeder
            if (feeder.getType().equals("airsquitter")) {
              jsonArray = new JSONArray(jsonStr);
            } else if (feeder.getType().equals("vrs")) {
              JSONObject jsonObject = new JSONObject(jsonStr);
              jsonArray = jsonObject.getJSONArray("acList");
            } else {
              JSONObject jsonObject = new JSONObject(jsonStr);
              jsonArray = jsonObject.getJSONArray("aircraft");
            }
  
            return jsonArray;
          }
        } catch (Exception e) {
          log.error("Server: Temporary aircraft list could not be updated from " + feeder.getIpAddress() + ": Exception = " + e);
        }
      }
      return null;
    }
  
    /**
     * Sucht alle Informationen über ein Flugzeug (aircraft) zusammen und gibt diese
     * zurück. Hinweis: Diese Funktion funktioniert nur für ein Flugzeug der Klasse
     * Aircraft
     *
     * @param hex          String
     * @param registration String
     * @return Object[] (Aircraft-Objekt, AirportData originAirportData, AirportData
     * destinationAirportData)
     */
    public Object[] getAllAircraftData(String hex, String registration) {
      if (hex != null && registration != null && !hex.isEmpty() && !registration.isEmpty()) {
        // Hole Flugzeug aus Datenbank
        Aircraft aircraft = aircraftRepository.findByHex(hex);
  
        if (aircraft != null) {
          // Setze Photo-Urls
          aircraftService.setAircraftPhotoUrls(aircraft);
  
          // Setze weitere Informationen an Flugzeug
          aircraftService.addInformationToAircraft(aircraft);
  
          // Hole Informationen über Herkunfts- und Zielflughafen
          AirportData originAirportData = null;
          AirportData destinationAirportData = null;
          if (aircraft.getOrigin() != null && !aircraft.getOrigin().isEmpty()) {
            originAirportData = airportDataService.getAirportData(aircraft.getOrigin());
          }
          if (aircraft.getDestination() != null && !aircraft.getDestination().isEmpty()) {
            destinationAirportData = airportDataService.getAirportData(aircraft.getDestination());
          }
  
          // Speichere Flugzeug in Datenbank
          try {
            aircraftRepository.save(aircraft);
          } catch (Exception e) {
            log.error("Server - DB error when saving aircraft : Exception = " + e);
          }
  
          // Baue Array als Rückgabewert
          return new Object[]{aircraft, originAirportData, destinationAirportData};
        }
      }
  
      return null;
    }
  
    /**
     * Gibt alle Flugzeuge innerhalb eines Extents zurück. Dabei beachtet wird der
     * ausgewählte Feeder und der gewünschte Zeitpunkt des letzten Updates der
     * Flugzeuge
     *
     * @param lomin          lower bound for the longitude in decimal degrees
     * @param lamin          lower bound for the latitude in decimal degrees
     * @param lomax          upper bound for the longitude in decimal degrees
     * @param lamax          upper bound for the latitude in decimal degrees
     * @param selectedFeeder Ausgewählter Feeder (oder keiner)
     * @param startTime      Zeitpunkt des letzten Updates
     * @return List<Aircraft>
     */
    public List<Aircraft> getPlanes(Double lomin, Double lamin, Double lomax, Double lamax,
                                    String selectedFeeder, long startTime, String markedHex, boolean showOnlyMilitary) {
      List<Aircraft> listAircraftRaw;
      listAircraftRaw = getPlanesWithoutExtent(lomin, lamin, lomax, lamax, selectedFeeder, startTime, markedHex, showOnlyMilitary);
  
      try {
        if (markedHex != null && !markedHex.isEmpty()) {
          final Aircraft markedAircraft = aircraftRepository.findByHex(markedHex);
          if (markedAircraft != null && listAircraftRaw != null) {
            listAircraftRaw.add(markedAircraft);
          } else if (markedAircraft != null) {
            listAircraftRaw = new ArrayList<>();
            listAircraftRaw.add(markedAircraft);
          }
        }
  
        if (listAircraftRaw != null && showOnlyMilitary)
          listAircraftRaw = listAircraftRaw.stream().filter(a -> a.getIsMilitary() != null).collect(Collectors.toList());
  
      } catch (Exception e) {
        log.error("Server - DB error when fetching planes : Exception = " + e);
      }
  
      return listAircraftRaw;
    }
  
    private List<Aircraft> getPlanesWithoutExtent(double lomin, double lamin, double lomax, double lamax, String selectedFeeder, long startTime, String markedHex, boolean showOnlyMilitary) {
      List<Aircraft> listAircraftRaw = null;
  
      try {
        if (selectedFeeder != null && !selectedFeeder.isEmpty()) {
          // Gebe Flugzeuge eines bestimmten Feeders zurück
          listAircraftRaw = aircraftRepository.findAllByLastUpdateAndFeederWithoutExtent(
              startTime, selectedFeeder);
        }
      } catch (Exception e) {
        log.error("Server - DB error when fetching planes : Exception = " + e);
      }
  
      return listAircraftRaw;
    }
  }
  