/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
/*
 * OutputEditorKit.java
 *
 * Created on May 9, 2004, 4:34 PM
 */

package org.netbeans.core.output2;

import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import javax.swing.Action;
import javax.swing.JEditorPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.TextAction;
import org.openide.util.Exceptions;

/**
 * A simple editor kit which provides instances of ExtPlainView/ExtWrappedPlainView as its views.
 *
 * @author  Tim Boudreau
 */
final class OutputEditorKit extends DefaultEditorKit implements javax.swing.text.ViewFactory, ChangeListener {
    private final boolean wrapped;
    private final JTextComponent comp;
    private static final Action[] actions = prepareActions();
    private final PropertyChangeListener propertyChangeListener;

    /** Creates a new instance of OutputEditorKit */
    OutputEditorKit(boolean wrapped, JTextComponent comp,
            PropertyChangeListener propertyChangeListener) {
        this.comp = comp;
        this.wrapped = wrapped;
        this.propertyChangeListener = propertyChangeListener;        
    }
    
    @Override
    public Action[] getActions() {
        return actions;
    }
    
    public WrappedTextView view() {
        return lastWrappedView;
    }

    private WrappedTextView lastWrappedView = null;
    public javax.swing.text.View create(Element element) {
        javax.swing.text.View result = wrapped
                ? new WrappedTextView(element, comp, propertyChangeListener)
                : new ExtPlainView(element);
        lastWrappedView = wrapped ? (WrappedTextView) result : null;
        return result;
    }

    @Override
    public javax.swing.text.ViewFactory getViewFactory() {
        return this;
    }

    public boolean isWrapped() {
        return wrapped;
    }
    
    @Override
    public void install(JEditorPane c) {
        super.install(c);
        if (wrapped) {
            c.getCaret().addChangeListener(this);
        }
    }
    
    @Override
    public void deinstall(JEditorPane c) {
        super.deinstall(c);
        if (wrapped) {
            c.getCaret().removeChangeListener(this);
        }
    }    
    
    private int lastMark = -1;
    private int lastDot = -1;
    private static final Rectangle scratch = new Rectangle();
    
    /**
     * Manages repainting when the selection changes
     */
    public void stateChanged (ChangeEvent ce) {
        int mark = comp.getSelectionStart();
        int dot = comp.getSelectionEnd();
        boolean hasSelection = mark != dot;
        boolean hadSelection = lastMark != lastDot;
        
//        System.err.println("Change: " + mark + " : " + dot + "/" + lastMark + ":" + lastDot + " hadSelection " + hadSelection + " hasSelection " + hasSelection);
        
        if (lastMark != mark || lastDot != dot) {
            int begin = Math.min (mark, dot);
            int end = Math.max (mark, dot);
            int oldBegin = Math.min (lastMark, lastDot);
            int oldEnd = Math.max (lastMark, lastDot);
            
            if (hadSelection && hasSelection) {
                if (begin != oldBegin) {
                    int startChar = Math.min (begin, oldBegin);
                    int endChar = Math.max (begin, oldBegin);
                    repaintRange (startChar, endChar);
                } else {
                    int startChar = Math.min (end, oldEnd);
                    int endChar = Math.max (end, oldEnd);
                    repaintRange (startChar, endChar);
                }
            } else if (hadSelection && !hasSelection) {
                repaintRange (oldBegin, oldEnd);
            } 
            
        }
        lastMark = mark;
        lastDot = dot;
    }
    
    private void repaintRange (int start, int end) {
        try {
            Rectangle r = (Rectangle) view().modelToView(end, scratch, Position.Bias.Forward);
            int y1 = r.y + r.height;
            r = (Rectangle) view().modelToView(start, scratch, Position.Bias.Forward);
            r.x = 0;
            r.width = comp.getWidth();
            r.height = y1 - r.y;
//            System.err.println("RepaintRange " + start + " to " + end + ": " + r);
            comp.repaint (r);
        } catch (BadLocationException e) {
            comp.repaint();
            Exceptions.printStackTrace(e);
        }
    }
    
    /*
     * Replaces DefaultEditorKit actions which uses Utilities.getRowEnd()/getRowStart()
     * which are very expensive for long lines in unwrapped mode
     */
    static Action[] prepareActions() {
        DefaultEditorKit dek = new DefaultEditorKit();
        Action[] defActions = dek.getActions();
        Action[] newActions = new Action[defActions.length];
        for (int i = 0; i < defActions.length; i++) {
            Object actionName = defActions[i].getValue(Action.NAME);
            if (actionName.equals(beginLineAction)) {
                newActions[i] = new OutputBeginLineAction(beginLineAction, false);
            } else if (actionName.equals(selectionBeginLineAction)) {
                newActions[i] = new OutputBeginLineAction(selectionBeginLineAction, true);
            } else if (actionName.equals(endLineAction)) {
                newActions[i] = new OutputEndLineAction(endLineAction, false);
            } else if (actionName.equals(selectionEndLineAction)) {
                newActions[i] = new OutputEndLineAction(selectionEndLineAction, true);
            } else {
                newActions[i] = defActions[i];
            }            
        }
        return newActions;
    }    

    /*
     * Position the caret to the beginning of the line.
     * @see DefaultEditorKit#beginLineAction
     * @see DefaultEditorKit#selectBeginLineAction
     * @see DefaultEditorKit#getActions
     */
    static class OutputBeginLineAction extends TextAction {

        /** 
         * Create this action with the appropriate identifier. 
         * @param nm  the name of the action, Action.NAME.
         * @param select whether to extend the selection when
         *  changing the caret position.
         */
        OutputBeginLineAction(String nm, boolean select) {
            super(nm);
            this.select = select;
        }

        /** The operation to perform when this action is triggered. */
        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            if (target != null) {
                Document doc = target.getDocument();
                Element map = doc.getDefaultRootElement();
                int offs = target.getCaretPosition();
                int lineIndex = map.getElementIndex(offs);
                int lineStart = map.getElement(lineIndex).getStartOffset();

                if (select) {
                    target.moveCaretPosition(lineStart);
                } else {
                    target.setCaretPosition(lineStart);
                }
            }
        }

        private boolean select;
    }
    
    /*
     * Position the caret to the end of the line.
     * @see DefaultEditorKit#endLineAction
     * @see DefaultEditorKit#selectEndLineAction
     * @see DefaultEditorKit#getActions
     */
    static class OutputEndLineAction extends TextAction {

        /** 
         * Create this action with the appropriate identifier. 
         * @param nm  the name of the action, Action.NAME.
         * @param select whether to extend the selection when
         *  changing the caret position.
         */
        OutputEndLineAction(String nm, boolean select) {
            super(nm);
            this.select = select;
        }

        /** The operation to perform when this action is triggered. */
        public void actionPerformed(ActionEvent e) {
            JTextComponent target = getTextComponent(e);
            if (target != null) {
                Document doc = target.getDocument();
                Element map = doc.getDefaultRootElement();
                int offs = target.getCaretPosition();
                int lineIndex = map.getElementIndex(offs);
                int lineEnd = map.getElement(lineIndex).getEndOffset() - 1;

                if (select) {
                    target.moveCaretPosition(lineEnd);
                } else {
                    target.setCaretPosition(lineEnd);
                }
            }            
        }

        private boolean select;
    }    
}
