/*
 * 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.text;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyListener;
import java.beans.PropertyChangeListener;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JSplitPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.undo.CannotUndoException;

import jnpad.GUIUtilities;
import jnpad.JNPadFrame;
import jnpad.JNPadKeyboardHandler;
import jnpad.action.ActionManager;
import jnpad.action.JNPadActions;
import jnpad.config.Config;
import jnpad.ui.JNPadSplitPane;
import jnpad.ui.status.IStatusBar;
import jnpad.ui.status.ITextStatusBar.StatusMessage;
import jnpad.util.LineSeparator;
import jnpad.util.Utilities;

/**
 * The Class Buffer.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public final class Buffer extends JPanel implements IBuffer, IView {
  /** The Constant PROPERTY_DIRTY. */
  public static final String  PROPERTY_DIRTY              = "Buffer.dirty";                        //$NON-NLS-1$

  /** The Constant PROPERTY_READ_ONLY. */
  public static final String  PROPERTY_READ_ONLY          = "Buffer.readOnly";                     //$NON-NLS-1$

  /** The Constant PROPERTY_LINE_SEPARATOR. */
  public static final String  PROPERTY_LINE_SEPARATOR     = "Buffer.lineSeparator";                //$NON-NLS-1$

  /** The Constant PROPERTY_ENCODING. */
  public static final String  PROPERTY_ENCODING           = "Buffer.encoding";                     //$NON-NLS-1$

  private EditPane            editPane;
  private JSplitPane          splitPane;
  private Component           mainContent;
  private JPanel              mainPanel                   = new JPanel();
  private String              filePath;

  private CompoundUndoManager undoManager;

  private LineSeparator       lineSeparator               = LineSeparator.getDefault();

  private String              charSet                     = Config.FILE_ENCODING.getValue();

  private boolean             readOnly;
  private boolean             isDirty;
  private boolean             isHandleValueChangedEnabled = true;

  private String              initialSplitConfig;
  private String              lastSplitConfig;
  private int                 lastIndex;
  private List<Integer>       lastCaretPositions          = new ArrayList<Integer>();

  BufferSet                   bufferSet;
  JNPadFrame                  jNPad;

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

  /** UID */
  private static final long   serialVersionUID            = 1104199986574484076L;

  /**
   * Instantiates a new buffer.
   *
   * @param bufferSet the buffer set
   * @param filePath the file path
   * @param text the text
   */
  public Buffer(BufferSet bufferSet, String filePath, String text) {
    super(new BorderLayout());
    try {
      this.bufferSet = bufferSet;
      this.jNPad       = bufferSet.getViewer()._jNPad;
      this.filePath    = Utilities.defaultString(filePath);
      this.undoManager = new CompoundUndoManager(this);

      setOpaque(false);
      mainPanel.setLayout(new BorderLayout());
      add(mainPanel, BorderLayout.CENTER);

      setMainContent(createEditPane(text));
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }
  
  /**
   * Instantiates a new buffer.
   *
   * @param bufferSet the buffer set
   * @param oldBuffer the old buffer
   */
  private Buffer(BufferSet bufferSet, Buffer oldBuffer) {
    super(new BorderLayout());
    try {
      this.bufferSet     = bufferSet;
      this.jNPad         = bufferSet.getViewer()._jNPad;
      this.filePath      = oldBuffer.filePath;
      this.undoManager   = oldBuffer.undoManager;
      this.isDirty       = oldBuffer.isDirty;
      this.readOnly      = oldBuffer.readOnly;
      this.lineSeparator = oldBuffer.lineSeparator;
      this.charSet       = oldBuffer.charSet;

      setOpaque(false);
      mainPanel.setLayout(new BorderLayout());
      add(mainPanel, BorderLayout.CENTER);

      setMainContent(createEditPane(oldBuffer.editPane));
      
      /*if (oldBuffer.isSplitted()) {
        lastSplitConfig = oldBuffer.getSplitConfig();
        lastIndex = oldBuffer.getSelectedIndex();
        lastCaretPositions.addAll(oldBuffer.lastCaretPositions);
        setLastConfig();
      }*/
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }
  
  /**
   * Adds the notify.
   *
   * @see javax.swing.JComponent#addNotify()
   */
  @Override
  public void addNotify() {
    //System.out.println("addNotify");
    super.addNotify();
  }

  /**
   * Removes the notify.
   *
   * @see javax.swing.JComponent#removeNotify()
   */
  @Override
  public void removeNotify() {
    //System.out.println("removeNotify");
    super.removeNotify();
  }
  
  /**
   * Creates the buffer.
   *
   * @param bufferSet the buffer set
   * @return the buffer
   */
  Buffer create(BufferSet bufferSet) {
    Buffer buffer = new Buffer(bufferSet, this);
    for (PropertyChangeListener l : getPropertyChangeListeners()) {
      buffer.doAddPropertyChangeListener(l);
    }
    return buffer;
  }

  /**
   * Creates the edit pane.
   *
   * @param text String
   * @return EditPane
   */
  private EditPane createEditPane(String text) {
    EditPane editPane = new EditPane(this, text);
    editPane.textArea.getDocument().addUndoableEditListener(undoManager);
    editPane.textArea.getDocument().addDocumentListener(new DocumentListener() {
      public void changedUpdate(final DocumentEvent e) {/*handleValueChanged(e);*/}
      public void insertUpdate (final DocumentEvent e) {handleValueChanged(e);}
      public void removeUpdate (final DocumentEvent e) {handleValueChanged(e);}
    });
    editPane.textArea.addFocusListener(new FocusHandler());
    editPane.textArea.setDragEnabled(true);
    editPane.textArea.setTransferHandler(new JNPadTransferHandler(jNPad));

    return editPane;
  }

  /**
   * Creates the edit pane.
   *
   * @param oldEditPane EditPane
   * @return EditPane
   */
  private EditPane createEditPane(EditPane oldEditPane) {
    EditPane editPane = new EditPane(this, oldEditPane);
    editPane.textArea.addFocusListener(new FocusHandler());
    editPane.textArea.setDragEnabled(true);
    editPane.textArea.setTransferHandler(new JNPadTransferHandler(jNPad));
    return editPane;
  }
  
  /**
   * Replace.
   *
   * @param start int
   * @param end int
   * @param replaceFor String
   */
  public void replace(int start, int end, String replaceFor) {
    beginCompoundEdit();
    editPane.textArea.select(start, end);
    editPane.textArea.replaceSelection(replaceFor);
    endCompoundEdit();
  }

  /**
   * Begin compound edit.
   */
  public void beginCompoundEdit() {
    undoManager.beginCompoundEdit();
  }

  /**
   * End compound edit.
   */
  public void endCompoundEdit() {
    undoManager.endCompoundEdit();
  }

  /**
   * Can undo.
   *
   * @return boolean
   */
  public boolean canUndo() {
    return undoManager.canUndo() && !isReadOnly();
  }

  /**
   * Can redo.
   *
   * @return boolean
   */
  public boolean canRedo() {
    return undoManager.canRedo() && !isReadOnly();
  }

  /**
   * Undo.
   *
   * @throws CannotUndoException the cannot undo exception
   */
  public void undo() throws CannotUndoException {
    if (isReadOnly())
      return;
    undoManager.undo();
    if(!undoManager.isModified()) {
      setDirty(false);
    }
  }

  /**
   * Redo.
   *
   * @throws CannotUndoException the cannot undo exception
   */
  public void redo() throws CannotUndoException {
    if (isReadOnly())
      return;
    undoManager.redo();
  }

  /**
   * Sets the as saved.
   */
  public void setAsSaved() {
    setDirty(false);
    undoManager.documentSaved();
  }

  /**
   * Sets the selected edit pane.
   *
   * @param editPane EditPane
   */
  private void setSelectedEditPane(EditPane editPane) {
    this.editPane = editPane;
    editPane.updateControls(CTRLS_ALL);
    repaintGutter();
  }
  
  /**
   * Repaint gutter.
   */
  void repaintGutter() {
    for (EditPane epane : getEditPanes()) {
      epane.repaintGutter();
    }
  }

  /**
   * Gets the selected text area.
   *
   * @return the selected text area
   */
  public JNPadTextArea getSelectedTextArea() {
    return editPane.getTextArea();
  }

  /**
   * Gets the text areas.
   *
   * @return the text areas
   */
  public JNPadTextArea[] getTextAreas() {
    EditPane[] epanes = getEditPanes();
    JNPadTextArea[] array = new JNPadTextArea[epanes.length];
    for (int i = 0; i < epanes.length; i++) {
      array[i] = epanes[i].getTextArea();
    }
    return array;
  }
  
  /**
   * Gets the selected edit pane.
   *
   * @return the selected edit pane
   */
  public EditPane getSelectedEditPane() {
    return editPane;
  }

  /**
   * Gets the edit panes.
   *
   * @return the edit panes
   */
  public EditPane[] getEditPanes() {
    if (splitPane == null) {
      return new EditPane[] { editPane };
    }
    List<EditPane> list = new ArrayList<EditPane>();
    lookForEditPanes(list, splitPane);
    EditPane[] array = new EditPane[list.size()];
    list.toArray(array);
    return array;
  }

  /**
   * Look for edit panes.
   *
   * @param list the list
   * @param comp the comp
   */
  private static void lookForEditPanes(List<EditPane> list, Component comp) {
    if (comp instanceof EditPane) {
      list.add((EditPane) comp);
    }
    else if (comp instanceof JSplitPane) {
      JSplitPane split = (JSplitPane) comp;
      lookForEditPanes(list, split.getLeftComponent());
      lookForEditPanes(list, split.getRightComponent());
    }
  }

  /**
   * Handle value changed.
   *
   * @param e the DocumentEvent
   */
  void handleValueChanged(final DocumentEvent e) {
    if (!isHandleValueChangedEnabled) {
      return;
    }
    setDirty(true);
    updateControls(CTRLS_TEXT_CHANGED);
  }

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class FocusHandler.
   */
  private class FocusHandler extends FocusAdapter {
    
    /**
     * Focus gained.
     *
     * @param e FocusEvent
     * @see java.awt.event.FocusAdapter#focusGained(java.awt.event.FocusEvent)
     */
    @Override
    public void focusGained(final FocusEvent e) {
      // walk up hierarchy, looking for an EditPane
      Component comp = (Component) e.getSource();

      while (!(comp instanceof EditPane)) {
        if (comp == null) {
          return;
        }
        comp = comp.getParent();
      }

      if (comp != editPane) {
        setSelectedEditPane((EditPane) comp);
      }

      Viewer eviewer = bufferSet.getViewer();
      BufferSet eviews = eviewer.getActiveBufferSet();
      if (eviews != bufferSet) {
        eviewer.setActiveBufferSet(bufferSet);
      }
      
    }
  }
  //////////////////////////////////////////////////////////////////////////////

  /**
   * Configure.
   *
   * @param cfg int
   * @see jnpad.config.Configurable#configure(int)
   */
  @Override
  public void configure(final int cfg) {
    for (EditPane epane : getEditPanes()) 
      epane.configure(cfg);
  }

  /**
   * Update controls.
   *
   * @see jnpad.config.Updatable#updateControls()
   */
  @Override
  public void updateControls() {
    updateControls(CTRLS_ALL);
  }

  /**
   * Update controls.
   *
   * @param ctrls the ctrls
   * @see jnpad.config.Updatable#updateControls(int)
   */
  @Override
  public void updateControls(final int ctrls) {
    if ((ctrls & CTRLS_UNDO) != 0 || (ctrls & CTRLS_EDITABLE) != 0) {
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_UNDO, canUndo());
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_REDO, canRedo());
      for (EditPane epane : getEditPanes()) {
        epane.actions.setUndoEnabled(canUndo());
        epane.actions.setRedoEnabled(canRedo());
      }
    }

    if ((ctrls & CTRLS_SPLITTING) != 0) {
      final boolean b = isSplitted();
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_UNSPLIT, b);
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_UNSPLIT_CURRENT, b);
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_NEXT_EDIT_PANE, b);
      ActionManager.INSTANCE.setEnabled(JNPadActions.ACTION_NAME_PREVIOUS_EDIT_PANE, b);
    }

    editPane.updateControls(ctrls);
  }

  /**
   * Gets the selected index.
   *
   * @return the selected index
   */
  public int getSelectedIndex() {
    EditPane[] epanes = getEditPanes();
    for (int i = 0; i < epanes.length; i++) {
      if (editPane == epanes[i]) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Next edit pane.
   */
  public void nextEditPane() {
    EditPane[] epanes = getEditPanes();

    int index = getSelectedIndex();

    if (index > -1 && index < epanes.length - 1) {
      setSelectedEditPane(epanes[++index]);
      editPane.requestFocus();
    }
    else if (index == epanes.length - 1) {
      setSelectedEditPane(epanes[0]);
      editPane.requestFocus();
    }
  }

  /**
   * Previous edit pane.
   */
  public void previousEditPane() {
    EditPane[] epanes = getEditPanes();

    int index = getSelectedIndex();

    if (index > 0 && index < epanes.length) {
      setSelectedEditPane(epanes[--index]);
      editPane.requestFocus();
    }
    else if (index == 0) {
      setSelectedEditPane(epanes[epanes.length - 1]);
      editPane.requestFocus();
    }
  }

  /**
   * Gets the file path.
   *
   * @return the file path
   * @see jnpad.text.IBuffer#getFilePath()
   */
  @Override
  public String getFilePath() {return filePath;}

  /**
   * Sets the file path.
   *
   * @param path the new file path
   * @see jnpad.text.IBuffer#setFilePath(java.lang.String)
   */
  @Override
  public void setFilePath(String path) {
    filePath = Utilities.defaultString(path);
  }

  /**
   * Gets the content type.
   *
   * @return the content type
   * @see jnpad.text.IBuffer#getContentType()
   */
  @Override
  public String getContentType() {
    return editPane.textArea.getContentType();
  }
  
  /**
   * Equals.
   *
   * @param obj the obj
   * @return true, if successful
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(Object obj) {
    return obj == this ||
        (obj instanceof IBuffer && filePath.equals(((IBuffer) obj).getFilePath()));
  }

  /**
   * Hash code.
   *
   * @return the int
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode() {return filePath.hashCode();}

  /**
   * Reload.
   *
   * @throws IOException Signals that an I/O exception has occurred.
   * @see jnpad.text.IBuffer#reload()
   */
  @Override
  public void reload() throws IOException {
    if (isNew()) {
      return;
    }

    String path = getFilePath();

    BufferedInputStream in = new BufferedInputStream(new FileInputStream(path));

    // crea una array de bytes del tamaņo del archivo,
    // para utiliarla como buffer de datos,en el que leer
    // los datos del archivo
    byte[] buffer = new byte[in.available()];

    // leer todos los bytes disponibles en el buffer
    in.read(buffer, 0, buffer.length);
    in.close();

    String encoding = getEncoding();
    String s;
    try {
      s = new String(buffer, 0, buffer.length, encoding);
    }
    catch (Exception ex) {
      try {
        encoding = Config.FILE_ENCODING.getValue();
        s = new String(buffer, 0, buffer.length, encoding);
      }
      catch(Exception ex2) {
        encoding = Config.FILE_ENCODING.getDefault();
        s = new String(buffer, 0, buffer.length, encoding);
      }
      setEncoding(encoding, false);
    }

    final boolean removeEndSpaces = Config.REMOVE_END_SPACES.getValue();

    final int tabSize = !getSelectedTextArea().getUseTabs() ? getSelectedTextArea().getTabSize() : -1;

    String result = GUIUtilities.convertString(s, removeEndSpaces, tabSize);

    final int caretPosition = getCaretPosition();

    setText(result);

    // set caret position
    try {
      getSelectedTextArea().setCaretPosition(caretPosition);
    }
    catch (Exception ex) {
      getSelectedTextArea().setCaretPosition(0);
    }
  }

  /**
   * Sets the text.
   *
   * @param text the new text
   */
  public void setText(String text) {
    editPane.setText(text);
    setDirty(true);
  }

  /**
   * Gets the text.
   *
   * @return the text
   */
  public String getText() {
    return editPane.getText();
  }

  /**
   * Checks for selection.
   *
   * @return true, if successful
   * @see jnpad.text.IBuffer#hasSelection()
   */
  @Override
  public boolean hasSelection() {
    return editPane.hasSelection();
  }

  /**
   * Gets the caret position.
   *
   * @return the caret position
   * @see jnpad.text.IBuffer#getCaretPosition()
   */
  @Override
  public int getCaretPosition() {
    return editPane.textArea.getCaretPosition();
  }

  /**
   * Checks if is line wrapped.
   *
   * @return true, if is line wrapped
   * @see jnpad.text.IBuffer#isLineWrapped()
   * @since 0.3
   */
  @Override
  public boolean isLineWrapped() {
    return editPane.getLineWrap();
  }
  
  /**
   * Checks if is new.
   *
   * @return true, if is new
   */
  public boolean isNew() {
    return!new File(filePath).isFile();
  }

  /**
   * Sets the editor font.
   *
   * @param f the new editor font
   */
  public void setEditorFont(Font f) {
    for (EditPane epane : getEditPanes())
      epane.setEditorFont(f);
  }

  /**
   * Sets the line wrap.
   *
   * @param b the new line wrap
   */
  public void setLineWrap(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setLineWrap(b);
  }

  /**
   * Gets the line wrap.
   *
   * @return the line wrap
   */
  public boolean getLineWrap() {
    return editPane.getLineWrap();
  }

  /**
   * Sets the line numbers visible.
   *
   * @param b the new line numbers visible
   * @see jnpad.text.IView#setLineNumbersVisible(boolean)
   */
  @Override
  public void setLineNumbersVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setLineNumbersVisible(b);
  }

  /**
   * Checks if is line numbers visible.
   *
   * @return true, if is line numbers visible
   */
  public boolean isLineNumbersVisible() {
    return editPane.isLineNumbersVisible();
  }

  /**
   * Checks if is active line visible.
   *
   * @return true, if is active line visible
   */
  public boolean isActiveLineVisible() {
    return editPane.isActiveLineVisible();
  }

  /**
   * Sets the active line visible.
   *
   * @param b the new active line visible
   * @see jnpad.text.IView#setActiveLineVisible(boolean)
   */
  @Override
  public void setActiveLineVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setActiveLineVisible(b);
  }

  /**
   * Checks if is occurrences highlighter visible.
   *
   * @return true, if is occurrences highlighter visible
   */
  public boolean isOccurrencesHighlighterVisible() {
    return editPane.isOccurrencesHighlighterVisible();
  }

  /**
   * Sets the occurrences highlighter visible.
   *
   * @param b the new occurrences highlighter visible
   * @see jnpad.text.IView#setOccurrencesHighlighterVisible(boolean)
   */
  @Override
  public void setOccurrencesHighlighterVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setOccurrencesHighlighterVisible(b);
  }

  /**
   * Checks if is bracket highlighter visible.
   *
   * @return true, if is bracket highlighter visible
   * @since 0.3
   */
  public boolean isBracketHighlighterVisible() {
    return editPane.isBracketHighlighterVisible();
  }

  /**
   * Sets the bracket highlighter visible.
   *
   * @param b the new bracket highlighter visible
   * @see jnpad.text.IView#setBracketHighlighterVisible(boolean)
   * @since 0.3
   */
  @Override
  public void setBracketHighlighterVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setBracketHighlighterVisible(b);
  }
  
  /**
   * Sets the right margin line visible.
   *
   * @param b the new right margin line visible
   * @see jnpad.text.IView#setRightMarginLineVisible(boolean)
   */
  @Override
  public void setRightMarginLineVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setRightMarginLineVisible(b);
  }

  /**
   * Checks if is right margin line visible.
   *
   * @return true, if is right margin line visible
   */
  public boolean isRightMarginLineVisible() {
    return editPane.isRightMarginLineVisible();
  }

  /**
   * Sets the mark strip visible.
   *
   * @param b the new mark strip visible
   * @see jnpad.text.IView#setMarkStripVisible(boolean)
   */
  @Override
  public void setMarkStripVisible(boolean b) {
    for (EditPane epane : getEditPanes())
      epane.setMarkStripVisible(b);
  }

  /**
   * Checks if is mark strip visible.
   *
   * @return true, if is mark strip visible
   */
  public boolean isMarkStripVisible() {
    return editPane.isMarkStripVisible();
  }
  
  /**
   * Request focus.
   *
   * @see javax.swing.JComponent#requestFocus()
   */
  @Override
  public void requestFocus() {
    if (editPane != null)
      editPane.requestFocus();
    else
      super.requestFocus();
  }

  /**
   * Request focus in window.
   *
   * @return true, if successful
   * @see javax.swing.JComponent#requestFocusInWindow()
   */
  @Override
  public boolean requestFocusInWindow() {
    if (editPane != null)
      editPane.requestFocusInWindow();
    return super.requestFocusInWindow();
  }
  
  // --- read only ---
  /**
   * @return boolean
   * @see jnpad.text.IBuffer#isReadOnly()
   */
  @Override
  public boolean isReadOnly() {
    return !editPane.isEditable();
  }
  
  /**
   * Sets the read only.
   *
   * @param b the new read only
   */
  public void setReadOnly(boolean b) {
    if (readOnly == b)
      return;
    doSetReadOnly(b);
    for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
      if (eviews == bufferSet)
        continue;
      for (Buffer eview : eviews) {
        if (equals(eview))
          eview.doSetReadOnly(b);
      }
    }
    // --- trick ---
    jNPad.requestFocus();
    requestFocus();
    // ---
  }
  
  /**
   * Do set read only.
   *
   * @param b the b
   */
  private void doSetReadOnly(boolean b) {
    if (readOnly != b) {
      readOnly = b;
      for (EditPane epane : getEditPanes()) {
        epane.setEditable(!b);
      }
      updateControls(CTRLS_EDITABLE);
      firePropertyChange(PROPERTY_READ_ONLY, !b, b);
    }
  }
  // ---
  
  // --- line separator ---
  /**
   * Gets the line separator.
   *
   * @return the line separator
   */
  public LineSeparator getLineSeparator() {
    return lineSeparator;
  }

  /**
   * Sets the line separator.
   *
   * @param lineSeparator the new line separator
   */
  public void setLineSeparator(LineSeparator lineSeparator) {
    setLineSeparator(lineSeparator, true);
  }

  /**
   * Sets the line separator.
   *
   * @param lineSeparator the line separator
   * @param setDirty the set dirty
   */
  public void setLineSeparator(LineSeparator lineSeparator, boolean setDirty) {
    if (this.lineSeparator == lineSeparator)
      return;
    doSetLineSeparator(lineSeparator, setDirty);
    for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
      if (eviews == bufferSet)
        continue;
      for (Buffer eview : eviews) {
        if (equals(eview))
          eview.doSetLineSeparator(lineSeparator, setDirty);
      }
    }
  }
  
  /**
   * Do set line separator.
   *
   * @param lineSeparator the line separator
   * @param setDirty the set dirty
   */
  private void doSetLineSeparator(LineSeparator lineSeparator, boolean setDirty) {
    if (this.lineSeparator != lineSeparator) {
      LineSeparator old = this.lineSeparator;
      this.lineSeparator = lineSeparator;
      firePropertyChange(PROPERTY_LINE_SEPARATOR, old, lineSeparator);
      if (setDirty) {
        setDirty(true);
      }
    }
  }
  // ---
  
  // --- dirty ---
  /**
   * Checks if is dirty.
   *
   * @return boolean
   * @see jnpad.text.IBuffer#isDirty()
   */
  @Override
  public boolean isDirty() {
    return isDirty;
  }

  /**
   * Sets the dirty.
   *
   * @param b the new dirty
   */
  public void setDirty(boolean b) {
    if (isDirty == b)
      return;
    //System.out.println("{");
    doSetDirty(b);
    for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
      if (eviews == bufferSet)
        continue;
      for (Buffer eview : eviews) {
        //System.out.println(" - " + equals(eview));
        if (equals(eview))
          eview.doSetDirty(b);
      }
    }
    //System.out.println("}");
  }
  
  /**
   * Do set dirty.
   *
   * @param b the b
   */
  private void doSetDirty(boolean b) {
    if (isDirty != b) {
      isDirty = b;
      firePropertyChange(PROPERTY_DIRTY, !b, b);
    }
  }
  // ---

  // --- property change listener ---
  /**
   * Adds the property change listener.
   *
   * @param listener the listener
   * @see java.awt.Container#addPropertyChangeListener(java.beans.PropertyChangeListener)
   */
  @Override
  public void addPropertyChangeListener(PropertyChangeListener listener) {
    doAddPropertyChangeListener(listener);
    if (bufferSet != null) { //4Nimbus
      for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
        if (eviews == bufferSet)
          continue;
        for (Buffer eview : eviews) {
          if (equals(eview))
            eview.doAddPropertyChangeListener(listener);
        }
      }
    }
  }

  /**
   * Do add property change listener.
   *
   * @param listener the listener
   */
  private void doAddPropertyChangeListener(PropertyChangeListener listener) {
    super.addPropertyChangeListener(listener);
  }
  
  /**
   * Removes the property change listener.
   *
   * @param listener the listener
   * @see java.awt.Component#removePropertyChangeListener(java.beans.PropertyChangeListener)
   */
  @Override
  public void removePropertyChangeListener(PropertyChangeListener listener) {
    doRemovePropertyChangeListener(listener);
    for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
      if (eviews == bufferSet)
        continue;
      for (Buffer eview : eviews) {
        if (equals(eview))
          eview.doRemovePropertyChangeListener(listener);
      }
    }
  }

  /**
   * Do remove property change listener.
   * 
   * @param listener the listener
   */
  private void doRemovePropertyChangeListener(PropertyChangeListener listener) {
    super.removePropertyChangeListener(listener);
  }
  // ---

  // --- encoding ---
  /**
   * Gets the encoding.
   * 
   * @return the encoding
   */
  public String getEncoding() {
    return charSet;
  }

  /**
   * Sets the encoding.
   * 
   * @param encoding the new encoding
   * @throws UnsupportedCharsetException the unsupported charset exception
   */
  public void setEncoding(String encoding) throws UnsupportedCharsetException {
    setEncoding(encoding, true);
  }

  /**
   * Sets the encoding.
   *
   * @param encoding the encoding
   * @param setDirty the set dirty
   * @throws UnsupportedCharsetException the unsupported charset exception
   */
  public void setEncoding(String encoding, boolean setDirty) throws UnsupportedCharsetException {
    if (!Charset.isSupported(encoding)) {
      throw new UnsupportedCharsetException(encoding);
    }
    if(charSet.equals(encoding))
      return;
    String old = charSet;
    doSetEncoding(encoding, setDirty);
    for (BufferSet eviews : bufferSet.getViewer().getBufferSets()) {
      if (eviews == bufferSet)
        continue;
      for (Buffer eview : eviews) {
        if (equals(eview))
          eview.doSetEncoding(encoding, setDirty);
      }
    }
    firePropertyChange(PROPERTY_ENCODING, old, encoding);
    if (setDirty) {
      setDirty(true);
    }
  }
  
  /**
   * Do set encoding.
   *
   * @param encoding the encoding
   * @param setDirty the set dirty
   */
  private void doSetEncoding(String encoding, boolean setDirty) {
    charSet = encoding;
  }
  // ---
  
  // --- support ---
  /**
   * 
   * @return IStatusBar
   */
  IStatusBar getStatusBar() {
    return jNPad.getStatusBar();
  }

  /**
   * Clear status message character.
   */
  void clearStatusMessageCharacter() {
    jNPad.getStatusBar().setMessage(StatusMessage.CHARACTER, Utilities.EMPTY_STRING);
  }

  /**
   * 
   * @return JNPadKeyboardHandler
   */
  JNPadKeyboardHandler getKeyboardHandler() {
    return jNPad.getKeyboardHandler();
  }

  /**
   * 
   * @return KeyListener
   */
  KeyListener getKeyEventInterceptor() {
    return jNPad.getKeyEventInterceptor();
  }
  
  /**
   * Sets the key event interceptor.
   *
   * @param listener the new key event interceptor
   */
  void setKeyEventInterceptor(KeyListener listener) {
    jNPad.setKeyEventInterceptor(listener);
  }
  // ---
  
  // --- splitting ---
  /**
   * Split horizontally.
   *
   * @return the edit pane
   */
  public EditPane splitHorizontally() {
    return split(JSplitPane.VERTICAL_SPLIT);
  }

  /**
   * Split vertically.
   *
   * @return the edit pane
   */
  public EditPane splitVertically() {
    return split(JSplitPane.HORIZONTAL_SPLIT);
  }

  /**
   * Split.
   *
   * @param orientation the orientation
   * @return the edit pane
   */
  public EditPane split(int orientation) {
    isHandleValueChangedEnabled = false;

    EditPane oldEditPane = editPane;
    EditPane newEditPane = createEditPane(oldEditPane);

    JComponent oldParent = (JComponent) oldEditPane.getParent();

    final JSplitPane newSplitPane = new JNPadSplitPane(orientation);

    int parentSize = orientation == JSplitPane.VERTICAL_SPLIT ? oldEditPane.getHeight() : oldEditPane.getWidth();
    final int dividerPosition = (int) ((parentSize - newSplitPane.getDividerSize()) * 0.5);
    newSplitPane.setDividerLocation(dividerPosition);

    if (oldParent instanceof JSplitPane) {
      JSplitPane oldSplitPane = (JSplitPane) oldParent;
      int dividerPos = oldSplitPane.getDividerLocation();

      Component left = oldSplitPane.getLeftComponent();

      if (left == oldEditPane) {
        oldSplitPane.setLeftComponent(newSplitPane);
      }
      else {
        oldSplitPane.setRightComponent(newSplitPane);

      }
      newSplitPane.setLeftComponent(oldEditPane);
      newSplitPane.setRightComponent(newEditPane);

      oldSplitPane.setDividerLocation(dividerPos);
    }
    else {
      splitPane = newSplitPane;

      newSplitPane.setLeftComponent(oldEditPane);
      newSplitPane.setRightComponent(newEditPane);

      setMainContent(newSplitPane);
    }

    EventQueue.invokeLater(new Runnable() {
      public void run() {
        newSplitPane.setDividerLocation(dividerPosition);
        isHandleValueChangedEnabled = true;
      }
    });
    
    newEditPane.focusOnTextArea();
    newEditPane.refreshCaretPosition();
    
    return newEditPane;
  }

  /**
   * Unsplit.
   */
  public void unsplit() {
    if (splitPane != null) {
      saveLastConfig();
      setMainContent(editPane);
      splitPane = null;
      editPane.focusOnTextArea();
    }
    else {
      GUIUtilities.beep();
    }
  }

  /**
   * Unsplit current.
   */
  public void unsplitCurrent() {
    if (splitPane != null) {
      saveLastConfig();

      // find first split pane parenting current edit pane
      Component comp = editPane;
      while (!(comp instanceof JSplitPane) && comp != null) {
        comp = comp.getParent();
      }

      JComponent parent = comp == null ? null : (JComponent) comp.getParent();
      if (parent instanceof JSplitPane) {
        JSplitPane parentSplit = (JSplitPane) parent;
        int pos = parentSplit.getDividerLocation();
        if (parentSplit.getLeftComponent() == comp) {
          parentSplit.setLeftComponent(editPane);
        }
        else {
          parentSplit.setRightComponent(editPane);
        }
        parentSplit.setDividerLocation(pos);
        parent.revalidate();
      }
      else {
        setMainContent(editPane);
        splitPane = null;
      }
      editPane.focusOnTextArea();
    }
    else {
      GUIUtilities.beep();
    }
  }

  /**
   * Restore split.
   */
  public void restoreSplit() {
    if (lastSplitConfig == null)
      GUIUtilities.beep();
    else
      setLastConfig();
  }

  /**
   * Sets the main content.
   *
   * @param c Component
   */
  private void setMainContent(Component c) {
    if (mainContent != null) {
      mainPanel.remove(mainContent);
    }
    mainContent = c;
    mainPanel.add(mainContent, BorderLayout.CENTER);
    if (c instanceof JSplitPane) {
      splitPane = (JSplitPane) c;
    }
    else {
      splitPane = null;
      editPane = (EditPane) c;
    }
    mainPanel.revalidate();
    mainPanel.repaint();
  }

  /**
   * Checks if is splitted.
   *
   * @return boolean
   */
  public boolean isSplitted() {
    return (splitPane != null);
  }
  
  /**
   * 
   */
  private void saveLastConfig() {
    lastSplitConfig = getSplitConfig();
    lastIndex = getSelectedIndex();
    lastCaretPositions.clear();
    EditPane[] editPanes = getEditPanes();
    for (EditPane editPane1 : editPanes) {
      lastCaretPositions.add(editPane1.getTextArea().getCaretPosition());
    }
  }

  /**
   * 
   */
  private void setLastConfig() {
    isHandleValueChangedEnabled = false;
    setSplitConfig(lastSplitConfig);
    try {
      getEditPanes()[lastIndex].focusOnTextArea();
      for(int i = 0; i < lastCaretPositions.size(); i++) {
        try {
          getEditPanes()[i].getTextArea().setCaretPosition(lastCaretPositions.get(i));
        }
        catch (Exception ex) {
          //ignored
        }
      }
    }
    catch (Exception ex) {
      //ignored
    }
    isHandleValueChangedEnabled = true;
  }
  
  /**
   * Sets the split config.
   *
   * @param splitConfig the new split config
   */
  public void setSplitConfig(String splitConfig) {
    try {
      Component comp = restoreSplitConfig(splitConfig);
      setMainContent(comp);
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Restore split config.
   *
   * @param splitConfig the split config
   * @return the component
   * @throws IOException Signals that an I/O exception has occurred.
   */
  private Component restoreSplitConfig(String splitConfig) throws IOException {
    if (Utilities.isEmptyString(splitConfig)) {
      return editPane;
    }

    Stack<Object> stack = new Stack<Object>();
    
    //System.out.println(splitConfig);

    // we create a stream tokenizer for parsing a simple
    // stack-based language
    StreamTokenizer st = new StreamTokenizer(new StringReader(splitConfig));
    st.whitespaceChars(0, ' ');
    /* all printable ASCII characters */
    st.wordChars('#', '~');
    st.commentChar('!');
    st.quoteChar('"');
    st.eolIsSignificant(false);

    loop: while (true) {
      switch (st.nextToken()) {
        case StreamTokenizer.TT_EOF:
          break loop;
        case StreamTokenizer.TT_WORD:
          if (st.sval.equals("vertical") || st.sval.equals("horizontal")) { //$NON-NLS-1$ //$NON-NLS-2$
            int orientation = st.sval.equals("vertical") ? JSplitPane.VERTICAL_SPLIT : JSplitPane.HORIZONTAL_SPLIT; //$NON-NLS-1$
            int divider = (Integer) stack.pop();

            Object obj1 = stack.pop();
            Object obj2 = stack.pop();
            if (obj1 instanceof EditPane) {
              EditPane b1 = (EditPane) obj1;
              obj1 = editPane = createEditPane(b1);
            }
            if (obj2 instanceof EditPane) {
              EditPane b2 = (EditPane) obj2;
              obj2 = createEditPane(b2);
            }

            stack.push(splitPane = new JNPadSplitPane(orientation, (Component)obj1, (Component)obj2));
            splitPane.setDividerLocation(divider);
          }
          else if(st.sval.equals("EditPane")) { //$NON-NLS-1$
            stack.push(editPane);
          }
          break;
        case StreamTokenizer.TT_NUMBER:
          stack.push((int) st.nval);
          break;
      }
    }

    Object obj = stack.peek();

    return (Component)obj;
  }
  
  /**
   * Gets the split config.
   *
   * @return the split config
   */
  public String getSplitConfig() {
    StringBuilder sb = new StringBuilder();

    if (splitPane != null) {
      appendToSplitConfig(splitPane, sb);
    }
    else {
      appendToSplitConfig(sb, editPane);
    }
    
    //System.out.println(sb);

    return sb.toString();
  }  
 
  /**
   * Append to split config.
   *
   * @param splitPane the split pane
   * @param sb the StringBuilder
   */
  private static void appendToSplitConfig(JSplitPane splitPane, StringBuilder sb) {
    Component right = splitPane.getRightComponent();
    appendToSplitConfig(sb, right);

    sb.append(' ');

    Component left = splitPane.getLeftComponent();
    appendToSplitConfig(sb, left);

    sb.append(' ');
    sb.append(splitPane.getDividerLocation());
    sb.append(' ');
    sb.append(splitPane.getOrientation() == JSplitPane.VERTICAL_SPLIT ? 
        "vertical" : "horizontal"); //$NON-NLS-1$ //$NON-NLS-2$
  }
  
  /**
   * Append to split config.
   *
   * @param sb the StringBuilder
   * @param component the component
   */
  private static void appendToSplitConfig(StringBuilder sb, Component component) {
    if(component instanceof JSplitPane) {
      // the component is a JSplitPane
      appendToSplitConfig((JSplitPane)component,sb);
    }
    else {
      sb.append("EditPane"); //$NON-NLS-1$
    }
  }

  /**
   * Gets the initial split config.
   * 
   * @return the initial split config
   */
  public String getInitialSplitConfig() {
    return initialSplitConfig;
  }

  /**
   * Super initial split config.
   * 
   * @param initialSplitConfig the initial split config
   */
  public void setInitialSplitConfig(String initialSplitConfig) {
    this.initialSplitConfig = initialSplitConfig;
  }
  // ---
}
