/*
 * Copyright 2000-2017 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.openapi.wm.impl;

import com.intellij.ide.RemoteDesktopService;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.ThreeComponentsSplitter;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowType;
import com.intellij.openapi.wm.impl.commands.FinalizableCommand;
import com.intellij.reference.SoftReference;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.ref.Reference;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * This panel contains all tool stripes and JLayeredPanle at the center area. All tool windows are
 * located inside this layered pane.
 *
 * @author Anton Katilin
 * @author Vladimir Kondratyev
 */
public final class ToolWindowsPane extends JBLayeredPane implements UISettingsListener, Disposable {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.wm.impl.ToolWindowsPane");

  private final IdeFrameImpl myFrame;

  private final HashMap<String, StripeButton> myId2Button = new HashMap<>();
  private final HashMap<String, InternalDecorator> myId2Decorator = new HashMap<>();
  private final HashMap<StripeButton, WindowInfoImpl> myButton2Info = new HashMap<>();
  private final HashMap<InternalDecorator, WindowInfoImpl> myDecorator2Info = new HashMap<>();
  private final HashMap<String, Float> myId2SplitProportion = new HashMap<>();
  private Pair<ToolWindow, Integer> myMaximizedProportion;
  /**
   * This panel is the layered pane where all sliding tool windows are located. The DEFAULT
   * layer contains splitters. The PALETTE layer contains all sliding tool windows.
   */
  private final MyLayeredPane myLayeredPane;
  /*
   * Splitters.
   */
  private final ThreeComponentsSplitter myVerticalSplitter;
  private final ThreeComponentsSplitter myHorizontalSplitter;

  /*
   * Tool stripes.
   */
  private final Stripe myLeftStripe;
  private final Stripe myRightStripe;
  private final Stripe myBottomStripe;
  private final Stripe myTopStripe;

  private final List<Stripe> myStripes = new ArrayList<>();

  private final ToolWindowManagerImpl myManager;

  private boolean myStripesOverlayed;
  private final Disposable myDisposable = Disposer.newDisposable();
  private boolean myWidescreen;
  private boolean myLeftHorizontalSplit;
  private boolean myRightHorizontalSplit;

  ToolWindowsPane(@NotNull IdeFrameImpl frame, @NotNull ToolWindowManagerImpl manager) {
    myManager = manager;

    setOpaque(false);
    myFrame = frame;

    // Splitters
    myVerticalSplitter = new ThreeComponentsSplitter(true);
    Disposer.register(this, myVerticalSplitter);
    myVerticalSplitter.setDividerWidth(0);
    myVerticalSplitter.setDividerMouseZoneSize(Registry.intValue("ide.splitter.mouseZone"));
    myVerticalSplitter.setBackground(Color.gray);
    myHorizontalSplitter = new ThreeComponentsSplitter(false);
    Disposer.register(this, myHorizontalSplitter);
    myHorizontalSplitter.setDividerWidth(0);
    myHorizontalSplitter.setDividerMouseZoneSize(Registry.intValue("ide.splitter.mouseZone"));
    myHorizontalSplitter.setBackground(Color.gray);
    myWidescreen = UISettings.getInstance().getWideScreenSupport();
    myLeftHorizontalSplit = UISettings.getInstance().getLeftHorizontalSplit();
    myRightHorizontalSplit = UISettings.getInstance().getRightHorizontalSplit();
    if (myWidescreen) {
      myHorizontalSplitter.setInnerComponent(myVerticalSplitter);
    }
    else {
      myVerticalSplitter.setInnerComponent(myHorizontalSplitter);
    }

    // Tool stripes

    myTopStripe = new Stripe(SwingConstants.TOP, manager);
    myStripes.add(myTopStripe);
    myLeftStripe = new Stripe(SwingConstants.LEFT, manager);
    myStripes.add(myLeftStripe);
    myBottomStripe = new Stripe(SwingConstants.BOTTOM, manager);
    myStripes.add(myBottomStripe);
    myRightStripe = new Stripe(SwingConstants.RIGHT, manager);
    myStripes.add(myRightStripe);

    updateToolStripesVisibility();

    // Layered pane

    myLayeredPane = new MyLayeredPane(myWidescreen ? myHorizontalSplitter : myVerticalSplitter);

    // Compose layout

    add(myTopStripe, JLayeredPane.POPUP_LAYER);
    add(myLeftStripe, JLayeredPane.POPUP_LAYER);
    add(myBottomStripe, JLayeredPane.POPUP_LAYER);
    add(myRightStripe, JLayeredPane.POPUP_LAYER);
    add(myLayeredPane, JLayeredPane.DEFAULT_LAYER);
  }

  @Override
  public void doLayout() {
    Dimension size = getSize();
    if (!myTopStripe.isVisible()) {
      myTopStripe.setBounds(0, 0, 0, 0);
      myBottomStripe.setBounds(0, 0, 0, 0);
      myLeftStripe.setBounds(0, 0, 0, 0);
      myRightStripe.setBounds(0, 0, 0, 0);
      myLayeredPane.setBounds(0, 0, getWidth(), getHeight());
    }
    else {
      Dimension topSize = myTopStripe.getPreferredSize();
      Dimension bottomSize = myBottomStripe.getPreferredSize();
      Dimension leftSize = myLeftStripe.getPreferredSize();
      Dimension rightSize = myRightStripe.getPreferredSize();

      myTopStripe.setBounds(0, 0, size.width, topSize.height);
      myLeftStripe.setBounds(0, topSize.height, leftSize.width, size.height - topSize.height - bottomSize.height);
      myRightStripe
        .setBounds(size.width - rightSize.width, topSize.height, rightSize.width, size.height - topSize.height - bottomSize.height);
      myBottomStripe.setBounds(0, size.height - bottomSize.height, size.width, bottomSize.height);

      if (UISettings.getInstance().getHideToolStripes() || UISettings.getInstance().getPresentationMode()) {
        myLayeredPane.setBounds(0, 0, size.width, size.height);
      }
      else {
        myLayeredPane.setBounds(leftSize.width, topSize.height, size.width - leftSize.width - rightSize.width,
                                size.height - topSize.height - bottomSize.height);
      }
    }
  }

  @Override
  protected void paintChildren(Graphics g) {
    super.paintChildren(g);
  }

  /**
   * Invoked when enclosed frame is being shown.
   */
  @Override
  public final void addNotify() {
    super.addNotify();
  }

  /**
   * Invoked when enclosed frame is being disposed.
   */
  @Override
  public final void removeNotify() {
    if (ScreenUtil.isStandardAddRemoveNotify(this)) {
      Disposer.dispose(myDisposable);
    }
    super.removeNotify();
  }

  public Project getProject() {
    return myFrame.getProject();
  }

  @Override
  public final void uiSettingsChanged(final UISettings uiSettings) {
    updateToolStripesVisibility();
    updateLayout();
  }

  /**
   * Creates command which adds button into the specified tool stripe.
   * Command uses copy of passed <code>info</code> object.
   *
   * @param button         button which should be added.
   * @param info           window info for the corresponded tool window.
   * @param comparator     which is used to sort buttons within the stripe.
   * @param finishCallBack invoked when the command is completed.
   */
  @NotNull
  final FinalizableCommand createAddButtonCmd(final StripeButton button,
                                              @NotNull WindowInfoImpl info,
                                              @NotNull Comparator<StripeButton> comparator,
                                              @NotNull Runnable finishCallBack) {
    final WindowInfoImpl copiedInfo = info.copy();
    myId2Button.put(copiedInfo.getId(), button);
    myButton2Info.put(button, copiedInfo);
    return new AddToolStripeButtonCmd(button, copiedInfo, comparator, finishCallBack);
  }

  /**
   * Creates command which shows tool window with specified set of parameters.
   * Command uses cloned copy of passed <code>info</code> object.
   *
   * @param dirtyMode if <code>true</code> then JRootPane will not be validated and repainted after adding
   *                  the decorator. Moreover in this (dirty) mode animation doesn't work.
   */
  @NotNull
  final FinalizableCommand createAddDecoratorCmd(@NotNull InternalDecorator decorator,
                                                 @NotNull WindowInfoImpl info,
                                                 final boolean dirtyMode,
                                                 @NotNull Runnable finishCallBack) {
    final WindowInfoImpl copiedInfo = info.copy();
    final String id = copiedInfo.getId();

    myDecorator2Info.put(decorator, copiedInfo);
    myId2Decorator.put(id, decorator);

    if (info.isDocked()) {
      WindowInfoImpl sideInfo = getDockedInfoAt(info.getAnchor(), !info.isSplit());
      if (sideInfo == null) {
        return new AddDockedComponentCmd(decorator, info, dirtyMode, finishCallBack);
      }
      else {
        return new AddAndSplitDockedComponentCmd(decorator, info, dirtyMode, finishCallBack);
      }
    }
    else if (info.isSliding()) {
      return new AddSlidingComponentCmd(decorator, info, dirtyMode, finishCallBack);
    }
    else {
      throw new IllegalArgumentException("Unknown window type: " + info.getType());
    }
  }

  /**
   * Creates command which removes tool button from tool stripe.
   *
   * @param id <code>ID</code> of the button to be removed.
   */
  @NotNull
  final FinalizableCommand createRemoveButtonCmd(@NotNull String id, @NotNull Runnable finishCallBack) {
    final StripeButton button = getButtonById(id);
    final WindowInfoImpl info = getButtonInfoById(id);

    myButton2Info.remove(button);
    myId2Button.remove(id);
    return new RemoveToolStripeButtonCmd(button, info, finishCallBack);
  }

  /**
   * Creates command which hides tool window with specified set of parameters.
   *
   * @param dirtyMode if <code>true</code> then JRootPane will not be validated and repainted after removing
   *                  the decorator. Moreover in this (dirty) mode animation doesn't work.
   */
  @NotNull
  final FinalizableCommand createRemoveDecoratorCmd(@NotNull String id, final boolean dirtyMode, @NotNull Runnable finishCallBack) {
    final Component decorator = getDecoratorById(id);
    final WindowInfoImpl info = getDecoratorInfoById(id);

    myDecorator2Info.remove(decorator);
    myId2Decorator.remove(id);

    WindowInfoImpl sideInfo = getDockedInfoAt(info.getAnchor(), !info.isSplit());

    if (info.isDocked()) {
      if (sideInfo == null) {
        return new RemoveDockedComponentCmd(info, dirtyMode, finishCallBack);
      }
      else {
        return new RemoveSplitAndDockedComponentCmd(info, dirtyMode, finishCallBack);
      }
    }
    else if (info.isSliding()) {
      return new RemoveSlidingComponentCmd(decorator, info, dirtyMode, finishCallBack);
    }
    else {
      throw new IllegalArgumentException("Unknown window type");
    }
  }

  /**
   * Creates command which sets specified document component.
   *
   * @param component component to be set.
   */
  @NotNull
  final FinalizableCommand createSetEditorComponentCmd(final JComponent component, @NotNull Runnable finishCallBack) {
    return new SetEditorComponentCmd(component, finishCallBack);
  }

  @NotNull
  final FinalizableCommand createUpdateButtonPositionCmd(@NotNull String id, @NotNull Runnable finishCallback) {
    return new UpdateButtonPositionCmd(id, finishCallback);
  }

  @NotNull
  public final JComponent getMyLayeredPane() {
    return myLayeredPane;
  }

  @Nullable
  private StripeButton getButtonById(final String id) {
    return myId2Button.get(id);
  }

  private Component getDecoratorById(final String id) {
    return myId2Decorator.get(id);
  }

  /**
   * @param id <code>ID</code> of tool stripe butoon.
   * @return <code>WindowInfo</code> associated with specified tool stripe button.
   */
  private WindowInfoImpl getButtonInfoById(final String id) {
    return myButton2Info.get(myId2Button.get(id));
  }

  /**
   * @param id <code>ID</code> of decorator.
   * @return <code>WindowInfo</code> associated with specified window decorator.
   */
  private WindowInfoImpl getDecoratorInfoById(final String id) {
    return myDecorator2Info.get(myId2Decorator.get(id));
  }

  /**
   * Sets (docks) specified component to the specified anchor.
   */
  private void setComponent(final JComponent component, @NotNull ToolWindowAnchor anchor, final float weight) {
    if (ToolWindowAnchor.TOP == anchor) {
      myVerticalSplitter.setFirstComponent(component);
      myVerticalSplitter.setFirstSize((int)(myLayeredPane.getHeight() * weight));
    }
    else if (ToolWindowAnchor.LEFT == anchor) {
      myHorizontalSplitter.setFirstComponent(component);
      myHorizontalSplitter.setFirstSize((int)(myLayeredPane.getWidth() * weight));
    }
    else if (ToolWindowAnchor.BOTTOM == anchor) {
      myVerticalSplitter.setLastComponent(component);
      myVerticalSplitter.setLastSize((int)(myLayeredPane.getHeight() * weight));
    }
    else if (ToolWindowAnchor.RIGHT == anchor) {
      myHorizontalSplitter.setLastComponent(component);
      myHorizontalSplitter.setLastSize((int)(myLayeredPane.getWidth() * weight));
    }
    else {
      LOG.error("unknown anchor: " + anchor);
    }
  }

  private JComponent getComponentAt(@NotNull ToolWindowAnchor anchor) {
    if (ToolWindowAnchor.TOP == anchor) {
      return myVerticalSplitter.getFirstComponent();
    }
    else if (ToolWindowAnchor.LEFT == anchor) {
      return myHorizontalSplitter.getFirstComponent();
    }
    else if (ToolWindowAnchor.BOTTOM == anchor) {
      return myVerticalSplitter.getLastComponent();
    }
    else if (ToolWindowAnchor.RIGHT == anchor) {
      return myHorizontalSplitter.getLastComponent();
    }
    else {
      LOG.error("unknown anchor: " + anchor);
      return null;
    }
  }

  private float getPreferredSplitProportion(@NotNull String id, float defaultValue) {
    Float f = myId2SplitProportion.get(id);
    return f == null ? defaultValue : f;
  }

  private WindowInfoImpl getDockedInfoAt(@NotNull ToolWindowAnchor anchor, boolean side) {
    for (WindowInfoImpl info : myDecorator2Info.values()) {
      if (info.isVisible() && info.isDocked() && info.getAnchor() == anchor && side == info.isSplit()) {
        return info;
      }
    }

    return null;
  }

  private void setDocumentComponent(final JComponent component) {
    (myWidescreen ? myVerticalSplitter : myHorizontalSplitter).setInnerComponent(component);
  }

  private void updateToolStripesVisibility() {
    boolean oldVisible = myLeftStripe.isVisible();

    final boolean showButtons = !UISettings.getInstance().getHideToolStripes() && !UISettings.getInstance().getPresentationMode();
    boolean visible = showButtons || myStripesOverlayed;
    myLeftStripe.setVisible(visible);
    myRightStripe.setVisible(visible);
    myTopStripe.setVisible(visible);
    myBottomStripe.setVisible(visible);

    boolean overlayed = !showButtons && myStripesOverlayed;

    myLeftStripe.setOverlayed(overlayed);
    myRightStripe.setOverlayed(overlayed);
    myTopStripe.setOverlayed(overlayed);
    myBottomStripe.setOverlayed(overlayed);


    if (oldVisible != visible) {
      revalidate();
      repaint();
    }
  }

  public int getBottomHeight() {
    return myBottomStripe.isVisible() ? myBottomStripe.getHeight() : 0;
  }

  public boolean isBottomSideToolWindowsVisible() {
    return getComponentAt(ToolWindowAnchor.BOTTOM) != null;
  }

  @Nullable
  Stripe getStripeFor(String id) {
    ToolWindow window = myManager.getToolWindow(id);
    if (window == null) {
      return null;
    }

    final ToolWindowAnchor anchor = myManager.getToolWindow(id).getAnchor();
    if (ToolWindowAnchor.TOP == anchor) {
      return myTopStripe;
    }
    if (ToolWindowAnchor.BOTTOM == anchor) {
      return myBottomStripe;
    }
    if (ToolWindowAnchor.LEFT == anchor) {
      return myLeftStripe;
    }
    if (ToolWindowAnchor.RIGHT == anchor) {
      return myRightStripe;
    }

    throw new IllegalArgumentException("Anchor=" + anchor);
  }

  @Nullable
  Stripe getStripeFor(@NotNull Rectangle screenRec, @NotNull Stripe preferred) {
    if (preferred.containsScreen(screenRec)) {
      return myStripes.get(myStripes.indexOf(preferred));
    }

    for (Stripe each : myStripes) {
      if (each.containsScreen(screenRec)) {
        return myStripes.get(myStripes.indexOf(each));
      }
    }

    return null;
  }

  void startDrag() {
    for (Stripe each : myStripes) {
      each.startDrag();
    }
  }

  void stopDrag() {
    for (Stripe each : myStripes) {
      each.stopDrag();
    }
  }

  void stretchWidth(@NotNull ToolWindow wnd, int value) {
    stretch(wnd, value);
  }

  void stretchHeight(@NotNull ToolWindow wnd, int value) {
    stretch(wnd, value);
  }

  private void stretch(@NotNull ToolWindow wnd, int value) {
    Pair<Resizer, Component> pair = findResizerAndComponent(wnd);
    if (pair == null) return;

    boolean vertical = wnd.getAnchor() == ToolWindowAnchor.TOP || wnd.getAnchor() == ToolWindowAnchor.BOTTOM;
    int actualSize = (vertical ? pair.second.getHeight() : pair.second.getWidth()) + value;
    boolean first = wnd.getAnchor() == ToolWindowAnchor.LEFT  || wnd.getAnchor() == ToolWindowAnchor.TOP;
    int maxValue = vertical ? myVerticalSplitter.getMaxSize(first) : myHorizontalSplitter.getMaxSize(first);
    int minValue = vertical ? myVerticalSplitter.getMinSize(first) : myHorizontalSplitter.getMinSize(first);

    pair.first.setSize(Math.max(minValue, Math.min(maxValue, actualSize)));
  }

  @Nullable
  private Pair<Resizer, Component> findResizerAndComponent(@NotNull ToolWindow wnd) {
    if (!wnd.isVisible()) return null;

    Resizer resizer = null;
    Component cmp = null;

    if (wnd.getType() == ToolWindowType.DOCKED) {
      cmp = getComponentAt(wnd.getAnchor());

      if (cmp != null) {
        if (wnd.getAnchor().isHorizontal()) {
          resizer = myVerticalSplitter.getFirstComponent() == cmp
                    ? new Resizer.Splitter.FirstComponent(myVerticalSplitter)
                    : new Resizer.Splitter.LastComponent(myVerticalSplitter);
        }
        else {
          resizer = myHorizontalSplitter.getFirstComponent() == cmp
                    ? new Resizer.Splitter.FirstComponent(myHorizontalSplitter)
                    : new Resizer.Splitter.LastComponent(myHorizontalSplitter);
        }
      }
    }
    else if (wnd.getType() == ToolWindowType.SLIDING) {
      cmp = wnd.getComponent();
      while (cmp != null) {
        if (cmp.getParent() == myLayeredPane) break;
        cmp = cmp.getParent();
      }

      if (cmp != null) {
        if (wnd.getAnchor() == ToolWindowAnchor.TOP) {
          resizer = new Resizer.LayeredPane.Top(cmp);
        }
        else if (wnd.getAnchor() == ToolWindowAnchor.BOTTOM) {
          resizer = new Resizer.LayeredPane.Bottom(cmp);
        }
        else if (wnd.getAnchor() == ToolWindowAnchor.LEFT) {
          resizer = new Resizer.LayeredPane.Left(cmp);
        }
        else if (wnd.getAnchor() == ToolWindowAnchor.RIGHT) {
          resizer = new Resizer.LayeredPane.Right(cmp);
        }
      }
    }

    return resizer != null ? Pair.create(resizer, cmp) : null;
  }

  private void updateLayout() {
    UISettings uiSettings = UISettings.getInstance();
    if (myWidescreen != uiSettings.getWideScreenSupport()) {
      JComponent documentComponent = (myWidescreen ? myVerticalSplitter : myHorizontalSplitter).getInnerComponent();
      myWidescreen = uiSettings.getWideScreenSupport();
      if (myWidescreen) {
        myVerticalSplitter.setInnerComponent(null);
        myHorizontalSplitter.setInnerComponent(myVerticalSplitter);
      }
      else {
        myHorizontalSplitter.setInnerComponent(null);
        myVerticalSplitter.setInnerComponent(myHorizontalSplitter);
      }
      myLayeredPane.remove(myWidescreen ? myVerticalSplitter : myHorizontalSplitter);
      myLayeredPane.add(myWidescreen ? myHorizontalSplitter : myVerticalSplitter, DEFAULT_LAYER);
      setDocumentComponent(documentComponent);
    }
    if (myLeftHorizontalSplit != uiSettings.getLeftHorizontalSplit()) {
      JComponent component = getComponentAt(ToolWindowAnchor.LEFT);
      if (component instanceof Splitter) {
        Splitter splitter = (Splitter)component;
        InternalDecorator first = (InternalDecorator)splitter.getFirstComponent();
        InternalDecorator second = (InternalDecorator)splitter.getSecondComponent();
        setComponent(splitter, ToolWindowAnchor.LEFT, ToolWindowAnchor.LEFT.isSplitVertically()
                                                      ? first.getWindowInfo().getWeight()
                                                      : first.getWindowInfo().getWeight() + second.getWindowInfo().getWeight());
      }
      myLeftHorizontalSplit = uiSettings.getLeftHorizontalSplit();
    }
    if (myRightHorizontalSplit != uiSettings.getRightHorizontalSplit()) {
      JComponent component = getComponentAt(ToolWindowAnchor.RIGHT);
      if (component instanceof Splitter) {
        Splitter splitter = (Splitter)component;
        InternalDecorator first = (InternalDecorator)splitter.getFirstComponent();
        InternalDecorator second = (InternalDecorator)splitter.getSecondComponent();
        setComponent(splitter, ToolWindowAnchor.RIGHT, ToolWindowAnchor.RIGHT.isSplitVertically()
                                                       ? first.getWindowInfo().getWeight()
                                                       : first.getWindowInfo().getWeight() + second.getWindowInfo().getWeight());
      }
      myRightHorizontalSplit = uiSettings.getRightHorizontalSplit();
    }
  }

  public boolean isMaximized(@NotNull ToolWindow wnd) {
      return myMaximizedProportion != null && myMaximizedProportion.first == wnd;
  }

  void setMaximized(@NotNull ToolWindow wnd, boolean maximized) {
    Pair<Resizer, Component> resizerAndComponent = findResizerAndComponent(wnd);
    if (resizerAndComponent == null) return;

    if (!maximized) {
      ToolWindow maximizedWindow = myMaximizedProportion.first;
      assert maximizedWindow == wnd;
      resizerAndComponent.first.setSize(myMaximizedProportion.second);
      myMaximizedProportion = null;
    } else {
      int size = wnd.getAnchor().isHorizontal() ? resizerAndComponent.second.getHeight() : resizerAndComponent.second.getWidth();
      stretch(wnd, Short.MAX_VALUE);
      myMaximizedProportion = Pair.create(wnd, size);
    }
    doLayout();
  }


  @FunctionalInterface
  interface Resizer {
    void setSize(int size);


    abstract class Splitter implements Resizer {
      ThreeComponentsSplitter mySplitter;

      Splitter(@NotNull ThreeComponentsSplitter splitter) {
        mySplitter = splitter;
      }

      static class FirstComponent extends Splitter {
        FirstComponent(@NotNull ThreeComponentsSplitter splitter) {
          super(splitter);
        }

        @Override
        public void setSize(int size) {
          mySplitter.setFirstSize(size);
        }
      }

      static class LastComponent extends Splitter {
        LastComponent(@NotNull ThreeComponentsSplitter splitter) {
          super(splitter);
        }

        @Override
        public void setSize(int size) {
          mySplitter.setLastSize(size);
        }
      }
    }

    abstract class LayeredPane implements Resizer {
      Component myComponent;

      LayeredPane(@NotNull Component component) {
        myComponent = component;
      }

      @Override
      public final void setSize(int size) {
        _setSize(size);
        if (myComponent.getParent() instanceof JComponent) {
          JComponent parent = (JComponent)myComponent;
          parent.revalidate();
          parent.repaint();
        }
      }

      abstract void _setSize(int size);

      static class Left extends LayeredPane {

        Left(@NotNull Component component) {
          super(component);
        }

        @Override
        public void _setSize(int size) {
          myComponent.setSize(size, myComponent.getHeight());
        }
      }

      static class Right extends LayeredPane {
        Right(@NotNull Component component) {
          super(component);
        }

        @Override
        public void _setSize(int size) {
          Rectangle bounds = myComponent.getBounds();
          int delta = size - bounds.width;
          bounds.x -= delta;
          bounds.width += delta;
          myComponent.setBounds(bounds);
        }
      }

      static class Top extends LayeredPane {
        Top(@NotNull Component component) {
          super(component);
        }

        @Override
        public void _setSize(int size) {
          myComponent.setSize(myComponent.getWidth(), size);
        }
      }

      static class Bottom extends LayeredPane {
        Bottom(@NotNull Component component) {
          super(component);
        }

        @Override
        public void _setSize(int size) {
          Rectangle bounds = myComponent.getBounds();
          int delta = size - bounds.height;
          bounds.y -= delta;
          bounds.height += delta;
          myComponent.setBounds(bounds);
        }
      }
    }
  }

  private final class AddDockedComponentCmd extends FinalizableCommand {
    private final JComponent myComponent;
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    public AddDockedComponentCmd(@NotNull JComponent component,
                                 @NotNull WindowInfoImpl info,
                                 final boolean dirtyMode,
                                 @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myComponent = component;
      myInfo = info;
      myDirtyMode = dirtyMode;
    }

    @Override
    public final void run() {
      try {
        final ToolWindowAnchor anchor = myInfo.getAnchor();
        setComponent(myComponent, anchor, normalizeWeigh(myInfo.getWeight()));
        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class AddAndSplitDockedComponentCmd extends FinalizableCommand {
    private final JComponent myNewComponent;
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    private AddAndSplitDockedComponentCmd(@NotNull JComponent newComponent,
                                          @NotNull WindowInfoImpl info,
                                          final boolean dirtyMode,
                                          @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myNewComponent = newComponent;
      myInfo = info;
      myDirtyMode = dirtyMode;
    }

    @Override
    public void run() {
      try {
        final ToolWindowAnchor anchor = myInfo.getAnchor();
        class MySplitter extends Splitter implements UISettingsListener {
          @Override
          public void uiSettingsChanged(UISettings uiSettings) {
            if (anchor == ToolWindowAnchor.LEFT) {
              setOrientation(!uiSettings.getLeftHorizontalSplit());
            }
            else if (anchor == ToolWindowAnchor.RIGHT) {
              setOrientation(!uiSettings.getRightHorizontalSplit());
            }
          }
        }
        Splitter splitter = new MySplitter();
        splitter.setOrientation(anchor.isSplitVertically());
        if (!anchor.isHorizontal()) {
          splitter.setAllowSwitchOrientationByMouseClick(true);
          splitter.addPropertyChangeListener(evt -> {
            if (!Splitter.PROP_ORIENTATION.equals(evt.getPropertyName())) return;
            boolean isSplitterHorizontalNow = !splitter.isVertical();
            UISettings settings = UISettings.getInstance();
            if (anchor == ToolWindowAnchor.LEFT) {
              if (settings.getLeftHorizontalSplit() != isSplitterHorizontalNow) {
                settings.setLeftHorizontalSplit(isSplitterHorizontalNow);
                settings.fireUISettingsChanged();
              }
            }
            if (anchor == ToolWindowAnchor.RIGHT) {
              if (settings.getRightHorizontalSplit() != isSplitterHorizontalNow) {
                settings.setRightHorizontalSplit(isSplitterHorizontalNow);
                settings.fireUISettingsChanged();
              }
            }
          });
        }
        JComponent c = getComponentAt(anchor);
        float newWeight;
        if (c instanceof InternalDecorator) {
          InternalDecorator oldComponent = (InternalDecorator)c;
          if (myInfo.isSplit()) {
            splitter.setFirstComponent(oldComponent);
            splitter.setSecondComponent(myNewComponent);
            float proportion = getPreferredSplitProportion(oldComponent.getWindowInfo().getId(),
                                                           normalizeWeigh(oldComponent.getWindowInfo().getSideWeight() /
                                                                          (oldComponent.getWindowInfo().getSideWeight() +
                                                                           myInfo.getSideWeight())));
            splitter.setProportion(proportion);
            if (!anchor.isHorizontal() && !anchor.isSplitVertically()) {
              newWeight = normalizeWeigh(oldComponent.getWindowInfo().getWeight() + myInfo.getWeight());
            }
            else {
              newWeight = normalizeWeigh(oldComponent.getWindowInfo().getWeight());
            }
          }
          else {
            splitter.setFirstComponent(myNewComponent);
            splitter.setSecondComponent(oldComponent);
            splitter.setProportion(normalizeWeigh(myInfo.getSideWeight()));
            if (!anchor.isHorizontal() && !anchor.isSplitVertically()) {
              newWeight = normalizeWeigh(oldComponent.getWindowInfo().getWeight() + myInfo.getWeight());
            }
            else {
              newWeight = normalizeWeigh(myInfo.getWeight());
            }
          }
        } else {
          newWeight = normalizeWeigh(myInfo.getWeight());
        }
        setComponent(splitter, anchor, newWeight);

        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class AddSlidingComponentCmd extends FinalizableCommand {
    private final Component myComponent;
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    public AddSlidingComponentCmd(@NotNull Component component,
                                  @NotNull WindowInfoImpl info,
                                  final boolean dirtyMode,
                                  @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myComponent = component;
      myInfo = info;
      myDirtyMode = dirtyMode;
    }
    @Override
    public final void run() {
      try {
        // Show component.
        if (!myDirtyMode && UISettings.getInstance().getAnimateWindows() && !RemoteDesktopService.isRemoteSession()) {
          // Prepare top image. This image is scrolling over bottom image.
          final Image topImage = myLayeredPane.getTopImage();
          final Graphics topGraphics = topImage.getGraphics();

          Rectangle bounds;

          try {
            myLayeredPane.add(myComponent, JLayeredPane.PALETTE_LAYER);
            myLayeredPane.moveToFront(myComponent);
            myLayeredPane.setBoundsInPaletteLayer(myComponent, myInfo.getAnchor(), myInfo.getWeight());
            bounds = myComponent.getBounds();
            myComponent.paint(topGraphics);
            myLayeredPane.remove(myComponent);
          }
          finally {
            topGraphics.dispose();
          }
          // Prepare bottom image.
          final Image bottomImage = myLayeredPane.getBottomImage();
          final Graphics bottomGraphics = bottomImage.getGraphics();
          try {
            bottomGraphics.setClip(0, 0, bounds.width, bounds.height);
            bottomGraphics.translate(-bounds.x, -bounds.y);
            myLayeredPane.paint(bottomGraphics);
          }
          finally {
            bottomGraphics.dispose();
          }
          // Start animation.
          final Surface surface = new Surface(topImage, bottomImage, 1, myInfo.getAnchor(), UISettings.ANIMATION_DURATION);
          myLayeredPane.add(surface, JLayeredPane.PALETTE_LAYER);
          surface.setBounds(bounds);
          myLayeredPane.validate();
          myLayeredPane.repaint();

          surface.runMovement();
          myLayeredPane.remove(surface);
          myLayeredPane.add(myComponent, JLayeredPane.PALETTE_LAYER);
        }
        else { // not animated
          myLayeredPane.add(myComponent, JLayeredPane.PALETTE_LAYER);
          myLayeredPane.setBoundsInPaletteLayer(myComponent, myInfo.getAnchor(), myInfo.getWeight());
        }
        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class AddToolStripeButtonCmd extends FinalizableCommand {
    private final StripeButton myButton;
    private final WindowInfoImpl myInfo;
    private final Comparator<StripeButton> myComparator;

    public AddToolStripeButtonCmd(final StripeButton button,
                                  @NotNull WindowInfoImpl info,
                                  @NotNull Comparator<StripeButton> comparator,
                                  @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myButton = button;
      myInfo = info;
      myComparator = comparator;
    }

    @Override
    public final void run() {
      try {
        final ToolWindowAnchor anchor = myInfo.getAnchor();
        if (ToolWindowAnchor.TOP == anchor) {
          myTopStripe.addButton(myButton, myComparator);
        }
        else if (ToolWindowAnchor.LEFT == anchor) {
          myLeftStripe.addButton(myButton, myComparator);
        }
        else if (ToolWindowAnchor.BOTTOM == anchor) {
          myBottomStripe.addButton(myButton, myComparator);
        }
        else if (ToolWindowAnchor.RIGHT == anchor) {
          myRightStripe.addButton(myButton, myComparator);
        }
        else {
          LOG.error("unknown anchor: " + anchor);
        }
        validate();
        repaint();
      }
      finally {
        finish();
      }
    }
  }

  private final class RemoveToolStripeButtonCmd extends FinalizableCommand {
    private final StripeButton myButton;
    private final WindowInfoImpl myInfo;

    public RemoveToolStripeButtonCmd(@NotNull StripeButton button, @NotNull WindowInfoImpl info, @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myButton = button;
      myInfo = info;
    }

    @Override
    public final void run() {
      try {
        final ToolWindowAnchor anchor = myInfo.getAnchor();
        if (ToolWindowAnchor.TOP == anchor) {
          myTopStripe.removeButton(myButton);
        }
        else if (ToolWindowAnchor.LEFT == anchor) {
          myLeftStripe.removeButton(myButton);
        }
        else if (ToolWindowAnchor.BOTTOM == anchor) {
          myBottomStripe.removeButton(myButton);
        }
        else if (ToolWindowAnchor.RIGHT == anchor) {
          myRightStripe.removeButton(myButton);
        }
        else {
          LOG.error("unknown anchor: " + anchor);
        }
        validate();
        repaint();
      }
      finally {
        finish();
      }
    }
  }

  private final class RemoveDockedComponentCmd extends FinalizableCommand {
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    public RemoveDockedComponentCmd(@NotNull WindowInfoImpl info, final boolean dirtyMode, @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myInfo = info;
      myDirtyMode = dirtyMode;
    }

    @Override
    public final void run() {
      try {
        setComponent(null, myInfo.getAnchor(), 0);
        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class RemoveSplitAndDockedComponentCmd extends FinalizableCommand {
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    private RemoveSplitAndDockedComponentCmd(@NotNull WindowInfoImpl info,
                                             boolean dirtyMode,
                                             @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myInfo = info;
      myDirtyMode = dirtyMode;
    }

    @Override
    public void run() {
      try {
        ToolWindowAnchor anchor = myInfo.getAnchor();
        JComponent c = getComponentAt(anchor);
        if (c instanceof Splitter) {
          Splitter splitter = (Splitter)c;
          final InternalDecorator component =
            myInfo.isSplit() ? (InternalDecorator)splitter.getFirstComponent() : (InternalDecorator)splitter.getSecondComponent();
          if (myInfo.isSplit() && component != null) {
              myId2SplitProportion.put(component.getWindowInfo().getId(), splitter.getProportion());
          }
          setComponent(component, anchor, component != null ? component.getWindowInfo().getWeight() : 0);
        } else {
          setComponent(null, anchor, 0);
        }
        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class RemoveSlidingComponentCmd extends FinalizableCommand {
    private final Component myComponent;
    private final WindowInfoImpl myInfo;
    private final boolean myDirtyMode;

    public RemoveSlidingComponentCmd(Component component, @NotNull WindowInfoImpl info, boolean dirtyMode, @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myComponent = component;
      myInfo = info;
      myDirtyMode = dirtyMode;
    }
    @Override
    public final void run() {
      try {
        final UISettings uiSettings = UISettings.getInstance();
        if (!myDirtyMode && uiSettings.getAnimateWindows() && !RemoteDesktopService.isRemoteSession()) {
          final Rectangle bounds = myComponent.getBounds();
          // Prepare top image. This image is scrolling over bottom image. It contains
          // picture of component is being removed.
          final Image topImage = myLayeredPane.getTopImage();
          final Graphics topGraphics = topImage.getGraphics();
          try {
            myComponent.paint(topGraphics);
          }
          finally {
            topGraphics.dispose();
          }
          // Prepare bottom image. This image contains picture of component that is located
          // under the component to is being removed.
          final Image bottomImage = myLayeredPane.getBottomImage();
          final Graphics bottomGraphics = bottomImage.getGraphics();
          try {
            myLayeredPane.remove(myComponent);
            bottomGraphics.clipRect(0, 0, bounds.width, bounds.height);
            bottomGraphics.translate(-bounds.x, -bounds.y);
            myLayeredPane.paint(bottomGraphics);
          }
          finally {
            bottomGraphics.dispose();
          }
          // Remove component from the layered pane and start animation.
          final Surface surface = new Surface(topImage, bottomImage, -1, myInfo.getAnchor(), UISettings.ANIMATION_DURATION);
          myLayeredPane.add(surface, JLayeredPane.PALETTE_LAYER);
          surface.setBounds(bounds);
          myLayeredPane.validate();
          myLayeredPane.repaint();

          surface.runMovement();
          myLayeredPane.remove(surface);
        }
        else { // not animated
          myLayeredPane.remove(myComponent);
        }
        if (!myDirtyMode) {
          myLayeredPane.validate();
          myLayeredPane.repaint();
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class SetEditorComponentCmd extends FinalizableCommand {
    private final JComponent myComponent;

    public SetEditorComponentCmd(final JComponent component, @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myComponent = component;
    }

    @Override
    public void run() {
      try {
        setDocumentComponent(myComponent);
        myLayeredPane.validate();
        myLayeredPane.repaint();
      }
      finally {
        finish();
      }
    }
  }

  private final class UpdateButtonPositionCmd extends FinalizableCommand {
    private final String myId;

    private UpdateButtonPositionCmd(@NotNull String id, @NotNull Runnable finishCallBack) {
      super(finishCallBack);
      myId = id;
    }

    @Override
    public void run() {
      try {
        StripeButton stripeButton = getButtonById(myId);
        if (stripeButton == null) {
          return;
        }

        WindowInfoImpl info = stripeButton.getWindowInfo();
        ToolWindowAnchor anchor = info.getAnchor();

        if (ToolWindowAnchor.TOP == anchor) {
          myTopStripe.revalidate();
        }
        else if (ToolWindowAnchor.LEFT == anchor) {
          myLeftStripe.revalidate();
        }
        else if (ToolWindowAnchor.BOTTOM == anchor) {
          myBottomStripe.revalidate();
        }
        else if (ToolWindowAnchor.RIGHT == anchor) {
          myRightStripe.revalidate();
        }
        else {
          LOG.error("unknown anchor: " + anchor);
        }
      }
      finally {
        finish();
      }
    }
  }

  private final class MyLayeredPane extends JBLayeredPane {
    /*
     * These images are used to perform animated showing and hiding of components.
     * They are the member for performance reason.
     */
    private Reference<BufferedImage> myBottomImageRef;
    private Reference<BufferedImage> myTopImageRef;

    public MyLayeredPane(@NotNull JComponent splitter) {
      setOpaque(false);
      add(splitter, JLayeredPane.DEFAULT_LAYER);
    }

    final Image getBottomImage() {
      Pair<BufferedImage, Reference<BufferedImage>> result = getImage(myBottomImageRef);
      myBottomImageRef = result.second;
      return result.first;
    }

    final Image getTopImage() {
      Pair<BufferedImage, Reference<BufferedImage>> result = getImage(myTopImageRef);
      myTopImageRef = result.second;
      return result.first;
    }

    @NotNull
    private Pair<BufferedImage, Reference<BufferedImage>> getImage(@Nullable Reference<BufferedImage> imageRef) {
      LOG.assertTrue(UISettings.getInstance().getAnimateWindows());
      BufferedImage image = SoftReference.dereference(imageRef);
      if (image == null || image.getWidth(null) < getWidth() || image.getHeight(null) < getHeight()) {
        final int width = Math.max(Math.max(1, getWidth()), myFrame.getWidth());
        final int height = Math.max(Math.max(1, getHeight()), myFrame.getHeight());
        if (SystemInfo.isWindows) {
          image = myFrame.getGraphicsConfiguration().createCompatibleImage(width, height);
        }
        else {
          // Under Linux we have found that images created by createCompatibleImage(),
          // createVolatileImage(), etc extremely slow for rendering. TrueColor buffered image
          // is MUCH faster.
          // On Mac we create a retina-compatible image

          image = UIUtil.createImage(getGraphics(), width, height, BufferedImage.TYPE_INT_RGB);
        }
        imageRef = new SoftReference<>(image);
      }
      return Pair.create(image, imageRef);
    }

    /**
     * When component size becomes larger then bottom and top images should be enlarged.
     */
    @Override
    public void doLayout() {
      final int width = getWidth();
      final int height = getHeight();
      if (width < 0 || height < 0) {
        return;
      }
      // Resize component at the DEFAULT layer. It should be only on component in that layer
      Component[] components = getComponentsInLayer(JLayeredPane.DEFAULT_LAYER.intValue());
      LOG.assertTrue(components.length <= 1);
      for (final Component component : components) {
        component.setBounds(0, 0, getWidth(), getHeight());
      }
      // Resize components at the PALETTE layer
      components = getComponentsInLayer(JLayeredPane.PALETTE_LAYER.intValue());
      for (final Component component : components) {
        if (!(component instanceof InternalDecorator)) {
          continue;
        }
        final WindowInfoImpl info = myDecorator2Info.get(component);
        // In normal situation info is not null. But sometimes Swing sends resize
        // event to removed component. See SCR #19566.
        if (info == null) {
          continue;
        }

        final float weight;
        if (info.getAnchor().isHorizontal()) {
          weight = (float)component.getHeight() / (float)getHeight();
        }
        else {
          weight = (float)component.getWidth() / (float)getWidth();
        }
        setBoundsInPaletteLayer(component, info.getAnchor(), weight);
      }
    }

    final void setBoundsInPaletteLayer(@NotNull Component component, @NotNull ToolWindowAnchor anchor, float weight) {
      if (weight < .0f) {
        weight = WindowInfoImpl.DEFAULT_WEIGHT;
      }
      else if (weight > 1.0f) {
        weight = 1.0f;
      }
      if (ToolWindowAnchor.TOP == anchor) {
        component.setBounds(0, 0, getWidth(), (int)(getHeight() * weight + .5f));
      }
      else if (ToolWindowAnchor.LEFT == anchor) {
        component.setBounds(0, 0, (int)(getWidth() * weight + .5f), getHeight());
      }
      else if (ToolWindowAnchor.BOTTOM == anchor) {
        final int height = (int)(getHeight() * weight + .5f);
        component.setBounds(0, getHeight() - height, getWidth(), height);
      }
      else if (ToolWindowAnchor.RIGHT == anchor) {
        final int width = (int)(getWidth() * weight + .5f);
        component.setBounds(getWidth() - width, 0, width, getHeight());
      }
      else {
        LOG.error("unknown anchor " + anchor);
      }
    }
  }

  void setStripesOverlayed(boolean stripesOverlayed) {
    myStripesOverlayed = stripesOverlayed;
    updateToolStripesVisibility();
  }

  @Override
  public void dispose() {
  }

  private static float normalizeWeigh(final float weight) {
    if (weight <= 0) return WindowInfoImpl.DEFAULT_WEIGHT;
    if (weight >= 1) return 1 - WindowInfoImpl.DEFAULT_WEIGHT;
    return weight;
  }
}
