/*
 * Copyright 2000-2009 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.ui;

import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
import com.intellij.ui.tabs.JBTabs;
import com.intellij.util.IJSwingUtilities;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.MouseListener;

/**
 * @author Anton Katilin
 * @author Vladimir Kondratyev
 */
public class TabbedPaneWrapper  {
  protected TabbedPane myTabbedPane;
  protected JComponent myTabbedPaneHolder;

  private TabFactory myFactory;

  protected TabbedPaneWrapper(boolean construct) {
    if (construct) {
      init(SwingConstants.TOP, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, new JTabbedPaneFactory(this));
    }
  }

  public TabbedPaneWrapper(@NotNull Disposable parentDisposable) {
    this(SwingConstants.TOP, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, parentDisposable);
  }

  /**
   * Creates tabbed pane wrapper with specified tab placement
   *
   * @param tabPlacement tab placement. It one of the <code>SwingConstants.TOP</code>,
   * <code>SwingConstants.LEFT</code>, <code>SwingConstants.BOTTOM</code> or
   * <code>SwingConstants.RIGHT</code>.
   */
  public TabbedPaneWrapper(int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, @NotNull Disposable parentDisposable) {
    final TabFactory factory;
    if (SwingConstants.BOTTOM == tabPlacement || SwingConstants.TOP == tabPlacement) {
      factory = new JBTabsFactory(this, null, parentDisposable);
    } else {
      factory = new JTabbedPaneFactory(this);
    }

    init(tabPlacement, installKeyboardNavigation, factory);
  }

  void init(int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, TabFactory tabbedPaneFactory) {
    myFactory = tabbedPaneFactory;

    myTabbedPane = createTabbedPane(tabPlacement);
    myTabbedPane.putClientProperty(TabbedPaneWrapper.class, myTabbedPane);
    myTabbedPane.setKeyboardNavigation(installKeyboardNavigation);

    myTabbedPaneHolder = createTabbedPaneHolder();
    myTabbedPaneHolder.add(myTabbedPane.getComponent(), BorderLayout.CENTER);
    myTabbedPaneHolder.setFocusTraversalPolicyProvider(true);
    myTabbedPaneHolder.setFocusTraversalPolicy(new _MyFocusTraversalPolicy());

    assertIsDispatchThread();
  }

  public boolean isDisposed() {
    return myTabbedPane != null && myTabbedPane.isDisposed();
  }

  private void assertIsDispatchThread() {
    final ApplicationEx application = ApplicationManagerEx.getApplicationEx();
    if (application != null){
      application.assertIsDispatchThread(myTabbedPane.getComponent());
    }
  }

  public final void addChangeListener(final ChangeListener listener){
    assertIsDispatchThread();
    myTabbedPane.addChangeListener(listener);
  }

  public final void removeChangeListener(final ChangeListener listener){
    assertIsDispatchThread();
    myTabbedPane.removeChangeListener(listener);
  }

  protected TabbedPaneHolder createTabbedPaneHolder() {
    return myFactory.createTabbedPaneHolder();
  }

  public final JComponent getComponent() {
    assertIsDispatchThread();
    return myTabbedPaneHolder;
  }

  /**
   * @see javax.swing.JTabbedPane#addTab(java.lang.String, javax.swing.Icon, java.awt.Component, java.lang.String)
   */
  public final synchronized void addTab(final String title, final Icon icon, final JComponent component, final String tip) {
    insertTab(title, icon, component, tip, myTabbedPane.getTabCount());
  }

  public final synchronized void addTab(final String title, final JComponent component) {
    insertTab(title, null, component, null, myTabbedPane.getTabCount());
  }

  public synchronized void insertTab(final String title, final Icon icon, final JComponent component, final String tip, final int index) {
    myTabbedPane.insertTab(title, icon, createTabWrapper(component), tip, index);
  }

  protected TabWrapper createTabWrapper(JComponent component) {
    return myFactory.createTabWrapper(component);
  }

  protected TabbedPane createTabbedPane(final int tabPlacement) {
    return myFactory.createTabbedPane(tabPlacement);
  }

  /**
   * @see javax.swing.JTabbedPane#setTabPlacement
   */
  public final void setTabPlacement(final int tabPlacement) {
    assertIsDispatchThread();
    myTabbedPane.setTabPlacement(tabPlacement);
  }

  public final void addMouseListener(final MouseListener listener) {
    assertIsDispatchThread();
    myTabbedPane.addMouseListener(listener);
  }

  public final synchronized int getSelectedIndex() {
    return myTabbedPane.getSelectedIndex();
  }

  /**
   * @see javax.swing.JTabbedPane#getSelectedComponent()
   */
  public final synchronized JComponent getSelectedComponent() {
    // Workaround for JDK 6 bug
    final TabWrapper tabWrapper = myTabbedPane.getTabCount() > 0 ? (TabWrapper)myTabbedPane.getSelectedComponent():null;
    return tabWrapper != null ? tabWrapper.getComponent() : null;
  }

  public final void setSelectedIndex(final int index) {
    setSelectedIndex(index, true);
  }

  public final void setSelectedIndex(final int index, boolean requestFocus) {
    assertIsDispatchThread();

    final boolean hadFocus = IJSwingUtilities.hasFocus2(myTabbedPaneHolder);
    myTabbedPane.setSelectedIndex(index);
    if (hadFocus && requestFocus) {
      myTabbedPaneHolder.requestFocus();
    }
  }

  public final void setSelectedComponent(final JComponent component){
    assertIsDispatchThread();

    final int index=indexOfComponent(component);
    if(index==-1){
      throw new IllegalArgumentException("component not found in tabbed pane wrapper");
    }
    setSelectedIndex(index);
  }

  public final synchronized void removeTabAt(final int index) {
    assertIsDispatchThread();

    final boolean hadFocus = IJSwingUtilities.hasFocus2(myTabbedPaneHolder);
    final TabWrapper wrapper = getWrapperAt(index);
    try {
      myTabbedPane.removeTabAt(index);
      if (myTabbedPane.getTabCount() == 0) {
        // to clear BasicTabbedPaneUI.visibleComponent field
        myTabbedPane.revalidate();
      }
      if (hadFocus) {
        myTabbedPaneHolder.requestFocus();
      }
    }
    finally {
      wrapper.dispose();
    }
  }

  public final synchronized int getTabCount() {
    return myTabbedPane.getTabCount();
  }

  public final Color getForegroundAt(final int index){
    assertIsDispatchThread();
    return myTabbedPane.getForegroundAt(index);
  }

  /**
   * @see javax.swing.JTabbedPane#setForegroundAt(int, java.awt.Color)
   */
  public final void setForegroundAt(final int index,final Color color){
    assertIsDispatchThread();
    myTabbedPane.setForegroundAt(index,color);
  }

  public final Component getTabComponentAt(final int index) {
    return myTabbedPane.getTabComponentAt(index);
  }
  /**
   * @see javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)
   */
  public final synchronized JComponent getComponentAt(final int i) {
    return getWrapperAt(i).getComponent();
  }

  private TabWrapper getWrapperAt(final int i) {
    return (TabWrapper)myTabbedPane.getComponentAt(i);
  }

  public final void setTitleAt(final int index, final String title) {
    assertIsDispatchThread();
    myTabbedPane.setTitleAt(index, title);
  }

  public final void setToolTipTextAt(final int index, final String toolTipText) {
    assertIsDispatchThread();
    myTabbedPane.setToolTipTextAt(index, toolTipText);
  }

  /**
   * @see javax.swing.JTabbedPane#setComponentAt(int, java.awt.Component)
   */
  public final synchronized void setComponentAt(final int index, final JComponent component) {
    assertIsDispatchThread();
    myTabbedPane.setComponentAt(index, createTabWrapper(component));
  }

  /**
   * @see javax.swing.JTabbedPane#setIconAt(int, javax.swing.Icon)
   */
  public final void setIconAt(final int index, final Icon icon) {
    assertIsDispatchThread();
    myTabbedPane.setIconAt(index, icon);
  }

  public final void setEnabledAt(final int index, final boolean enabled) {
    assertIsDispatchThread();
    myTabbedPane.setEnabledAt(index, enabled);
  }

  /**
   * @see javax.swing.JTabbedPane#indexOfComponent(java.awt.Component)
   */
  public final synchronized int indexOfComponent(final JComponent component) {
    for (int i=0; i < myTabbedPane.getTabCount(); i++) {
      final JComponent c = getWrapperAt(i).getComponent();
      if (c == component) {
        return i;
      }
    }
    return -1;
  }

  /**
   * @see javax.swing.JTabbedPane#getTabLayoutPolicy
   */
  public final synchronized int getTabLayoutPolicy(){
    return myTabbedPane.getTabLayoutPolicy();
  }

  /**
   * @see javax.swing.JTabbedPane#setTabLayoutPolicy
   */
  public final synchronized void setTabLayoutPolicy(final int policy){
    myTabbedPane.setTabLayoutPolicy(policy);
    final int index=myTabbedPane.getSelectedIndex();
    if(index!=-1){
      myTabbedPane.scrollTabToVisible(index);
    }
  }

  /**
   * @deprecated Keyboard navigation is installed/deinstalled automatically. This method does nothing now.
   */
  public final void installKeyboardNavigation(){
  }

  /**
   * @deprecated Keyboard navigation is installed/deinstalled automatically. This method does nothing now.
   */
  public final void uninstallKeyboardNavigation(){
  }

  public final String getTitleAt(final int i) {
    return myTabbedPane.getTitleAt(i);
  }

  public void setSelectedTitle(@Nullable final String title) {
    if (title == null) return;

    for (int i = 0; i < myTabbedPane.getTabCount(); i++) {
      final String each = myTabbedPane.getTitleAt(i);
      if (title.equals(each)) {
        myTabbedPane.setSelectedIndex(i);
        break;
      }
    }
  }

  @Nullable
  public String getSelectedTitle() {
    return getSelectedIndex() < 0 ? null : getTitleAt(getSelectedIndex());
  }

  public void removeAll() {
    myTabbedPane.removeAll();
  }

  public static final class TabWrapper extends JPanel implements DataProvider{
    private JComponent myComponent;

    boolean myCustomFocus = true;

    public TabWrapper(@NotNull final JComponent component) {
      super(new BorderLayout());
      myComponent = component;
      add(component, BorderLayout.CENTER);
    }

    /*
     * Make possible to search down for DataProviders
     */
    public Object getData(final String dataId) {
      if(myComponent instanceof DataProvider){
        return ((DataProvider)myComponent).getData(dataId);
      } else {
        return null;
      }
    }

    public JComponent getComponent() {
      return myComponent;
    }

    /**
     * TabWrappers are never reused so we can fix the leak in some LAF's TabbedPane UI by cleanuping ourselves.
     */
    public void dispose() {
      if (myComponent != null) {
        remove(myComponent);
        myComponent = null;
      }
    }

    public boolean requestDefaultFocus() {
      if (!myCustomFocus) return super.requestDefaultFocus();
      if (myComponent == null) return false; // Just in case someone requests the focus when we're already removed from the Swing tree.
      final JComponent preferredFocusedComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myComponent);
      if (preferredFocusedComponent != null) {
        if (!preferredFocusedComponent.requestFocusInWindow()) {
          preferredFocusedComponent.requestFocus();
        }
        return true;
      } else {
        return myComponent.requestDefaultFocus();
      }
    }

    public void requestFocus() {
      if (!myCustomFocus) {
        super.requestFocus();
      } else {
        requestDefaultFocus();
      }
    }

    public boolean requestFocusInWindow() {
      if (!myCustomFocus) return super.requestFocusInWindow();
      return requestDefaultFocus();
    }
  }

  private final class _MyFocusTraversalPolicy extends IdeFocusTraversalPolicy{
    @Override
    public boolean isNoDefaultComponent() {
      return false;
    }

    public final Component getDefaultComponentImpl(final Container focusCycleRoot) {
      final JComponent component=getSelectedComponent();
      if(component!=null){
        return IdeFocusTraversalPolicy.getPreferredFocusedComponent(component, this);
      }else{
        return null;
      }
    }
  }

  public static class TabbedPaneHolder extends JPanel {

    private final TabbedPaneWrapper myWrapper;

    protected TabbedPaneHolder(TabbedPaneWrapper wrapper) {
      super(new BorderLayout());
      myWrapper = wrapper;
    }

    public boolean requestDefaultFocus() {
      final JComponent preferredFocusedComponent = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myWrapper.myTabbedPane.getComponent());
      if (preferredFocusedComponent != null) {
        if (!preferredFocusedComponent.requestFocusInWindow()) {
          preferredFocusedComponent.requestFocus();
        }
        return true;
      } else {
        return super.requestDefaultFocus();
      }
    }

    public final void requestFocus() {
      requestDefaultFocus();
    }

    public final boolean requestFocusInWindow() {
      return requestDefaultFocus();
    }

    public void updateUI() {
      super.updateUI();
      if (myWrapper != null) {
        myWrapper.myTabbedPane.updateUI();
      }
    }

    public TabbedPaneWrapper getTabbedPaneWrapper() {
      return myWrapper;
    }
  }

  public static TabbedPaneWrapper get(JTabbedPane tabs) {
    return (TabbedPaneWrapper)tabs.getClientProperty(TabbedPaneWrapper.class);
  }

  private interface TabFactory {
    TabbedPane createTabbedPane(int tabPlacement);
    TabbedPaneHolder createTabbedPaneHolder();
    TabWrapper createTabWrapper(JComponent component);
  }

  private static class JTabbedPaneFactory implements TabFactory {
    private final TabbedPaneWrapper myWrapper;

    private JTabbedPaneFactory(TabbedPaneWrapper wrapper) {
      myWrapper = wrapper;
    }

    public TabbedPane createTabbedPane(int tabPlacement) {
      return new TabbedPaneImpl(tabPlacement);
    }

    public TabbedPaneHolder createTabbedPaneHolder() {
      return new TabbedPaneHolder(myWrapper);
    }

    public TabWrapper createTabWrapper(JComponent component) {
      return new TabWrapper(component);
    }
  }

  private static class JBTabsFactory implements TabFactory {

    private final Project myProject;
    private final Disposable myParent;
    private final TabbedPaneWrapper myWrapper;

    private JBTabsFactory(TabbedPaneWrapper wrapper, Project project, @NotNull Disposable parent) {
      myWrapper = wrapper;
      myProject = project;
      myParent = parent;
    }

    public TabbedPane createTabbedPane(int tabPlacement) {
      return new JBTabsPaneImpl(myProject, tabPlacement, myParent);
    }

    public TabbedPaneHolder createTabbedPaneHolder() {
      return new TabbedPaneHolder(myWrapper) {
        @Override
        public boolean requestDefaultFocus() {
          getTabs().requestFocus();
          return true;
        }

      };
    }

    public TabWrapper createTabWrapper(JComponent component) {
      final TabWrapper tabWrapper = new TabWrapper(component);
      tabWrapper.myCustomFocus = false;
      return tabWrapper;
    }

    public JBTabs getTabs() {
      return ((JBTabsPaneImpl)myWrapper.myTabbedPane).getTabs();
    }

    public void dispose() {
    }
  }

  public static class AsJBTabs extends TabbedPaneWrapper {
    public AsJBTabs(@Nullable Project project, int tabPlacement, PrevNextActionsDescriptor installKeyboardNavigation, @NotNull Disposable parent) {
      super(false);
      init(tabPlacement, installKeyboardNavigation, new JBTabsFactory(this, project, parent));
    }

    public JBTabs getTabs() {
      return ((JBTabsPaneImpl)myTabbedPane).getTabs();
    }
  }

  public static class AsJTabbedPane extends TabbedPaneWrapper {
    public AsJTabbedPane(int tabPlacement) {
      super(false);
      init(tabPlacement, TabbedPaneImpl.DEFAULT_PREV_NEXT_SHORTCUTS, new JTabbedPaneFactory(this));
    }
  }

}
