/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.ide.ui.laf.darcula.ui;

import com.intellij.openapi.util.registry.Registry;
import com.intellij.ui.Gray;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.WindowMoveListener;
import com.intellij.ui.WindowResizeListener;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;

import javax.swing.*;
import javax.swing.event.MouseInputListener;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.basic.BasicRootPaneUI;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * @author Konstantin Bulenkov
 */
public class DarculaRootPaneUI extends BasicRootPaneUI {
  private Window myWindow;

  private JComponent myTitlePane;

  private MouseInputListener myMouseInputListener;

  private MouseInputListener myTitleMouseInputListener;

  private LayoutManager myLayoutManager;

  private LayoutManager myOldLayout;

  protected JRootPane myRootPane;

  protected WindowListener myWindowListener;

  protected Window myCurrentWindow;

  protected HierarchyListener myHierarchyListener;

  protected ComponentListener myWindowComponentListener;

  protected GraphicsConfiguration currentRootPaneGC;

  protected PropertyChangeListener myPropertyChangeListener;

  @SuppressWarnings("MethodOverridesStaticMethodOfSuperclass")
  public static ComponentUI createUI(JComponent comp) {
    return isCustomDecoration() ? new DarculaRootPaneUI() : createDefaultWindowsRootPaneUI();
  }

  private static ComponentUI createDefaultWindowsRootPaneUI() {
    try {
      return (ComponentUI)Class.forName("com.sun.java.swing.plaf.windows.WindowsRootPaneUI").newInstance();
    } catch (Exception e) {
      return new BasicRootPaneUI();
    }
  }

  @Override
  public void installUI(JComponent c) {
    super.installUI(c);

    if (isCustomDecoration()) {
      myRootPane = (JRootPane)c;
      int style = myRootPane.getWindowDecorationStyle();
      if (style != JRootPane.NONE) {
        installClientDecorations(myRootPane);
      }
    }
  }

  public void installMenuBar(JMenuBar menu) {
    if (menu != null && isCustomDecoration()) {
      getTitlePane().add(menu);
    }
  }

  @Override
  public void uninstallUI(JComponent c) {
    super.uninstallUI(c);

    if (isCustomDecoration()) {
      uninstallClientDecorations(myRootPane);
      myLayoutManager = null;
      myMouseInputListener = null;
      myRootPane = null;
    }
  }

  private static boolean isCustomDecoration() {
    return Registry.is("ide.win.frame.decoration");
  }

  public void installBorder(JRootPane root) {
    int style = root.getWindowDecorationStyle();

    if (style == JRootPane.NONE) {
      LookAndFeel.uninstallBorder(root);
    }
    else {
      root.setBorder(JBUI.Borders.customLine(Gray._73, 1, 1, 1, 1));
      //LookAndFeel.installBorder(root, "RootPane.border");
    }
  }

  private static void uninstallBorder(JRootPane root) {
    LookAndFeel.uninstallBorder(root);
  }

  private void installWindowListeners(JRootPane root, Component parent) {
    myWindow = parent == null ? null : UIUtil.getWindow(parent);

    if (myWindow != null) {
      if (myMouseInputListener == null) {
        //noinspection UseDPIAwareInsets
        myMouseInputListener = new WindowResizeListener(parent, JBUI.insets(11), null) {
          @Override
          protected Insets getResizeOffset(Component view) {
            return getResizeBorder(view);
          }
        };
      }
      myWindow.addMouseListener(myMouseInputListener);
      myWindow.addMouseMotionListener(myMouseInputListener);

      if (myTitlePane != null) {
        if (myTitleMouseInputListener == null) {
          myTitleMouseInputListener = new WindowMoveListener(myTitlePane) {
            @Override
            protected boolean isDisabled(Component view) {
              if (view instanceof RootPaneContainer) {
                RootPaneContainer container = (RootPaneContainer)view;
                JRootPane pane = container.getRootPane();
                if (pane != null && JRootPane.NONE == pane.getWindowDecorationStyle()) return true;
              }
              return super.isDisabled(view);
            }
          };
        }
        myTitlePane.addMouseMotionListener(myTitleMouseInputListener);
        myTitlePane.addMouseListener(myTitleMouseInputListener);
      }
      setMaximized();
    }
  }

  private void uninstallWindowListeners(JRootPane root) {
    if (myWindow != null) {
      myWindow.removeMouseListener(myMouseInputListener);
      myWindow.removeMouseMotionListener(myMouseInputListener);
    }
    if (myTitlePane != null) {
      myTitlePane.removeMouseListener(myTitleMouseInputListener);
      myTitlePane.removeMouseMotionListener(myTitleMouseInputListener);
    }
  }

  private void installLayout(JRootPane root) {
    if (myLayoutManager == null) {
      myLayoutManager = createLayoutManager();
    }
    myOldLayout = root.getLayout();
    root.setLayout(myLayoutManager);
  }

  @Override
  protected void installListeners(final JRootPane root) {
    super.installListeners(root);

    myHierarchyListener = new HierarchyListener() {
      public void hierarchyChanged(HierarchyEvent e) {
        Component parent = root.getParent();
        if (parent == null) {
          return;
        }
        if (parent.getClass().getName().startsWith("org.jdesktop.jdic.tray")
            || (parent.getClass().getName().compareTo("javax.swing.Popup$HeavyWeightWindow") == 0)) {

          //noinspection SSBasedInspection
          SwingUtilities.invokeLater(() -> {
            root.removeHierarchyListener(myHierarchyListener);
            myHierarchyListener = null;
          });
        }

        Window currWindow = UIUtil.getWindow(parent);
        if (myWindowListener != null) {
          myCurrentWindow.removeWindowListener(myWindowListener);
          myWindowListener = null;
        }
        if (myWindowComponentListener != null) {
          myCurrentWindow.removeComponentListener(myWindowComponentListener);
          myWindowComponentListener = null;
        }
        if (currWindow != null) {
          myWindowListener = new WindowAdapter() {
            @Override
            public void windowClosed(WindowEvent e) {
              //noinspection SSBasedInspection
              SwingUtilities.invokeLater(() -> {
                Frame[] frames = Frame.getFrames();
                for (Frame frame : frames) {
                  if (frame.isDisplayable()) {
                    return;
                  }
                }
              });
            }
          };

          if (!(parent instanceof JInternalFrame)) {
            currWindow.addWindowListener(myWindowListener);
          }

          myWindowComponentListener = new ComponentAdapter() {
            @Override
            public void componentMoved(ComponentEvent e) {
              processNewPosition();
            }

            @Override
            public void componentResized(ComponentEvent e) {
              processNewPosition();
            }

            private void processNewPosition() {
              //noinspection SSBasedInspection
              SwingUtilities.invokeLater(() -> {
                if (myWindow == null) {
                  return;
                }

                if (!myWindow.isShowing() || !myWindow.isDisplayable()) {
                  currentRootPaneGC = null;
                  return;
                }

                GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
                GraphicsDevice[] gds = ge.getScreenDevices();
                if (gds.length == 1) {
                  return;
                }
                Point midLoc = new Point(myWindow.getLocationOnScreen().x + myWindow.getWidth() / 2,
                                         myWindow.getLocationOnScreen().y + myWindow.getHeight() / 2);

                for (GraphicsDevice gd : gds) {
                  GraphicsConfiguration gc = gd.getDefaultConfiguration();
                  Rectangle bounds = gc.getBounds();
                  if (bounds.contains(midLoc)) {
                    if (gc != currentRootPaneGC) {
                      currentRootPaneGC = gc;
                      setMaximized();
                    }
                    break;
                  }
                }
              });
            }
          };

          if (parent instanceof JFrame) {
            currWindow.addComponentListener(myWindowComponentListener);
          }

          myWindow = currWindow;
        }
        myCurrentWindow = currWindow;
      }
    };
    root.addHierarchyListener(myHierarchyListener);
    root.addPropertyChangeListener(myPropertyChangeListener);
  }

  @Override
  protected void uninstallListeners(JRootPane root) {
    if (myWindow != null) {
      myWindow.removeWindowListener(myWindowListener);
      myWindowListener = null;
      myWindow.removeComponentListener(myWindowComponentListener);
      myWindowComponentListener = null;
    }
    root.removeHierarchyListener(myHierarchyListener);
    myHierarchyListener = null;

    root.removePropertyChangeListener(myPropertyChangeListener);
    myPropertyChangeListener = null;

    super.uninstallListeners(root);
  }

  /**
   * Uninstalls the previously installed <code>LayoutManager</code>.
   *
   * @param root Root pane.
   */
  private void uninstallLayout(JRootPane root) {
    if (myOldLayout != null) {
      root.setLayout(myOldLayout);
      myOldLayout = null;
    }
  }

  /**
   * Installs the necessary state onto the JRootPane to render client
   * decorations. This is ONLY invoked if the <code>JRootPane</code> has a
   * decoration style other than <code>JRootPane.NONE</code>.
   *
   * @param root Root pane.
   */
  private void installClientDecorations(JRootPane root) {
    installBorder(root);

    JComponent titlePane = createTitlePane(root);

    setTitlePane(root, titlePane);
    installWindowListeners(root, root.getParent());
    installLayout(root);
    if (myWindow != null) {
      root.revalidate();
      root.repaint();
    }
  }

  private void uninstallClientDecorations(JRootPane root) {
    uninstallBorder(root);
    uninstallWindowListeners(root);
    setTitlePane(root, null);
    uninstallLayout(root);
    int style = root.getWindowDecorationStyle();
    if (style == JRootPane.NONE) {
      root.repaint();
      root.revalidate();
    }

    if (myWindow != null) {
      myWindow.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
    }
    myWindow = null;
  }

  protected JComponent createTitlePane(JRootPane root) {
    return new DarculaTitlePane(root, this);
  }

  protected LayoutManager createLayoutManager() {
    return new DarculaRootLayout();
  }

  private void setTitlePane(JRootPane root, JComponent titlePane) {
    JLayeredPane layeredPane = root.getLayeredPane();
    JComponent oldTitlePane = getTitlePane();

    if (oldTitlePane != null) {
      layeredPane.remove(oldTitlePane);
    }
    if (titlePane != null) {
      layeredPane.add(titlePane, JLayeredPane.FRAME_CONTENT_LAYER);
      titlePane.setVisible(true);
    }
    myTitlePane = titlePane;
  }

  public void setMaximized() {
    if (Registry.is("darcula.fix.maximized.frame.bounds")) return;

    Component tla = myRootPane.getTopLevelAncestor();
    GraphicsConfiguration gc = (currentRootPaneGC != null) ? currentRootPaneGC : tla.getGraphicsConfiguration();
    Rectangle screenBounds = gc.getBounds();
    screenBounds.x = 0;
    screenBounds.y = 0;
    Insets screenInsets = ScreenUtil.getScreenInsets(gc);
    Rectangle maxBounds = new Rectangle(screenBounds.x + screenInsets.left,
                                        screenBounds.y + screenInsets.top,
                                        screenBounds.width - (screenInsets.left + screenInsets.right),
                                        screenBounds.height - (screenInsets.top + screenInsets.bottom));
    if (tla instanceof JFrame) {
      ((JFrame)tla).setMaximizedBounds(maxBounds);
    }
  }

  public JComponent getTitlePane() {
    return myTitlePane;
  }

  protected JRootPane getRootPane() {
    return myRootPane;
  }

  @Override
  public void propertyChange(PropertyChangeEvent e) {
    super.propertyChange(e);

    String propertyName = e.getPropertyName();
    if (propertyName == null) {
      return;
    }

    if (propertyName.equals("windowDecorationStyle")) {
      JRootPane root = (JRootPane)e.getSource();
      int style = root.getWindowDecorationStyle();

      uninstallClientDecorations(root);
      if (style != JRootPane.NONE) {
        installClientDecorations(root);
      }
    }
    if (propertyName.equals("ancestor")) {
      uninstallWindowListeners(myRootPane);
      if (((JRootPane)e.getSource()).getWindowDecorationStyle() != JRootPane.NONE) {
        installWindowListeners(myRootPane, myRootPane.getParent());
      }
    }
  }

  protected class DarculaRootLayout implements LayoutManager2 {
    public Dimension preferredLayoutSize(Container parent) {
      Dimension cpd, mbd, tpd;
      int cpWidth = 0;
      int cpHeight = 0;
      int mbWidth = 0;
      int mbHeight = 0;
      int tpWidth = 0;
      int tpHeight = 0;
      Insets i = parent.getInsets();
      JRootPane root = (JRootPane)parent;

      if (root.getContentPane() != null) {
        cpd = root.getContentPane().getPreferredSize();
      }
      else {
        cpd = root.getSize();
      }
      if (cpd != null) {
        cpWidth = cpd.width;
        cpHeight = cpd.height;
      }

      if (root.getJMenuBar() != null) {
        mbd = root.getJMenuBar().getPreferredSize();
        if (mbd != null) {
          mbWidth = mbd.width;
          mbHeight = mbd.height;
        }
      }

      if ((root.getWindowDecorationStyle() != JRootPane.NONE)
          && (root.getUI() instanceof DarculaRootPaneUI)) {
        JComponent titlePane = ((DarculaRootPaneUI)root.getUI()).getTitlePane();
        if (titlePane != null) {
          tpd = titlePane.getPreferredSize();
          if (tpd != null) {
            tpWidth = tpd.width;
            tpHeight = tpd.height;
          }
        }
      }

      return new Dimension(max(cpWidth, mbWidth, tpWidth) + i.left + i.right, cpHeight + mbHeight + tpHeight + i.top + i.bottom);
    }

    public Dimension minimumLayoutSize(Container parent) {
      Dimension cpd, mbd, tpd;
      int cpWidth = 0;
      int cpHeight = 0;
      int mbWidth = 0;
      int mbHeight = 0;
      int tpWidth = 0;
      int tpHeight = 0;
      Insets i = parent.getInsets();
      JRootPane root = (JRootPane)parent;

      if (root.getContentPane() != null) {
        cpd = root.getContentPane().getMinimumSize();
      }
      else {
        cpd = root.getSize();
      }
      if (cpd != null) {
        cpWidth = cpd.width;
        cpHeight = cpd.height;
      }

      if (root.getJMenuBar() != null) {
        mbd = root.getJMenuBar().getMinimumSize();
        if (mbd != null) {
          mbWidth = mbd.width;
          mbHeight = mbd.height;
        }
      }
      if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof DarculaRootPaneUI)) {
        JComponent titlePane = ((DarculaRootPaneUI)root.getUI()).getTitlePane();
        if (titlePane != null) {
          tpd = titlePane.getMinimumSize();
          if (tpd != null) {
            tpWidth = tpd.width;
            tpHeight = tpd.height;
          }
        }
      }

      return new Dimension(max(cpWidth, mbWidth, tpWidth) + i.left + i.right, cpHeight + mbHeight + tpHeight + i.top + i.bottom);
    }

    public Dimension maximumLayoutSize(Container target) {
      Dimension cpd, mbd, tpd;
      int cpWidth = Integer.MAX_VALUE;
      int cpHeight = Integer.MAX_VALUE;
      int mbWidth = Integer.MAX_VALUE;
      int mbHeight = Integer.MAX_VALUE;
      int tpWidth = Integer.MAX_VALUE;
      int tpHeight = Integer.MAX_VALUE;
      Insets i = target.getInsets();
      JRootPane root = (JRootPane)target;

      if (root.getContentPane() != null) {
        cpd = root.getContentPane().getMaximumSize();
        if (cpd != null) {
          cpWidth = cpd.width;
          cpHeight = cpd.height;
        }
      }

      if (root.getJMenuBar() != null) {
        mbd = root.getJMenuBar().getMaximumSize();
        if (mbd != null) {
          mbWidth = mbd.width;
          mbHeight = mbd.height;
        }
      }

      if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof DarculaRootPaneUI)) {
        JComponent titlePane = ((DarculaRootPaneUI)root.getUI()).getTitlePane();
        if (titlePane != null) {
          tpd = titlePane.getMaximumSize();
          if (tpd != null) {
            tpWidth = tpd.width;
            tpHeight = tpd.height;
          }
        }
      }

      int maxHeight = max(cpHeight, mbHeight, tpHeight);
      if (maxHeight != Integer.MAX_VALUE) {
        maxHeight = cpHeight + mbHeight + tpHeight + i.top + i.bottom;
      }

      int maxWidth = max(cpWidth, mbWidth, tpWidth);

      if (maxWidth != Integer.MAX_VALUE) {
        maxWidth += i.left + i.right;
      }

      return new Dimension(maxWidth, maxHeight);
    }

    public void layoutContainer(Container parent) {
      JRootPane root = (JRootPane)parent;
      Rectangle b = root.getBounds();
      Insets i = root.getInsets();
      int nextY = 0;
      int w = b.width - i.right - i.left;
      int h = b.height - i.top - i.bottom;

      if (root.getLayeredPane() != null) {
        root.getLayeredPane().setBounds(i.left, i.top, w, h);
      }
      if (root.getGlassPane() != null) {
        root.getGlassPane().setBounds(i.left, i.top, w, h);
      }

      if ((root.getWindowDecorationStyle() != JRootPane.NONE) && (root.getUI() instanceof DarculaRootPaneUI)) {
        JComponent titlePane = ((DarculaRootPaneUI)root.getUI()).getTitlePane();
        if (titlePane != null) {
          Dimension tpd = titlePane.getPreferredSize();
          if (tpd != null) {
            int tpHeight = tpd.height;
            titlePane.setBounds(0, 0, w, tpHeight);
            nextY += tpHeight;
          }
        }
      }
      if (root.getJMenuBar() != null) {
        Dimension mbd = root.getJMenuBar().getPreferredSize();
        root.getJMenuBar().setBounds(0, nextY, w, mbd.height);
        nextY += mbd.height;
      }
      if (root.getContentPane() != null) {

        root.getContentPane().setBounds(0, nextY, w, h < nextY ? 0 : h - nextY);
      }
    }

    public void addLayoutComponent(String name, Component comp) {
    }

    public void removeLayoutComponent(Component comp) {
    }

    public void addLayoutComponent(Component comp, Object constraints) {
    }

    public float getLayoutAlignmentX(Container target) {
      return 0.0f;
    }

    public float getLayoutAlignmentY(Container target) {
      return 0.0f;
    }

    public void invalidateLayout(Container target) {
    }
  }

  private static int max(int a, int b, int...others) {
    int result = Math.max(a, b);
    for (int other : others) {
      result = Math.max(result, other);
    }
    return result;
  }
}