public class Mephisto3 extends Application {
  public static final int WIDTH = Callete.getConfiguration().getInt("width", 700);
  public static final int HEIGHT = Callete.getConfiguration().getInt("height", 395);
  public static StackPane rootStack;

  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(final Stage primaryStage) {
    Callete.getGPIOService().setSimulationMode(SystemUtils.isWindows());

    //force rendering of small fonts
    System.setProperty("prism.lcdtext", "false");

    //apply new image cache dir
    if(!SystemUtils.isWindows()) {
      AlbumCoverCache.setCacheDir(new File("../image_cache/"));
    }

    //create root component with background
    rootStack = new StackPane();
    rootStack.getChildren().add(new Center());

    Scene scene = new Scene(rootStack, (double) WIDTH, (double) HEIGHT);
    scene.getStylesheets().add(ResourceLoader.getResource("theme.css"));
    primaryStage.setScene(scene);
    primaryStage.getScene().setRoot(rootStack);

    primaryStage.addEventFilter(KeyEvent.KEY_PRESSED, new Mephisto3KeyEventFilter());
    addDisposeListener(primaryStage);

    if(!SystemUtils.isWindows()) {
      primaryStage.initStyle(StageStyle.UNDECORATED);
    }
    
    int x = Callete.getConfiguration().getInt("position.x", 0);
    int y = Callete.getConfiguration().getInt("position.y", 0);
    if(x == 0 && y == 0) {
      primaryStage.centerOnScreen();
    }
    else {
      primaryStage.setX(x);
      primaryStage.setY(y);
    }

    //finally show the stage
    primaryStage.show();    
    
    Center.getInstance().loadServices();
  }

  //--------------------------- Helper --------------------------------------------

  private static void addDisposeListener(Stage primaryStage) {
    //ensures that the process is terminated on window dispose
    primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
      @Override
      public void handle(WindowEvent windowEvent) {
        Platform.exit();
        System.exit(0);
      }
    });
  }
} public interface ControlListener {

  /**
   * Fired when the user has inputted something.
   * The event type contains the type of input event.
   */
  void controlEvent(ServiceControlEvent event);
} public class ServiceControlEvent {
  public static enum EVENT_TYPE {PUSH, LONG_PUSH, NEXT, PREVIOUS}

  ;

  private ServiceState state;
  private EVENT_TYPE eventType;

  public ServiceControlEvent(EVENT_TYPE eventType, ServiceState state) {
    this.state = state;
    this.eventType = eventType;
  }

  public EVENT_TYPE getEventType() {
    return eventType;
  }

  public ServiceState getServiceState() {
    return state;
  }
} public class ServiceController {
  private final static Logger LOG = LoggerFactory.getLogger(ServiceController.class);
  
  public final static String SERVICE_NAME_RADIO = "Radio";
  public final static String SERVICE_NAME_WEATHER = "Wetter";
  public final static String SERVICE_NAME_MUSIC = "Musik";
  public final static String SERVICE_NAME_MP3 = "MP3s";
  public final static String SERVICE_NAME_SETTINGS = "System";

  public static final String ROTARY_ENCODER_PUSH_BUTTON_NAME = "Rotary Push";
  public static final String ROTARY_ENCODER_NAME = "Rotary Encoder";

  private static ServiceController instance;

  private List<ServiceChangeListener> serviceChangeListeners = Collections.synchronizedList(new ArrayList<>());
  private List<ControlListener> controlListeners = Collections.synchronizedList(new ArrayList<>());
  private ServiceState serviceState = new ServiceState();
  private boolean controlEnabled = false;

  public static ServiceController getInstance() {
    if(instance == null) {
      instance = new ServiceController();
      instance.initGPIO();
      instance.initServiceState();
    }
    return instance;
  }

  public void addServiceChangeListener(ServiceChangeListener listener) {
    this.serviceChangeListeners.add(listener);
  }

  public void addControlListener(ControlListener listener) {
    this.controlListeners.add(listener);
  }

  public void removeControlListener(ControlListener listener) {
    this.controlListeners.remove(listener);
  }

  /**
   * Activates the UI for the given service.
   *
   * @param service the service to activate.
   */
  public void switchService(Service service) {
    Executor.run(new Runnable() {
      @Override
      public void run() {
        if(service.equals(Callete.getWeatherService())) {
          serviceState.setService(Callete.getWeatherService());
          serviceState.setModels(Callete.getWeatherService().getWeather());
        }
        else if(service.equals(Callete.getStreamingService())) {
          serviceState.setService(Callete.getStreamingService());
          serviceState.setModels(Callete.getStreamingService().getStreams());
        }
        else if(service.equals(Callete.getGoogleMusicService())) {
          serviceState.setService(Callete.getGoogleMusicService());
          serviceState.setModels(Callete.getGoogleMusicService().getAlbums());
        }
        else if(service.equals(Callete.getSystemService())) {
          serviceState.setService(Callete.getSystemService());
          serviceState.setModels(Collections.<ServiceModel>emptyList());
        }
        else if(service.equals(Callete.getNetworkMusicService())) {
          serviceState.setService(Callete.getNetworkMusicService());
          serviceState.setModels(Callete.getNetworkMusicService().getAlbums());
        }

        serviceChanged();
      }
    });
  }

  public void setControlEnabled(boolean b) {
    this.controlEnabled = b;
  }

  public ServiceState getServiceState() {
    return serviceState;
  }

  // ------------------- Helper -----------------------------------
  public void serviceChanged() {
    for(ServiceChangeListener listener : serviceChangeListeners) {
      listener.serviceChanged(serviceState);
    }
  }

  /**
   * Sets the initial service state, therefore the initial service.
   */
  private void initServiceState() {
    serviceState.setService(Callete.getStreamingService());
    List<Stream> streams = Callete.getStreamingService().getStreams();
    serviceState.setModels(streams);

    //update index with last save state
    int index = Callete.getSettings().getInt(ServiceState.SETTING_SERVICE_SELECTION, 0);
    if(index >= streams.size()) {
      index = 0;
    }
    serviceState.setServiceIndex(index);
  }

  private void initGPIO() {
    GPIOService gpioService = Callete.getGPIOService();

    int pin = Callete.getConfiguration().getInt("rotary.encoder.push.pin");
    PushButton pushButton = gpioService.connectPushButton(pin, ROTARY_ENCODER_PUSH_BUTTON_NAME);
    pushButton.addPushListener(new PushListener() {
      @Override
      public void pushed(final PushEvent pushEvent) {
        if(!controlEnabled) {
          LOG.info("Ignoring PUSH event since control is disabled.");
          return;
        }
        Platform.runLater(new Runnable() {
          @Override
          public void run() {
            ServiceControlEvent.EVENT_TYPE push = ServiceControlEvent.EVENT_TYPE.PUSH;

            if(pushEvent.isLongPush()) {
              serviceState.setModels(null);
              push = ServiceControlEvent.EVENT_TYPE.LONG_PUSH;
            }
            for(ControlListener listener : new ArrayList<>(controlListeners)) {
              listener.controlEvent(new ServiceControlEvent(push, serviceState));
            }
          }
        });
      }

      @Override
      public long getPushDebounceMillis() {
        return Callete.getConfiguration().getInt("push.debounce.millis", 700);
      }

      @Override
      public long getLongPushDebounceMillis() {
        return Callete.getConfiguration().getInt("long.push.debounce.millis", 1500);
      }
    });

    int pinA = Callete.getConfiguration().getInt("rotary.encoder.pin.a");
    int pinB = Callete.getConfiguration().getInt("rotary.encoder.pin.b");
    RotaryEncoder rotary = gpioService.connectRotaryEncoder(pinA, pinB, ROTARY_ENCODER_NAME);
    rotary.addChangeListener(new RotaryEncoderListener() {
      @Override
      public void rotated(final RotaryEncoderEvent event) {
        if(!controlEnabled) {
          return;
        }
        ServiceControlEvent.EVENT_TYPE eventType;
        if(event.rotatedLeft()) {
          eventType = ServiceControlEvent.EVENT_TYPE.PREVIOUS;
        }
        else {
          eventType = ServiceControlEvent.EVENT_TYPE.NEXT;
        }

        //skip this in service chooser mode
        if(serviceState.getModels() != null) {
          if(eventType.equals(ServiceControlEvent.EVENT_TYPE.PREVIOUS)) {
            serviceState.decrementIndex();
          }
          else if(eventType.equals(ServiceControlEvent.EVENT_TYPE.NEXT)) {
            serviceState.incrementIndex();
          }
        }


        final ServiceControlEvent serviceControlEvent = new ServiceControlEvent(eventType, serviceState);
        Platform.runLater(new Runnable() {
          @Override
          public void run() {
            for(ControlListener listener : controlListeners) {
              listener.controlEvent(serviceControlEvent);
            }
          }
        });
      }
    });
  }

  public void fireControlEvent(KeyCode code) {
    SimulatorRotaryEncoder encoder = (SimulatorRotaryEncoder) Callete.getGPIOService().getSimulatedGPIOComponent(ROTARY_ENCODER_NAME);
    SimulatorPushButton pushButton = (SimulatorPushButton) Callete.getGPIOService().getSimulatedGPIOComponent(ROTARY_ENCODER_PUSH_BUTTON_NAME);

    if(code == KeyCode.RIGHT) {
      encoder.right();
    }
    else if(code == KeyCode.LEFT) {
      encoder.left();
    }
    else if(code == KeyCode.DOWN) {
      pushButton.push(false);
    }
    else if(code == KeyCode.UP) {
      pushButton.push(true);
    }
    else if(code == KeyCode.Q || code == KeyCode.ESCAPE) {
      System.exit(0);
    }

    if(code == KeyCode.D) {
      NodeDebugger.dump(Mephisto3.rootStack);
    }
  }
} public class ServiceState {
  public final static String SETTING_SERVICE_NAME = "service.name";
  public final static String SETTING_SERVICE_SELECTION = "service.selection";

  private Service service;
  private List<?> models;
  private Map<Service, Integer> serviceIndex = new HashMap<>();

  public List<?> getModels() {
    return models;
  }

  public void setModels(List<?> models) {
    this.models = models;
  }

  public Service getService() {
    return service;
  }

  public void setService(Service service) {
    if(!serviceIndex.containsKey(service)) {
      serviceIndex.put(service, 0);
    }
    this.service = service;
  }

  /**
   * Returns the last active service model of
   * the service of this model.
   */
  public int getServiceIndex() {
    return serviceIndex.get(service);
  }

  /**
   * Remembers the last selected position for the service.
   */
  public void setServiceIndex(int pos) {
    serviceIndex.put(service, pos);
  }

  public void decrementIndex() {
    int index = getServiceIndex();
    if(index == 0) {
      index = models.size() - 1;
    }
    else {
      index--;
    }
    setServiceIndex(index);
//    System.out.println(service + " has index " + index);
  }

  public void incrementIndex() {
    int index = getServiceIndex();
    if(index == models.size() - 1) {
      index = 0;
    }
    else {
      index++;
    }
    setServiceIndex(index);
//    System.out.println(service + " has index " + index);
  }

  public Object getSelection() {
    return models.get(getServiceIndex());
  }

  public void saveState() {
    //Callete.saveSetting(SETTING_SERVICE_NAME, service.toString());
    Callete.saveSetting(SETTING_SERVICE_SELECTION, getServiceIndex());
  }
} public class GoogleMusicPanel extends ControllablePanel {
  private final static Logger LOG = LoggerFactory.getLogger(GoogleMusicPanel.class);

  public GoogleMusicPanel() {
  }

  //--------------- Helper ------------------------------------------------------

  /**
   * Google Music Login
   */
  public boolean loadGoogleMusic() {
    try {
      return Callete.getGoogleMusicService().authenticate();
    } catch (Exception e) {
      LOG.error("Error authenticating Google music: " + e.getMessage(), e);
    }
    return false;
  }
} public class NetworkMusicPanel extends ControllablePanel {
  private final static Logger LOG = LoggerFactory.getLogger(NetworkMusicPanel.class);

  public NetworkMusicPanel() {
  }

  //--------------- Helper ------------------------------------------------------

  public boolean loadMusic() {
    try {
      return Callete.getNetworkMusicService().authenticate();
    } catch (Exception e) {
      LOG.error("Error creating music dict: " + e.getMessage(), e);
    }
    return false;
  }
} public class StreamsController extends ControllablePanel implements PlaylistMetaDataChangeListener {
  private final static Logger LOG = LoggerFactory.getLogger(StreamsController.class);

  private Stream selectedStream;
  private Stream activeStream;

  private PlaylistMetaData currentMetaData;
  private StreamsUI streamsUI;

  public StreamsController() {
    //initial station selection
    activeStream = (Stream) ServiceController.getInstance().getServiceState().getSelection();
    selectedStream = activeStream;

    //create the basic UI panel
    streamsUI = new StreamsUI(this, activeStream);
    getChildren().add(streamsUI);
  }

  //--------------------------- UI control ----------------------------------------------

  @Override
  public void pushed(ServiceState serviceState) {
    //reset meta data status
    currentMetaData = null;

    //save last selected state first
    Stream selection = (Stream) serviceState.getSelection();

    //check if the push button was pressed for the current selection, select next station then
    if(selection.equals(activeStream)) {
      serviceState.incrementIndex();
      activeStream = (Stream) serviceState.getSelection();
      selectedStream = activeStream;
      serviceState.saveState();
      
      streamsUI.updateSelection();

      //well, play the selected stream
      playActiveStream();
      return;
    }
    activeStream = selection;
    serviceState.saveState();
    
    playActiveStream();

  }

  @Override
  protected void serviceStateChanged(ServiceState serviceState) {
    selectedStream = (Stream) serviceState.getSelection();
    updateUI(null);
  }

  //--------------------------- Event listeners  ----------------------------------------------

  @Override
  public void updateMetaData(final PlaylistMetaData metaData) {
    if(!metaData.getItem().equals(activeStream)) {
      return;
    }
    //store data for re-selection
    currentMetaData = metaData;

    //apply the labels if the current stream is the active stream
    updateUI(null);
  }

  // -------------------- Overridden UI states -----------------------------

  @Override
  public void showPanel() {
    startStreaming();
    Callete.getMusicPlayer().getPlaylist().addMetaDataChangeListener(this);
    streamsUI.showControl();
    super.showPanel();
  }

  @Override
  public void hidePanel() {
    Callete.getMusicPlayer().getPlaylist().removeMetaDataChangeListener(this);
    streamsUI.hideControl();
    super.hidePanel();
  }

  // ------------------- Helper --------------------------------------------

  private void playActiveStream() {
    //well, play the selected stream
    final MusicPlayerPlaylist playlist = Callete.getMusicPlayer().getPlaylist();
    playlist.setActiveItem(activeStream);
    Callete.getMusicPlayer().play();

    //update the UI
    updateUI(activeStream);
  }

  private void updateUI(final Stream stream) {
    //if a stream is given, it's from a new selection, so activate it.
    if(stream != null) {
      streamsUI.activateStream(stream);
    }
    else if(selectedStream == activeStream) {
      streamsUI.selectActiveStream(activeStream, currentMetaData);
    }
    else {
      streamsUI.selectStream();
    }
  }

  /**
   * No matter if the UI is build yet, start playing the stream.
   */
  private void startStreaming() {
    final MusicPlayerPlaylist playlist = Callete.getMusicPlayer().getPlaylist();
    playlist.setActiveItem(activeStream);

    Platform.runLater(new Runnable() {
      @Override
      public void run() {
        streamsUI.reset();
      }
    });

    Callete.getMusicPlayer().play();
    LOG.info("Starting playback of last stream selection: " + activeStream);
  }

} public class SystemPanel extends ControllablePanel {
  private final static Logger LOG = LoggerFactory.getLogger(SystemPanel.class);

  private ProgressIndicator diskSpace;
  private Text freeMem;
  private Text usedMem;

  private ProgressIndicator heapSpace;
  private Text freeDisk;
  private Text usedDisk;

  private Timer refreshTimer;

  private ProgressIndicator cpuUsage;

  public SystemPanel() {
    setMinWidth(Mephisto3.WIDTH);
  }

  @Override
  public void pushed(ServiceState serviceState) {
    ServiceController.getInstance().fireControlEvent(KeyCode.UP);
  }

  @Override
  public void showPanel() {
    if(diskSpace == null) {
      buildUI();
    }

    super.showPanel();
    this.refreshTimer = new Timer();
    this.refreshTimer.schedule(new TimerTask() {
      @Override
      public void run() {
        Platform.runLater(new Runnable() {
          @Override
          public void run() {
            refresh();
          }
        });
      }
    }, 1000, 3000);
  }

  @Override
  public void hidePanel() {
    super.hidePanel();
    this.refreshTimer.cancel();
    this.refreshTimer.purge();
  }

  //--------------- Helper ------------------------------------------------------

  private void refresh() {
    final SystemService systemService = Callete.getSystemService();
    usedDisk.setText(SystemUtils.humanReadableByteCount(systemService.getUsedDiskSpace()));
    freeDisk.setText(SystemUtils.humanReadableByteCount(systemService.getAvailableDiskSpace()));
    double usage = (systemService.getAvailableDiskSpace() * 100 / systemService.getUsedDiskSpace()) / new Double(100);
    diskSpace.setProgress((1 - usage));

    usedMem.setText(SystemUtils.humanReadableByteCount(systemService.getFreeMemory()));
    freeMem.setText(SystemUtils.humanReadableByteCount(systemService.getTotalMemory()));

    double memoryUsage = (systemService.getFreeMemory() * 100 / systemService.getTotalMemory()) / new Double(100);
    heapSpace.setProgress(memoryUsage);

    double cpuPercentage = systemService.getCpuUsage();
    if(!Double.isNaN(cpuPercentage)) {
      double v = cpuPercentage / 100;
      if(v == 1) {
        v = 9.99;
      }
      cpuUsage.setProgress(v);
    }
  }

  private Text createInfo(Pane parent, String key, String value) {
    HBox infoBox = new HBox(10);
    ComponentUtil.createText(key, "system-key", infoBox);
    Text valueText = ComponentUtil.createText(value, "default-16", infoBox);
    parent.getChildren().add(infoBox);
    return valueText;
  }

  private void buildUI() {
    LOG.info("Creating System Panel UI");
    final SystemService systemService = Callete.getSystemService();


    Label titleLabel = ComponentUtil.createCustomLabel("System Information", "system-tab-title", this);
    titleLabel.setPadding(new Insets(20, 30, 0, 20));

    BorderPane root = new BorderPane();
    root.setPadding(new Insets(90, 20, 10, 20));

    HBox center = new HBox(20);
    center.setAlignment(Pos.TOP_CENTER);
    root.setCenter(center);

    //disc usage
    VBox diskSpaceBox = new VBox(20);
    diskSpaceBox.setAlignment(Pos.TOP_CENTER);
    center.getChildren().add(diskSpaceBox);

    ComponentUtil.createText("Datenspeicher", "system-title", diskSpaceBox);
    diskSpace = new ProgressIndicator();
    diskSpace.getStyleClass().add("progressIndicator");
    diskSpaceBox.getChildren().add(diskSpace);

    VBox detailsDiskSpace = new VBox(5);
    detailsDiskSpace.setAlignment(Pos.TOP_CENTER);
    detailsDiskSpace.setPadding(new Insets(10, 5, 5, 5));
    freeDisk = createInfo(detailsDiskSpace, "Frei:", SystemUtils.humanReadableByteCount(systemService.getAvailableDiskSpace()));
    usedDisk = createInfo(detailsDiskSpace, "Benutzt:", SystemUtils.humanReadableByteCount(systemService.getUsedDiskSpace()));
    diskSpaceBox.getChildren().add(detailsDiskSpace);

    //memory
    VBox memoryBox = new VBox(20);
    memoryBox.setAlignment(Pos.TOP_CENTER);
    center.getChildren().add(memoryBox);

    ComponentUtil.createText("Heap-Speicher", "system-title", memoryBox);
    heapSpace = new ProgressIndicator();
    heapSpace.getStyleClass().add("progressIndicator");
    memoryBox.getChildren().add(heapSpace);
    VBox detailsMemory = new VBox(5);
    detailsMemory.setAlignment(Pos.TOP_CENTER);
    detailsMemory.setPadding(new Insets(10, 5, 5, 5));
    freeMem = createInfo(detailsMemory, "Maximaler Heap:", SystemUtils.humanReadableByteCount(systemService.getFreeMemory()));
    usedMem = createInfo(detailsMemory, "Aktueller Heap:", SystemUtils.humanReadableByteCount(systemService.getTotalMemory()));
    memoryBox.getChildren().add(detailsMemory);


    //CPU
    VBox cpuBox = new VBox(20);
    cpuBox.setAlignment(Pos.TOP_CENTER);
    center.getChildren().add(cpuBox);
    ComponentUtil.createText("CPU", "system-title", cpuBox);

    cpuUsage = new ProgressIndicator();
    cpuUsage.getStyleClass().add("progressIndicator");
    cpuBox.getChildren().add(cpuUsage);

    VBox detailsCPU= new VBox(5);
    detailsCPU.setAlignment(Pos.TOP_CENTER);
    createInfo(detailsCPU, "Rechername:", systemService.getHostname());
    createInfo(detailsCPU, "IP Adresse:", systemService.getHostAddress());
    createInfo(detailsCPU, "Processors:", Runtime.getRuntime().availableProcessors() + "");
    cpuBox.getChildren().add(detailsCPU);

    getChildren().add(root);
  }
} public class WeatherPanel extends ControllablePanel {
  private final static Logger LOG = LoggerFactory.getLogger(WeatherPanel.class);

  private SlideshowPanel slideshowPanel = new SlideshowPanel();
  private ServiceScroller scroller = new ServiceScroller();
  private List<WeatherForecastPanel> forecastPanels = new ArrayList<>();

  private Weather activeWeather;
  private static Map<String, SlideShow> cachedSlideShows = new HashMap<>();

  private ImageView weatherIconView;
  private Label cityLabel;
  private Text degreeLabel;
  private Text tempLabel;
  private HBox busyIndicator;

  public WeatherPanel() {
    for(Weather w : Callete.getWeatherService().getWeather()) {
      getSlideShow(w.getCity());
    }
  }

  @Override
  public void pushed(ServiceState serviceState) {
    ServiceController.getInstance().fireControlEvent(KeyCode.UP);
  }

  @Override
  public void showPanel() {
    if(busyIndicator == null) {
      buildUI();
    }
    
    busyIndicator.setOpacity(1);
    super.showPanel();
    scroller.showScroller();
    activeWeather = (Weather) ServiceController.getInstance().getServiceState().getSelection();

    cityLabel.getStyleClass().clear();
    cityLabel.getStyleClass().addAll("label", "weather-city");
    cityLabel.setText(activeWeather.getCity());

    Executor.run(new Runnable() {
      @Override
      public void run() {
        String city = activeWeather.getCity();
        SlideShow slideShow = getSlideShow(city);
        slideshowPanel.setSlideShow(slideShow);
        Platform.runLater(new Runnable() {
          @Override
          public void run() {
            cityLabel.getStyleClass().clear();
            cityLabel.getStyleClass().addAll("label", "weather-city-active");
            slideshowPanel.startSlideShow();
            busyIndicator.setOpacity(0);
          }
        });
      }
    });
  }

  @Override
  public void hidePanel() {
    super.hidePanel();
    scroller.hideScroller();
    slideshowPanel.stopSlideShow();
  }

  @Override
  protected void serviceStateChanged(ServiceState serviceState) {
    this.activeWeather = (Weather) serviceState.getSelection();
    final Image image = new Image(WeatherConditionMapper.getWeatherForecastIcon(activeWeather), 55, 55, false, true);

    String city = activeWeather.getCity();
    SlideShow slideShow = getSlideShow(city);
    slideshowPanel.setSlideShow(slideShow);
    slideshowPanel.startSlideShow();

    ParallelTransition transition = new ParallelTransition(
        TransitionUtil.createOutFader(weatherIconView),
        TransitionUtil.createOutFader(cityLabel),
        TransitionUtil.createOutFader(degreeLabel),
        TransitionUtil.createOutFader(tempLabel)
    );
    transition.setOnFinished(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent actionEvent) {
        weatherIconView.setImage(image);
        cityLabel.setText(activeWeather.getCity());
        degreeLabel.setText(activeWeather.getTemp() + "°C");
        tempLabel.setText(activeWeather.getHighTemp() + "/" + activeWeather.getLowTemp() + " °C");

        ParallelTransition pt = new ParallelTransition(
            TransitionUtil.createInFader(weatherIconView),
            TransitionUtil.createInFader(cityLabel),
            TransitionUtil.createInFader(degreeLabel),
            TransitionUtil.createInFader(tempLabel)
        );
        pt.play();
      }
    });
    transition.play();

    Iterator<Weather> iterator = activeWeather.getForecast().iterator();
    iterator.next();
    Iterator<WeatherForecastPanel> panelIterator = forecastPanels.iterator();
    while(iterator.hasNext()) {
      panelIterator.next().setForecast(iterator.next());
    }
  }

  // ------------------- Helper ----------------

  private void buildUI() {
    Weather weather = Callete.getWeatherService().getWeatherAt(1);
    if(weather == null) {
      return;
    }
    
    busyIndicator = new HBox();
    ComponentUtil.createLabel("Lade Bildergallerie...", "weather-busy-indicator", busyIndicator);
    busyIndicator.setPadding(new Insets(90, 30, 0, 30));

    ProgressIndicator pi = new ProgressIndicator();
    pi.setMaxHeight(25);
    busyIndicator.getChildren().add(pi);

    getChildren().add(busyIndicator);

    getChildren().add(slideshowPanel);

    VBox root = new VBox();
    cityLabel = ComponentUtil.createCustomLabel("", "weather-city", root);
    cityLabel.setPadding(new Insets(20, 30, 0, 30));

    VBox spacer = new VBox();
    spacer.setMinHeight(205);
    root.getChildren().add(spacer);

    root.getChildren().add(scroller);

    StackPane status = new StackPane();
    status.setMinHeight(120);
    status.getStyleClass().add("weather-status-panel");

    HBox weatherStatus = new HBox();
    status.getChildren().add(weatherStatus);
    weatherStatus.setPadding(new Insets(10, 0, 0, 0));

    VBox locationDetailsBox = new VBox(0);
    locationDetailsBox.setMinWidth(300);
    locationDetailsBox.setAlignment(Pos.TOP_CENTER);
    weatherStatus.getChildren().add(locationDetailsBox);

    HBox row1 = new HBox(25);
    row1.setAlignment(Pos.TOP_CENTER);
    locationDetailsBox.getChildren().addAll(row1);

    degreeLabel = ComponentUtil.createText(weather.getTemp() + "°C", "weather-degree", row1);
    VBox imgWrapper = new VBox();
    imgWrapper.setAlignment(Pos.TOP_CENTER);
    imgWrapper.setPadding(new Insets(14, 0, 0, 0));
    weatherIconView = new ImageView(new Image(WeatherConditionMapper.getWeatherForecastIcon(weather), 55, 55, false, true));
    imgWrapper.getChildren().add(weatherIconView);
    tempLabel = ComponentUtil.createText(weather.getHighTemp() + "/" + weather.getLowTemp() + " °C", "forecast-temp", imgWrapper);
    row1.getChildren().add(imgWrapper);

    Iterator<Weather> iterator = weather.getForecast().iterator();
    iterator.next();
    while(iterator.hasNext()) {
      WeatherForecastPanel forecastPanel = new WeatherForecastPanel(iterator.next());
      forecastPanel.setMinWidth(95);
      weatherStatus.getChildren().add(forecastPanel);
      forecastPanels.add(forecastPanel);
    }

    root.getChildren().add(status);
    getChildren().add(root);
  }

  private SlideShow getSlideShow(String city) {
    if(!cachedSlideShows.containsKey(city)) {
      LOG.info("Loading image gallery for " + city + " into cache.");
      cachedSlideShows.put(city, new SlideShowImpl(new File("slideshows/" + city.toLowerCase() + "/"), true));
    }
    return cachedSlideShows.get(city);
  }
} public class Executor {

  public static Thread run(final Runnable r) {
    Thread t = new Thread(r);
    t.start();
    return t;
  }
} public class TransitionUtil {

  public static final int FADER_DEFAULT = 200;

  /**
   * Creates a fade in effect without playing it
   */
  public static FadeTransition createInFader(Node node) {
    return createInFader(node, FADER_DEFAULT);
  }

  public static FadeTransition createInFader(Node node, long duration) {
    FadeTransition fadeTransition = new FadeTransition(Duration.millis(duration), node);
    fadeTransition.setFromValue(0);
    fadeTransition.setToValue(1);
    applyDefaults(node, fadeTransition);
    return fadeTransition;
  }

  public static FadeTransition createOutFader(Node node, long duration) {
    FadeTransition fadeTransition = new FadeTransition(Duration.millis(duration), node);
    fadeTransition.setFromValue(1);
    fadeTransition.setToValue(0);
    applyDefaults(node, fadeTransition);
    return fadeTransition;
  }

  public static FadeTransition createOutFader(Node node) {
    FadeTransition fadeTransition = new FadeTransition(Duration.millis(FADER_DEFAULT), node);
    fadeTransition.setFromValue(1);
    fadeTransition.setToValue(0);
    applyDefaults(node, fadeTransition);
    return fadeTransition;
  }

  public static ScaleTransition createScaler(Node node, double factor) {
    ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(ServiceChooser.SCROLL_DURATION), node);
    scaleTransition.setToX(factor);
    scaleTransition.setToY(factor);
    applyDefaults(node, scaleTransition);
    return scaleTransition;
  }

  /**
   * Creates a blink out effect without playing it
   */
  public static FadeTransition createBlink(Node node) {
    FadeTransition fadeTransition = new FadeTransition(Duration.millis(80), node);
    fadeTransition.setFromValue(0.1);
    fadeTransition.setCycleCount(3);
    fadeTransition.setToValue(1);
    applyDefaults(node, fadeTransition);
    return fadeTransition;
  }

  /**
   * Expands the width of the given Pane to the target width.
   *
   * @param node   the pane to expand
   * @param offset the offset width of the pane
   */
  public static Transition createMaxWidthTransition(final Pane node, final int originalWidth, final int offset, final boolean increase) {
    final Transition transition = new Transition() {
      {
        setCycleDuration(Duration.millis(200));
      }

      @Override
      protected void interpolate(double v) {
        double percent = v * 100;
        double newWidthOffset = offset * percent / 100;

        if(increase) {
          node.setMinWidth(originalWidth + newWidthOffset);
        }
        else {
          node.setMinWidth(originalWidth - newWidthOffset);
        }
      }
    };
    transition.setAutoReverse(false);
    transition.setInterpolator(Interpolator.EASE_BOTH);
    applyDefaults(node, transition);
    return transition;
  }

  /**
   * Translate transition used for scrolling
   */
  public static TranslateTransition createTranslateByXTransition(Node node, long duration, int width) {
    TranslateTransition translateTransition = new TranslateTransition(Duration.millis(duration), node);
    translateTransition.setByX(width);
    applyDefaults(node, translateTransition);
    return translateTransition;
  }

  /**
   * Translate transition used for scrolling
   */
  public static TranslateTransition createTranslateByYTransition(Node node, long duration, int height) {
    TranslateTransition translateTransition = new TranslateTransition(Duration.millis(duration), node);
    translateTransition.setByY(height);
    applyDefaults(node, translateTransition);
    return translateTransition;
  }

  /**
   * Applies common settings for transitions and their nodes.
   *
   * @param node       the node the transition is working on
   * @param transition the transition to apply the defaults for
   */
  private static void applyDefaults(Node node, Transition transition) {
    transition.setAutoReverse(false);
    transition.setInterpolator(Interpolator.EASE_BOTH);

    //apply speed as default cache strategy.
    if(!node.getCacheHint().equals(CacheHint.SPEED)) {
      node.setCache(true);
      node.setCacheHint(CacheHint.SPEED);
    }
  }
}
import callete.api.Callete;
import callete.api.services.Service;
import de.calette.mephisto3.Mephisto3;
import de.calette.mephisto3.control.ControlListener;
import de.calette.mephisto3.control.ServiceControlEvent;
import de.calette.mephisto3.control.ServiceController;
import de.calette.mephisto3.control.ServiceState;
import de.calette.mephisto3.ui.google.GoogleMusicPanel;
import de.calette.mephisto3.ui.mp3.NetworkMusicPanel;
import de.calette.mephisto3.ui.radio.StreamsController;
import de.calette.mephisto3.ui.system.SystemPanel;
import de.calette.mephisto3.ui.weather.WeatherPanel;
import de.calette.mephisto3.util.Executor;
import de.calette.mephisto3.util.TransitionUtil;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
package de.calette.mephisto3.ui;



/**
 * The center panel which is a stackpane so that
 * the control overlay can be displayed.
 * <p/>
 * The center region is replaced with ControllablePanel instances.
 */
public class Center extends BorderPane implements ControlListener, ServiceChangeListener {
  private final static Logger LOG = LoggerFactory.getLogger(Center.class);

  protected StackPane stackPane;
  protected ControllablePanel activeControlPanel;
  private ControllablePanel newControlPanel;
  private ServiceChooser serviceChooser;

  private Map<Service, ControllablePanel> servicePanels = new HashMap<>();
  private static Center instance;
  
  public static Center getInstance() {
    return instance;    
  }

  public Center() {
    setMaxWidth(Mephisto3.WIDTH);