/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.ui.tab;

import java.awt.Component;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JTabbedPane;
import javax.swing.SwingUtilities;
import javax.swing.plaf.TabbedPaneUI;
import javax.swing.plaf.basic.BasicTabbedPaneUI;

import jnpad.GUIUtilities;
import jnpad.config.Configurable;
import jnpad.util.Platform;

/**
 * The Class AbstractTabbedPane.
 *
 * @version 0.3
 * @since jNPad 0.1
 */
public abstract class AbstractTabbedPane extends JTabbedPane implements ITabbedPane {
  
  /** The _suppress state changed events. */
  private boolean              _suppressStateChangedEvents = false;
  
  /** The _auto focus on tab hide close. */
  private boolean              _autoFocusOnTabHideClose    = true;
  
  /** The _suppress set selected index. */
  private boolean              _suppressSetSelectedIndex   = false;
  
  /** The _auto request focus. */
  private boolean              _autoRequestFocus           = true;
  
  /** The _scrollable tab support. */
  private ScrollableTabSupport _scrollableTabSupport;

  /** Logger. */
  private final static Logger  LOGGER                      = Logger.getLogger(AbstractTabbedPane.class.getName());

  /** UID. */
  private static final long    serialVersionUID            = -5254761801301385146L;

  /**
   * Instantiates a new abstract tabbed pane.
   */
  public AbstractTabbedPane() {
    this(TOP, WRAP_TAB_LAYOUT);
  }

  /**
   * Instantiates a new abstract tabbed pane.
   *
   * @param tabPlacement the tab placement
   */
  public AbstractTabbedPane(int tabPlacement) {
    this(tabPlacement, WRAP_TAB_LAYOUT);
  }

  /**
   * Instantiates a new abstract tabbed pane.
   *
   * @param tabPlacement the tab placement
   * @param tabLayoutPolicy the tab layout policy
   */
  public AbstractTabbedPane(int tabPlacement, int tabLayoutPolicy) {
    super(tabPlacement, tabLayoutPolicy);
  }

  /**
   * To component.
   *
   * @return JComponent
   * @see jnpad.ui.tab.ITabbedPane#toComponent()
   */
  @Override
  public JComponent toComponent() {
    return this;
  }
  
  /**
   * Process mouse event.
   *
   * @param e MouseEvent
   */
  @Override
  public void processMouseEvent(MouseEvent e) {
    try {
      super.processMouseEvent(e);
      if (e.getID() == MouseEvent.MOUSE_PRESSED) {
        handleMousePressed(e);
      }
      else if (e.getID() == MouseEvent.MOUSE_CLICKED) {
        handleMouseClicked(e);
      }
    }
    catch (Exception ex) {
      //empty
    }
  }

  /**
   * Handle mouse pressed.
   *
   * @param e MouseEvent
   */
  protected void handleMousePressed(MouseEvent e) {
    int i = getSelectedIndex();
    if ((i > -1) && (getTabCount() > i)) {
      Rectangle bounds = getUI().getTabBounds(this, i);
      if (bounds.contains(e.getPoint())) {
        focusOnSelectedComponent();
      }
    }
    else if (isAutoRequestFocus()) {
      requestFocusInWindow();
    }
  }

  /**
   * Handle mouse clicked.
   *
   * @param e MouseEvent
   */
  protected void handleMouseClicked(MouseEvent e) {
    int i = getSelectedIndex();
    if ((i > -1) && (getTabCount() > i)) {
      Rectangle bounds = getUI().getTabBounds(this, i);
      if (bounds.contains(e.getPoint())) {
        if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
          expand();
        }
        else if (SwingUtilities.isRightMouseButton(e)) {
          showPopupMenu(e);
        }
      }
    }
  }

  /**
   * Close.
   * 
   * @param i the i
   */
  protected void close(int i) {
    remove(i);
  }

  /**
   * Expand.
   */
  protected abstract void expand();

  /**
   * Show popup menu.
   *
   * @param e MouseEvent
   */
  protected abstract void showPopupMenu(MouseEvent e);
  
  /**
   * Focus on selected component.
   */
  @Override
  public void focusOnSelectedComponent() {
    Component selectedComponent = getSelectedComponent();
    if (selectedComponent != null) {
      GUIUtilities.requestFocus(selectedComponent);
    }
  }
  
  /**
   * Previous tab.
   */
  @Override
  public void previousTab() {
    if (getTabCount() > 1) {
      int i = getSelectedIndex();
      if (i == 0)
        i = getTabCount() - 1;
      else
        i--;
      setSelectedIndex(i);
    }
  }

  /**
   * Next tab.
   */
  @Override
  public void nextTab() {
    if (getTabCount() > 1) {
      int i = getSelectedIndex();
      if (i == getTabCount() - 1)
        i = 0;
      else
        i++;
      setSelectedIndex(i);
    }
  }
  
  /**
   * Configure.
   *
   * @param cfg int
   * @see jnpad.config.Configurable#configure(int)
   */
  @Override
  public void configure(final int cfg) {
    for (int i = 0; i < getTabCount(); i++) {
      Component c = getComponentAt(i);
      if (c instanceof Configurable)
        ((Configurable) c).configure(cfg);
    }
  }

  /**
   * Sets the component at.
   *
   * @param index int
   * @param component Component
   */
  @Override
  public void setComponentAt(int index, Component component) {
    super.setComponentAt(index, component);
    if (!isAutoFocusOnTabHideClose())
      clearVisComp();
  }

  /**
   * Removes the tab at.
   *
   * @param index the index
   * @see javax.swing.JTabbedPane#removeTabAt(int)
   */
  @Override
  public void removeTabAt(int index) {
    int tabCount = getTabCount();
    int selected = getSelectedIndex();
    boolean enforce = false;

    if (selected == index && selected < tabCount - 1) {
      // since JDK5 fixed this, we only need to enforce the event when it is not JDK5 and above.
      // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6368047
      enforce = !Platform.isJRE5Above();
    }

    if (!isAutoFocusOnTabHideClose())
      clearVisComp();

    super.removeTabAt(index);

    // We need to fire events
    if (enforce) {
      try {
        fireStateChanged();
      }
      catch (Throwable th) {
        LOGGER.log(Level.WARNING, th.getMessage(), th);
      }
    }
  }

  /**
   * Sets the suppress state changed events.
   *
   * @param suppress the new suppress state changed events
   */
  @Override
  public void setSuppressStateChangedEvents(boolean suppress) {
    _suppressStateChangedEvents = suppress;
  }

  /**
   * Checks if is suppress state changed events.
   *
   * @return true, if is suppress state changed events
   */
  @Override
  public boolean isSuppressStateChangedEvents() {
    return _suppressStateChangedEvents;
  }

  /**
   * Sets the auto focus on tab hide close.
   *
   * @param autoFocusOnTabHideClose the new auto focus on tab hide close
   */
  @Override
  public void setAutoFocusOnTabHideClose(boolean autoFocusOnTabHideClose) {
    _autoFocusOnTabHideClose = autoFocusOnTabHideClose;
  }

  /**
   * Checks if is auto focus on tab hide close.
   *
   * @return true, if is auto focus on tab hide close
   */
  @Override
  public boolean isAutoFocusOnTabHideClose() {
    return _autoFocusOnTabHideClose;
  }

  /**
   * Fire state changed.
   *
   * @see javax.swing.JTabbedPane#fireStateChanged()
   */
  @Override
  protected void fireStateChanged() {
    if (isSuppressStateChangedEvents())
      return;

    if (!isAutoFocusOnTabHideClose())
      clearVisComp();

    super.fireStateChanged();
  }
  
  /**
   * Checks if is auto request focus.
   *
   * @return true, if is auto request focus
   */
  @Override
  public boolean isAutoRequestFocus() {
    return _autoRequestFocus;
  }

  /**
   * Sets the auto request focus.
   *
   * @param autoRequestFocus the new auto request focus
   */
  @Override
  public void setAutoRequestFocus(boolean autoRequestFocus) {
    _autoRequestFocus = autoRequestFocus;
  }

  /**
   * Checks if is suppress set selected index.
   *
   * @return true, if is suppress set selected index
   */
  @Override
  public boolean isSuppressSetSelectedIndex() {
    return _suppressSetSelectedIndex;
  }

  /**
   * Sets the suppress set selected index.
   *
   * @param suppressSetSelectedIndex the new suppress set selected index
   */
  @Override
  public void setSuppressSetSelectedIndex(boolean suppressSetSelectedIndex) {
    _suppressSetSelectedIndex = suppressSetSelectedIndex;
  }

  /**
   * Sets the selected index.
   *
   * @param index int
   * @see javax.swing.JTabbedPane#setSelectedIndex(int)
   */
  @Override
  public void setSelectedIndex(int index) {
    if (index >= getTabCount() || isSuppressSetSelectedIndex()) {
      return;
    }

    boolean old = isFocusCycleRoot();
    setFocusCycleRoot(true);
    try {
      int oldIndex = getSelectedIndex();
      if (oldIndex != index) {
        super.setSelectedIndex(index);
      }
    }
    finally {
      setFocusCycleRoot(old);
    }

    scrollTabToVisible(index);
    doLayout();
  }

  /**
   * Sets the uI.
   *
   * @param ui TabbedPaneUI
   * @see javax.swing.JTabbedPane#setUI(javax.swing.plaf.TabbedPaneUI)
   */
  @Override
  public void setUI(final TabbedPaneUI ui) {
    super.setUI(ui);
    if (ui instanceof BasicTabbedPaneUI) {
      _scrollableTabSupport = new ScrollableTabSupport( (BasicTabbedPaneUI) ui);
    }
    else {
      _scrollableTabSupport = null;
    }
  }

  /**
   * Scroll tab to visible.
   *
   * @param index the index
   */
  @Override
  public final void scrollTabToVisible(final int index) {
    if (_scrollableTabSupport == null || WRAP_TAB_LAYOUT == getTabLayoutPolicy()) { // tab scrolling isn't supported by UI
      return;
    }
    final TabbedPaneUI tabbedPaneUI = getUI();
    Rectangle tabBounds = tabbedPaneUI.getTabBounds(this, index);
    final int tabPlacement = getTabPlacement();
    if (TOP == tabPlacement || BOTTOM == tabPlacement) { //tabs are on the top or bottom
      if (tabBounds.x < 50) { //if tab is to the left of visible area
        int leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
        while (leadingTabIndex != index && leadingTabIndex > 0 && tabBounds.x < 50) {
          _scrollableTabSupport.setLeadingTabIndex(leadingTabIndex - 1);
          leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
          tabBounds = tabbedPaneUI.getTabBounds(this, index);
        }
      }
      else if (tabBounds.x + tabBounds.width > getWidth() - 50) { // if tab's right side is out of visible range
        int leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
        while (leadingTabIndex != index && leadingTabIndex < getTabCount() - 1 && tabBounds.x + tabBounds.width > getWidth() - 50) {
          _scrollableTabSupport.setLeadingTabIndex(leadingTabIndex + 1);
          leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
          tabBounds = tabbedPaneUI.getTabBounds(this, index);
        }
      }
    }
    else { // tabs are on left or right side
      if (tabBounds.y < 30) { //tab is above visible area
        int leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
        while (leadingTabIndex != index && leadingTabIndex > 0 && tabBounds.y < 30) {
          _scrollableTabSupport.setLeadingTabIndex(leadingTabIndex - 1);
          leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
          tabBounds = tabbedPaneUI.getTabBounds(this, index);
        }
      }
      else if (tabBounds.y + tabBounds.height > getHeight() - 30) { //tab is under visible area
        int leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
        while (leadingTabIndex != index && leadingTabIndex < getTabCount() - 1 && tabBounds.y + tabBounds.height > getHeight() - 30) {
          _scrollableTabSupport.setLeadingTabIndex(leadingTabIndex + 1);
          leadingTabIndex = _scrollableTabSupport.getLeadingTabIndex();
          tabBounds = tabbedPaneUI.getTabBounds(this, index);
        }
      }
    }
  }
  
  /**
   * Moves selected tab from current position to the position specified in tabIndex.
   *
   * @param tabIndex new index
   */
  @Override
  public void moveSelectedTabTo(int tabIndex) {
    int selectedIndex = getSelectedIndex();
    if (selectedIndex == tabIndex || 
        tabIndex < 0 || tabIndex >= getTabCount() || 
        selectedIndex == -1) { // do nothing
      return;
    }

    Component selectedComponent = getComponentAt(selectedIndex);

    boolean old = isAutoRequestFocus();

    boolean shouldChangeFocus = false;
    // we will not let UI to auto request focus so we will have to do it here.
    // if the selected component has focus, we will request it after the tab is moved.
    if (selectedComponent != null) {
      if (GUIUtilities.isAncestorOfFocusOwner(selectedComponent) && isAutoFocusOnTabHideClose()) {
        shouldChangeFocus = true;
      }
    }

    try {
      setSuppressStateChangedEvents(true);
      setAutoRequestFocus(false);

      if (selectedIndex - tabIndex == 1 || tabIndex - selectedIndex == 1) {
        Component frame = getComponentAt(tabIndex);
        String title = getTitleAt(tabIndex);
        String tooltip = getToolTipTextAt(tabIndex);
        Icon icon = getIconAt(tabIndex);
        setSuppressStateChangedEvents(true);
        try {
          if (tabIndex > selectedIndex) {
            insertTab(title, icon, frame, tooltip, selectedIndex);
          }
          else {
            insertTab(title, icon, frame, tooltip, selectedIndex + 1);
          }
        }
        finally {
          setSuppressStateChangedEvents(false);
        }
      }
      else {
        Component frame = getComponentAt(selectedIndex);
        String title = getTitleAt(selectedIndex);
        String tooltip = getToolTipTextAt(selectedIndex);
        Icon icon = getIconAt(selectedIndex);
        setSuppressStateChangedEvents(true);
        try {
          if (tabIndex > selectedIndex) {
            insertTab(title, icon, frame, tooltip, tabIndex + 1);
          }
          else {
            insertTab(title, icon, frame, tooltip, tabIndex);
          }
        }
        finally {
          setSuppressStateChangedEvents(false);
        }
      }

      if (!Platform.isJRE5Above()) {
        // a workaround for Swing bug
        if (tabIndex == getTabCount() - 2) {
          setSelectedIndex(getTabCount() - 1);
        }
      }

      setAutoRequestFocus(old);
      setSelectedIndex(tabIndex);

    }
    finally {
      setSuppressStateChangedEvents(false);
      if (shouldChangeFocus) {
        requestFocusInWindow();
      }
    }
  }
  
  /**
   * Clear vis comp.
   */
  protected void clearVisComp() {
    // this is done so that the super removetab and fireselection do not attempt to manage focus
    // A very dirty hack to access a private variable is jtabpane. Note - this only works on 1.6
    try {
      java.lang.reflect.Field field = JTabbedPane.class.getDeclaredField("visComp"); //$NON-NLS-1$
      // set accessible true
      field.setAccessible(true);
      field.set(this, null);
      //superVisComp = (Component) field.get(this);
    }
    catch (Exception e) {
      // null
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  /**
   * That is hack-helper for working with scrollable tab layout. The problem is BasicTabbedPaneUI doesn't
   * have any API to scroll tab to visible area. Therefore we have to implement it...
   */
  private final class ScrollableTabSupport {
    
    /** The my ui. */
    private final BasicTabbedPaneUI myUI;
    
    /** The Constant TAB_SCROLLER_NAME. */
    public static final String      TAB_SCROLLER_NAME            = "tabScroller"; //$NON-NLS-1$
    
    /** The Constant LEADING_TAB_INDEX_NAME. */
    public static final String      LEADING_TAB_INDEX_NAME       = "leadingTabIndex"; //$NON-NLS-1$
    
    /** The Constant SET_LEADING_TAB_INDEX_METHOD. */
    public static final String      SET_LEADING_TAB_INDEX_METHOD = "setLeadingTabIndex"; //$NON-NLS-1$

    /**
     * Instantiates a new scrollable tab support.
     *
     * @param ui the ui
     */
    public ScrollableTabSupport(final BasicTabbedPaneUI ui) {
      myUI = ui;
    }

    /**
     * Gets the leading tab index.
     *
     * @return value of <code>leadingTabIndex</code> field of
     * BasicTabbedPaneUI.ScrollableTabSupport class.
     */
    public int getLeadingTabIndex() {
      try {
        final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME);
        tabScrollerField.setAccessible(true);
        final Object tabScrollerValue = tabScrollerField.get(myUI);

        final Field leadingTabIndexField = tabScrollerValue.getClass().getDeclaredField(LEADING_TAB_INDEX_NAME);
        leadingTabIndexField.setAccessible(true);
        return leadingTabIndexField.getInt(tabScrollerValue);
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
        return -1;
      }
    }

    /**
     * Sets the leading tab index.
     *
     * @param leadingIndex the new leading tab index
     */
    public void setLeadingTabIndex(final int leadingIndex) {
      try {
        final Field tabScrollerField = BasicTabbedPaneUI.class.getDeclaredField(TAB_SCROLLER_NAME);
        tabScrollerField.setAccessible(true);
        final Object tabScrollerValue = tabScrollerField.get(myUI);

        Method setLeadingIndexMethod = null;
        final Method[] methods = tabScrollerValue.getClass().getDeclaredMethods();
        for (final Method method : methods) {
          if (SET_LEADING_TAB_INDEX_METHOD.equals(method.getName())) {
            setLeadingIndexMethod = method;
            break;
          }
        }
        if (setLeadingIndexMethod == null) {
          LOGGER.warning("method setLeadingTabIndex not found"); //$NON-NLS-1$
          return;
        }
        setLeadingIndexMethod.setAccessible(true);
        setLeadingIndexMethod.invoke(tabScrollerValue, getTabPlacement(), leadingIndex);
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }
  //////////////////////////////////////////////////////////////////////////////
  
}
