protected JPopupMenu buildSystemTrayJPopupMenu(Stage primaryStage)
      throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException,
          IllegalAccessException {
    final JPopupMenu menu = new JPopupMenu();
    final JMenuItem showMenuItem = new JMenuItem("Show");
    final JMenuItem exitMenuItem = new JMenuItem("Exit");

    menu.add(showMenuItem);
    menu.addSeparator();
    menu.add(exitMenuItem);
    showMenuItem.addActionListener(ae -> Platform.runLater(primaryStage::show));
    exitMenuItem.addActionListener(ae -> System.exit(0));

    return menu;
  }
public class DesktopClient extends Application {
  private static final Logger logger = LoggerFactory.getLogger(DesktopClient.class.getSimpleName());
  private static final String TITLE = "Qabel Desktop Client";
  private static Path DATABASE_FILE =
      Paths.get(System.getProperty("user.home")).resolve(".qabel/db.sqlite");
  private final Map<String, Object> customProperties = new HashMap<>();
  private boolean inBound;
  private LayoutView view;
  private HttpDropConnector dropConnector = new HttpDropConnector();
  private PersistenceDropMessageRepository dropMessageRepository;
  private PersistenceContactRepository contactRepository;
  private BoxVolumeFactory boxVolumeFactory;
  private Stage primaryStage;
  private MonitoredTransferManager transferManager;
  private MessageRendererFactory rendererFactory = new MessageRendererFactory();
  private BlockSharingService sharingService;
  private ExecutorService executorService = Executors.newCachedThreadPool();
  private ClientConfiguration config;
  private boolean visible = false;
  private PersistenceIdentityRepository identityRepository;

  public static void main(String[] args) throws Exception {
    if (args.length > 0) {
      DATABASE_FILE = Paths.get(args[0]);
    }
    UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
    launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    primaryStage = stage;
    setUserAgentStylesheet(STYLESHEET_MODENA);

    config = initDiContainer();

    SceneAntialiasing aa = SceneAntialiasing.BALANCED;
    primaryStage
        .getIcons()
        .setAll(
            new javafx.scene.image.Image(getClass().getResourceAsStream("/logo-invert_small.png")));
    Scene scene;

    Platform.setImplicitExit(false);
    primaryStage.setTitle(TITLE);
    scene = new Scene(new LoginView().getView(), 370, 550, true, aa);
    primaryStage.setScene(scene);

    config.addObserver(
        (o, arg) -> {
          Platform.runLater(
              () -> {
                if (arg instanceof Account) {
                  try {
                    ClientConfiguration configuration =
                        (ClientConfiguration) customProperties.get("clientConfiguration");
                    Account acc = (Account) arg;
                    AccountingServer server =
                        new AccountingServer(
                            new URI(acc.getProvider()), acc.getUser(), acc.getAuth());
                    AccountingHTTP accountingHTTP =
                        new AccountingHTTP(server, new AccountingProfile());

                    BoxVolumeFactory factory =
                        new BlockBoxVolumeFactory(
                            configuration.getDeviceId().getBytes(),
                            accountingHTTP,
                            identityRepository);
                    boxVolumeFactory = new CachedBoxVolumeFactory(factory);
                    customProperties.put("boxVolumeFactory", boxVolumeFactory);
                    sharingService = new BlockSharingService(dropMessageRepository, dropConnector);
                    customProperties.put("sharingService", sharingService);

                    new Thread(getSyncDaemon(config)).start();
                    new Thread(getDropDaemon(config)).start();
                    view = new LayoutView();
                    Parent view = this.view.getView();
                    Scene layoutScene = new Scene(view, 800, 600, true, aa);
                    Platform.runLater(() -> primaryStage.setScene(layoutScene));

                    if (config.getSelectedIdentity() != null) {
                      addShareMessageRenderer(config.getSelectedIdentity());
                    }
                  } catch (Exception e) {
                    logger.error("failed to init background services: " + e.getMessage(), e);
                    // TODO to something with the fault
                  }
                } else if (arg instanceof Identity) {
                  addShareMessageRenderer((Identity) arg);
                }
              });
        });

    dropMessageRepository.addObserver(new ShareNotificationHandler(config));

    setTrayIcon(primaryStage);

    Runtime.getRuntime()
        .addShutdownHook(
            new Thread() {
              public void run() {
                Platform.exit();
              }
            });
    primaryStage.show();
  }

  private void addShareMessageRenderer(Identity arg) {
    executorService.submit(
        () -> {
          ShareNotificationRenderer renderer =
              new ShareNotificationRenderer(
                  ((BoxVolumeFactory) customProperties.get("boxVolumeFactory"))
                      .getVolume(config.getAccount(), arg)
                      .getReadBackend(),
                  sharingService);
          rendererFactory.addRenderer(
              DropMessageRepository.PAYLOAD_TYPE_SHARE_NOTIFICATION, renderer);
        });
  }

  protected SyncDaemon getSyncDaemon(ClientConfiguration config) {
    new Thread(transferManager, "TransactionManager").start();
    return new SyncDaemon(
        config.getBoxSyncConfigs(), new DefaultSyncerFactory(boxVolumeFactory, transferManager));
  }

  protected DropDaemon getDropDaemon(ClientConfiguration config)
      throws PersistenceException, EntityNotFoundExcepion {
    return new DropDaemon(config, dropConnector, contactRepository, dropMessageRepository);
  }

  private ClientConfiguration initDiContainer() throws Exception {
    if (!Files.exists(DATABASE_FILE) && !Files.exists(DATABASE_FILE.getParent())) {
      Files.createDirectories(DATABASE_FILE.getParent());
    }

    Persistence<String> persistence =
        new SQLitePersistence(DATABASE_FILE.toFile().getAbsolutePath());
    transferManager = new MonitoredTransferManager(new DefaultTransferManager());
    customProperties.put("loadManager", transferManager);
    customProperties.put("transferManager", transferManager);
    customProperties.put("persistence", persistence);
    customProperties.put("dropUrlGenerator", new DropUrlGenerator("https://qdrop.prae.me"));
    identityRepository = new PersistenceIdentityRepository(persistence);
    customProperties.put("identityRepository", identityRepository);
    PersistenceAccountRepository accountRepository = new PersistenceAccountRepository(persistence);
    customProperties.put("accountRepository", accountRepository);
    contactRepository = new PersistenceContactRepository(persistence);
    customProperties.put("contactRepository", contactRepository);
    dropMessageRepository = new PersistenceDropMessageRepository(persistence);
    customProperties.put("dropMessageRepository", dropMessageRepository);
    customProperties.put("dropConnector", dropConnector);
    customProperties.put("reportHandler", new HockeyApp());
    ClientConfiguration clientConfig =
        getClientConfiguration(persistence, identityRepository, accountRepository);
    if (!clientConfig.hasDeviceId()) {
      clientConfig.setDeviceId(generateDeviceId());
    }
    PersistenceContactRepository contactRepository = new PersistenceContactRepository(persistence);
    customProperties.put("contactRepository", contactRepository);
    customProperties.put("clientConfiguration", clientConfig);
    customProperties.put("primaryStage", primaryStage);

    rendererFactory.setFallbackRenderer(new PlaintextMessageRenderer());
    customProperties.put("messageRendererFactory", rendererFactory);

    Injector.setConfigurationSource(customProperties::get);
    Injector.setInstanceSupplier(new RecursiveInjectionInstanceSupplier(customProperties));
    return clientConfig;
  }

  private String generateDeviceId() {
    return UUID.randomUUID().toString();
  }

  private ClientConfiguration getClientConfiguration(
      Persistence<String> persistence,
      IdentityRepository identityRepository,
      AccountRepository accountRepository) {
    ClientConfigurationRepository repo =
        new PersistenceClientConfigurationRepository(
            persistence, new ClientConfigurationFactory(), identityRepository, accountRepository);
    final ClientConfiguration config = repo.load();
    config.addObserver((o, arg) -> repo.save(config));
    return config;
  }

  private void setTrayIcon(Stage primaryStage)
      throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException,
          IllegalAccessException {

    if (!SystemTray.isSupported()) {
      return;
    }

    SystemTray sTray = SystemTray.getSystemTray();
    primaryStage.setOnCloseRequest(arg0 -> primaryStage.hide());
    JPopupMenu popup = buildSystemTrayJPopupMenu(primaryStage);
    URL url = System.class.getResource("/logo-invert_small.png");
    Image img = Toolkit.getDefaultToolkit().getImage(url);
    TrayIcon icon = new TrayIcon(img, "Qabel");

    icon.setImageAutoSize(true);
    trayIconListener(popup, icon);

    try {
      sTray.add(icon);
    } catch (AWTException e) {
      logger.error("failed to add tray icon: " + e.getMessage(), e);
    }
  }

  private void trayIconListener(final JPopupMenu popup, TrayIcon icon) {

    Timer notificationTimer = new Timer();
    notificationTimer.schedule(
        new TimerTask() {
          @Override
          public void run() {
            if (visible && !inBound) {
              visible = !visible;
              popup.setVisible(visible);
            }
            inBound = false;
          }
        },
        250,
        1500);

    icon.addMouseMotionListener(
        new MouseMotionAdapter() {
          @Override
          public void mouseMoved(MouseEvent e) {
            inBound = true;
          }
        });

    popup.addMouseMotionListener(
        new MouseMotionAdapter() {
          @Override
          public void mouseMoved(MouseEvent e) {
            inBound = true;
          }
        });

    icon.addMouseListener(
        new MouseAdapter() {

          @Override
          public void mouseReleased(MouseEvent e) {

            if (e.getButton() != MouseEvent.BUTTON1) {
              return;
            }

            Point point = e.getPoint();
            Rectangle bounds = getScreenViewableBounds(getGraphicsDeviceAt(point));
            int x = point.x;
            int y = point.y;
            if (y < bounds.y) {
              y = bounds.y;
            } else if (y > bounds.y + bounds.height) {
              y = bounds.y + bounds.height;
            }
            if (x < bounds.x) {
              x = bounds.x;
            } else if (x > bounds.x + bounds.width) {
              x = bounds.x + bounds.width;
            }

            if (x + popup.getWidth() > bounds.x + bounds.width) {
              x = (bounds.x + bounds.width) - popup.getWidth();
            }
            if (y + popup.getWidth() > bounds.y + bounds.height) {
              y = (bounds.y + bounds.height) - popup.getHeight();
            }

            visible = !visible;

            if (visible) {
              popup.setLocation(x, y);
            }
            popup.setVisible(visible);
          }

          @Override
          public void mouseExited(MouseEvent e) {
            visible = false;
            popup.setVisible(visible);
          }
        });

    popup.addMouseListener(
        new MouseAdapter() {
          @Override
          public void mouseExited(MouseEvent e) {
            if ((e.getX() < popup.getBounds().getMaxX())
                && (e.getX() >= popup.getBounds().getMinX())
                && (e.getY() < popup.getBounds().getMaxY())
                && (e.getY() >= popup.getBounds().getMinY())) {
              return;
            }
            visible = false;
            popup.setVisible(visible);
          }
        });
  }

  protected JPopupMenu buildSystemTrayJPopupMenu(Stage primaryStage)
      throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException,
          IllegalAccessException {
    final JPopupMenu menu = new JPopupMenu();
    final JMenuItem showMenuItem = new JMenuItem("Show");
    final JMenuItem exitMenuItem = new JMenuItem("Exit");

    menu.add(showMenuItem);
    menu.addSeparator();
    menu.add(exitMenuItem);
    showMenuItem.addActionListener(ae -> Platform.runLater(primaryStage::show));
    exitMenuItem.addActionListener(ae -> System.exit(0));

    return menu;
  }

  public GraphicsDevice getGraphicsDeviceAt(Point pos) {

    GraphicsDevice device = null;
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice lstGDs[] = ge.getScreenDevices();
    ArrayList<GraphicsDevice> lstDevices = new ArrayList<>(lstGDs.length);

    for (GraphicsDevice gd : lstGDs) {
      GraphicsConfiguration gc = gd.getDefaultConfiguration();
      Rectangle screenBounds = gc.getBounds();
      if (screenBounds.contains(pos)) {
        lstDevices.add(gd);
      }
    }

    if (lstDevices.size() == 1) {
      device = lstDevices.get(0);
    }
    return device;
  }

  public Rectangle getScreenViewableBounds(GraphicsDevice gd) {

    Rectangle bounds = new Rectangle(0, 0, 0, 0);

    if (gd != null) {
      GraphicsConfiguration gc = gd.getDefaultConfiguration();
      bounds = gc.getBounds();
      Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gc);

      bounds.x += insets.left;
      bounds.y += insets.top;
      bounds.width -= (insets.left + insets.right);
      bounds.height -= (insets.top + insets.bottom);
    }
    return bounds;
  }
}