/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */
package com.sun.star.script.framework.provider.beanshell;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.BadLocationException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.util.List;
import java.util.ArrayList;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PlainSourceView extends JScrollPane implements
    ScriptSourceView, DocumentListener {

    private final ScriptSourceModel model;
    private JTextArea ta;
    private GlyphGutter gg;
    private int linecount;
    private boolean isModified = false;
    private static final String undoKey = "Undo";
    private static final String redoKey = "Redo";
    private CompoundEdit compoundEdit = null;
    private static final int noLimit = -1;
    UndoManager undoManager;
    private List<UnsavedChangesListener> unsavedListener = new ArrayList<UnsavedChangesListener>();

    private static final Pattern tabPattern = Pattern.compile("^ *(\\t)");
    private static final Pattern indentationPattern = Pattern.compile("^([^\\S\\r\\n]*)(([^\\{])*\\{\\s*)*");

    public PlainSourceView(ScriptSourceModel model) {
        this.model = model;
        initUI();
        model.setView(this);
    }

    public void undo(){
        if(compoundEdit!=null){
            compoundEdit.end();
            undoManager.addEdit(compoundEdit);
            compoundEdit = null;
        }
        if(undoManager.canUndo()){
            undoManager.undo();
        }
        // check if it's the last undoable change
        if(undoManager.canUndo() == false){
            setModified(false);
        }
    }
    public void redo(){
        if(undoManager.canRedo()){
            undoManager.redo();
        }
    }
    public void clear() {
        ta.setText("");
    }

    public void update() {
        /* Remove ourselves as a DocumentListener while loading the source
           so we don't get a storm of DocumentEvents during loading */
        ta.getDocument().removeDocumentListener(this);

        if (!isModified) {
            int pos = ta.getCaretPosition();
            ta.setText(model.getText());

            try {
                ta.setCaretPosition(pos);
            } catch (IllegalArgumentException iae) {
                // do nothing and allow JTextArea to set its own position
            }
        }

        // scroll to currentPosition of the model
        try {
            int line = ta.getLineStartOffset(model.getCurrentPosition());
            Rectangle rect = ta.modelToView(line);
            if (rect != null) {
                ta.scrollRectToVisible(rect);
            }
        } catch (BadLocationException e) {
            // couldn't scroll to line, do nothing
        }

        gg.repaint();

        // Add back the listener
        ta.getDocument().addDocumentListener(this);
    }

    public boolean isModified() {
        return isModified;
    }

    private void notifyListeners (boolean isUnsaved) {
        for (UnsavedChangesListener listener : unsavedListener) {
            listener.onUnsavedChanges(isUnsaved);
        }
    }

    public void setModified(boolean value) {
        if(value != isModified) {
            notifyListeners(value);
            isModified = value;
        }
    }

    private void initUI() {
        try{
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }
        catch(Exception e){
                // What to do here
        }
        ta = new JTextArea();
        ta.setTabSize(4);
        ta.setRows(15);
        ta.setColumns(40);
        ta.setLineWrap(false);
        ta.insert(model.getText(), 0);
        ta.setFont(new Font("Monospaced", ta.getFont().getStyle(), ta.getFont().getSize()));
        undoManager = new UndoManager();
        undoManager.setLimit(noLimit);
        ta.getDocument().addUndoableEditListener(new UndoableEditListener(){
            @Override
            public void undoableEditHappened(UndoableEditEvent editEvent) {
                if(compoundEdit == null){
                    compoundEdit = new CompoundEdit();
                }
                compoundEdit.addEdit(editEvent.getEdit());
            }
        });

        ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_MASK), undoKey);
        ta.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_MASK), redoKey);

        ta.addKeyListener(new KeyAdapter(){
            @Override
            public void keyPressed(KeyEvent ke) {
                // if shift + tab was pressed, remove the first tab before any code begins
                if (ke.isShiftDown() && ke.getKeyCode() == KeyEvent.VK_TAB) {
                    try {
                        int caretOffset = ta.getCaretPosition();
                        int lineOffset = ta.getLineOfOffset(caretOffset);
                        int startOffset = ta.getLineStartOffset(lineOffset);
                        int endOffset = ta.getLineEndOffset(lineOffset);

                        Matcher matcher = tabPattern.matcher(ta.getText(startOffset, endOffset - startOffset));
                        if (matcher.find()) {
                            ta.replaceRange(null, startOffset + matcher.start(1), startOffset + matcher.end(1));
                        }
                    } catch (BadLocationException e) {
                        // could not find correct location of the tab
                    }
                }
                // if the enter key was pressed, adjust indentation of the current line accordingly
                if (ke.getKeyCode() == KeyEvent.VK_ENTER) {
                    try {
                        int caretOffset = ta.getCaretPosition();
                        int lineOffset = ta.getLineOfOffset(caretOffset);
                        int startOffset = ta.getLineStartOffset(lineOffset);
                        int endOffset = ta.getLineEndOffset(lineOffset);

                        Matcher matcher = indentationPattern.matcher(ta.getText(startOffset, endOffset - startOffset));
                        // insert new line including indentation of the previous line
                        ta.insert("\n", caretOffset++);
                        if (matcher.find()) {
                            if (matcher.group(1).length() > 0) {
                                ta.insert(matcher.group(1), caretOffset++);
                            }
                            // if there is an open curly bracket in the current line, increase indentation level
                            if (matcher.group(3) != null) {
                                ta.insert("\t", caretOffset);
                            }
                        }
                        ke.consume();
                    } catch (BadLocationException e) {
                        // could not find correct location of the indentation
                    }
                }
            }

            @Override
            public void keyReleased(KeyEvent ke){
                if(ke.getKeyCode() == KeyEvent.VK_SPACE || ke.getKeyCode() == KeyEvent.VK_ENTER){
                    compoundEdit.end();
                    undoManager.addEdit(compoundEdit);
                    compoundEdit = null;
                }
            }
        });

        ta.getActionMap().put(undoKey, new AbstractAction(undoKey){
            @Override
            public void actionPerformed(ActionEvent event) {
                undo();
            }
        });

        ta.getActionMap().put(redoKey, new AbstractAction(redoKey){
            @Override
            public void actionPerformed(ActionEvent event) {
                redo();
            }
        });

        linecount = ta.getLineCount();

        gg = new GlyphGutter(this);

        setViewportView(ta);
        setRowHeaderView(gg);

        ta.getDocument().addDocumentListener(this);
    }

    /* Implementation of DocumentListener interface */
    public void insertUpdate(DocumentEvent e) {
        doChanged();
    }

    public void removeUpdate(DocumentEvent e) {
        doChanged();
    }

    public void changedUpdate(DocumentEvent e) {
        doChanged();
    }

    /* If the number of lines in the JTextArea has changed then update the
       GlyphGutter */
    private void doChanged() {
        setModified(true);

        if (linecount != ta.getLineCount()) {
            gg.update();
            linecount = ta.getLineCount();
        }
    }

    public String getText() {
        return ta.getText();
    }

    public JTextArea getTextArea() {
        return ta;
    }

    public int getCurrentPosition() {
        return model.getCurrentPosition();
    }

    public void addListener(UnsavedChangesListener toAdd) {
        unsavedListener.add(toAdd);
    }
}

class GlyphGutter extends JComponent {

    private final PlainSourceView view;
    private static final String DUMMY_STRING = "99";

    GlyphGutter(PlainSourceView view) {
        this.view = view;
        update();
    }

    public void update() {
        JTextArea textArea = view.getTextArea();
        Font font = textArea.getFont();
        setFont(font);

        FontMetrics metrics = getFontMetrics(font);
        int h = metrics.getHeight();
        int lineCount = textArea.getLineCount() + 1;

        String dummy = Integer.toString(lineCount);

        if (dummy.length() < 2) {
            dummy = DUMMY_STRING;
        }

        Dimension d = new Dimension();
        d.width = metrics.stringWidth(dummy) + 16;
        d.height = lineCount * h + 100;
        setPreferredSize(d);
        setSize(d);
    }

    @Override
    public void paintComponent(Graphics g) {
        JTextArea textArea = view.getTextArea();

        Font font = textArea.getFont();
        g.setFont(font);

        FontMetrics metrics = getFontMetrics(font);
        Rectangle clip = g.getClipBounds();

        g.setColor(getBackground());
        g.fillRect(clip.x, clip.y, clip.width, clip.height);

        int ascent = metrics.getMaxAscent();
        int h = metrics.getHeight();
        int lineCount = textArea.getLineCount() + 1;

        int startLine = clip.y / h;
        int endLine = (clip.y + clip.height) / h + 1;
        int width = getWidth();

        if (endLine > lineCount) {
            endLine = lineCount;
        }

        for (int i = startLine; i < endLine; i++) {
            String text;
            text = Integer.toString(i + 1) + " ";
            int y = i * h;
            g.setColor(Color.blue);
            g.drawString(text, 0, y + ascent);
            int x = width - ascent;

            // if currentPosition is not -1 then a red arrow will be drawn
            if (i == view.getCurrentPosition()) {
                drawArrow(g, ascent, x, y);
            }
        }
    }

    private void drawArrow(Graphics g, int ascent, int x, int y) {
        Polygon arrow = new Polygon();
        int dx = x;
        y += ascent - 10;
        int dy = y;
        arrow.addPoint(dx, dy + 3);
        arrow.addPoint(dx + 5, dy + 3);

        for (x = dx + 5; x <= dx + 10; x++, y++) {
            arrow.addPoint(x, y);
        }

        for (x = dx + 9; x >= dx + 5; x--, y++) {
            arrow.addPoint(x, y);
        }

        arrow.addPoint(dx + 5, dy + 7);
        arrow.addPoint(dx, dy + 7);

        g.setColor(Color.red);
        g.fillPolygon(arrow);
        g.setColor(Color.black);
        g.drawPolygon(arrow);
    }
}

