package com.fitbank.javascript;

import com.fitbank.js.JavascriptFormater;
import java.awt.Color;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JEditorPane;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.undo.UndoManager;

/**
 * Clase que extiende un JEditorPane para poder editar javascript.
 * 
 * @author Fit-Bank
 */
public class JavascriptEditorPane extends JEditorPane {
    /**
	 * 
	 */
    private static final long serialVersionUID = 1L;
    private static final String TABSPACE = "  ";
    private DefaultHighlighter.HighlightPainter hpB = new DefaultHighlighter.DefaultHighlightPainter(
            new Color(119, 153, 255));
    private DefaultHighlighter.HighlightPainter hpE = new DefaultHighlighter.DefaultHighlightPainter(
            new Color(255, 119, 119));
    private DefaultHighlighter.HighlightPainter[] hpP = {
            new DefaultHighlighter.DefaultHighlightPainter(new Color(204, 255,
                    153)),
            new DefaultHighlighter.DefaultHighlightPainter(new Color(153, 204,
                    51)) };
    private DefaultHighlighter h = new DefaultHighlighter();
    private Stack<Object> highlights = new Stack<Object>();
    private Stack<Object> busquedas = new Stack<Object>();
    private Stack<Object> resaltados = new Stack<Object>();
    private JPopupMenu menu = new JPopupMenu("Menu Contextual");
    private UndoManager manejador = new UndoManager();
    private int cuenta = 0;
    private boolean cambiando = false;
    private boolean editarEventos = true;

    /**
     * Constructor por defecto.
     */
    public JavascriptEditorPane() {
        super();
        jbInit();
    }

    /**
     * Constructor con contenido.
     */
    public JavascriptEditorPane(String contenido) {
        super();
        jbInit();
        setText(contenido);
        setCaretPosition(0);
    }

    public void jbInit() {
        JavascriptEditorKit kit = new JavascriptEditorKit();
        setEditorKitForContentType("text/javascript", kit);
        setContentType("text/javascript");
        setFont(new Font("Courier", 0, 12));
        setEditable(true);
        setHighlighter(h);
        h.install(this);

        ActionListener l = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                JavascriptEditorPane.this.menuActionPerformed(e);
            }
        };

        JMenuItem item;
        menu.add(item = new JMenuItem("Deshacer"));
        item.addActionListener(l);
        menu.add(item = new JMenuItem("Rehacer"));
        item.addActionListener(l);
        menu.addSeparator();
        menu.add(item = new JMenuItem("Cortar"));
        item.addActionListener(l);
        menu.add(item = new JMenuItem("Copiar"));
        item.addActionListener(l);
        menu.add(item = new JMenuItem("Pegar"));
        item.addActionListener(l);
        menu.add(item = new JMenuItem("Seleccionar Todo"));
        item.addActionListener(l);
        // menu.addSeparator();
        // menu.add(item = new JMenuItem("Formatear"));
        // item.addActionListener(l);
        menu.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuCanceled(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                if (JavascriptEditorPane.this.getSelectionStart() != JavascriptEditorPane.this
                        .getSelectionEnd()) {
                    ((JMenuItem) menu.getComponents()[3]).setEnabled(true);
                    ((JMenuItem) menu.getComponents()[4]).setEnabled(true);
                } else {
                    ((JMenuItem) menu.getComponents()[3]).setEnabled(false);
                    ((JMenuItem) menu.getComponents()[4]).setEnabled(false);
                }

                Transferable t = Toolkit.getDefaultToolkit()
                        .getSystemClipboard().getContents(this);

                if (t != null
                        && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
                    ((JMenuItem) menu.getComponents()[5]).setEnabled(true);
                } else {
                    ((JMenuItem) menu.getComponents()[5]).setEnabled(false);
                }

                if (manejador.canUndo()) {
                    ((JMenuItem) menu.getComponents()[0]).setEnabled(true);
                } else {
                    ((JMenuItem) menu.getComponents()[0]).setEnabled(false);
                }

                if (manejador.canRedo()) {
                    ((JMenuItem) menu.getComponents()[1]).setEnabled(true);
                } else {
                    ((JMenuItem) menu.getComponents()[1]).setEnabled(false);
                }
            }
        });
        this.add(menu);
        getDocument().addUndoableEditListener(manejador);
    }

    /**
     * Devuelve el numero de resultados de la ultima busqueda.
     * 
     * @return numero de resultados de la ultima busqueda
     */
    public int getCuenta() {
        return cuenta;
    }

    /**
     * Maneja eventos del menu.
     */
    public void menuActionPerformed(ActionEvent e) {
        if (e.getActionCommand().equals("Deshacer")) {
            if (manejador.canUndo()) {
                manejador.undo();
            }
        } else if (e.getActionCommand().equals("Rehacer")) {
            if (manejador.canRedo()) {
                manejador.redo();
            }
        } else if (e.getActionCommand().equals("Formatear")) {
            formatear(editarEventos);
        } else if (e.getActionCommand().equals("Cortar")) {
            cut();
        } else if (e.getActionCommand().equals("Copiar")) {
            copy();
        } else if (e.getActionCommand().equals("Pegar")) {
            paste();
        } else if (e.getActionCommand().equals("Seleccionar Todo")) {
            selectAll();
        }
    }

    /**
     * Resalta los pares en el documento.
     */
    protected void resaltarPares() {
        if (!cambiando) {
            int pos = getCaretPosition();

            try {
                while (!highlights.empty()) {
                    h.removeHighlight(highlights.pop());
                }

                int b = 0;

                for (int a = pos > 0 ? pos - 1 : pos; a <= pos && a >= 0
                        && a < this.getText().length(); a++) {
                    int c = -1;

                    if (this.getText(a, 1).equals("{")) {
                        c = buscarPareja("{", "}", a);
                    } else if (this.getText(a, 1).equals("[")) {
                        c = buscarPareja("[", "]", a);
                    } else if (this.getText(a, 1).equals("(")) {
                        c = buscarPareja("(", ")", a);
                    } else if (this.getText(a, 1).equals("}")) {
                        c = buscarParejaAnterior("{", "}", a);
                    } else if (this.getText(a, 1).equals("]")) {
                        c = buscarParejaAnterior("[", "]", a);
                    } else if (this.getText(a, 1).equals(")")) {
                        c = buscarParejaAnterior("(", ")", a);
                    } else {
                        c = -2;
                    }

                    if (c > -1) {
                        highlights.push(h.addHighlight(a, a + 1, hpP[b]));
                        highlights.push(h.addHighlight(c, c + 1, hpP[b]));
                        b = 1;
                    } else if (c == -1) {
                        highlights.push(h.addHighlight(a, a + 1, hpE));
                    }
                }
            } catch (BadLocationException b) {
                System.err.println("Error al resaltar pares");
            }
        }
    }

    /**
     * Metodo que controla el movimiento del cursor.
     * 
     * @param e
     */
    @Override
    protected void fireCaretUpdate(CaretEvent e) {
        resaltarPares();
        super.fireCaretUpdate(e);
    }

    /**
     * Busca una cadena en el texto y la resalta.
     * 
     * @param valorInicial
     *            es el valor inicial de busqueda
     * @return String con el valor que se buscó
     */
    public String buscar(String valorInicial) {
        return this.buscar(valorInicial, true);
    }

    /**
     * Busca una cadena en el texto y la resalta.
     * 
     * @param valorInicial
     *            es el valor inicial de busqueda
     * @return String con el valor que se buscó
     */
    public String buscar(String valorInicial, boolean mensaje) {
        String buscar = valorInicial;

        cuenta = 0;

        while (!busquedas.empty()) {
            h.removeHighlight(busquedas.pop());
        }

        if (buscar != null && mensaje) {
            buscar = JOptionPane.showInputDialog("Busqueda", valorInicial);
        }

        if (buscar != null) {
            Pattern p = Pattern.compile(buscar);
            Matcher m = p.matcher(this.getText());

            try {
                while (buscar != null && !buscar.equals("") && m.find()) {
                    busquedas.push(h.addHighlight(m.start(), m.end(), hpB));
                    cuenta++;
                }
            } catch (BadLocationException b) {
                System.err.println("Error al buscar");
            }
        }

        return buscar;
    }

    /**
     * Resalta un texto.
     * 
     * @param regex
     *            Expresion regular a buscar y reemplazar
     * @param hp
     *            Un DefaultHighlighter.HighlightPainter para resaltar
     */
    public void resaltar(String regex, DefaultHighlighter.HighlightPainter hp) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(this.getText());

        cuenta = 0;

        try {
            while (m.find()) {
                this.resaltar(m.start(), m.end(), hp);
                cuenta++;
            }
        } catch (BadLocationException b) {
            System.err.println("Error al resaltar");
        }
    }

    /**
     * Resalta un texto.
     * 
     * @param offs
     *            Inicio del resaltado
     * @param len
     *            Longitud del resaltado
     * @param hp
     *            Un DefaultHighlighter.HighlightPainter para resaltar
     */
    public void resaltar(int offs, int len,
            DefaultHighlighter.HighlightPainter hp) throws BadLocationException {
        resaltados.push(h.addHighlight(offs, len, hp));
    }

    /**
     * Quita los resaltados.
     */
    public void quitarResaltados() {
        while (!resaltados.empty()) {
            h.removeHighlight(resaltados.pop());
        }
    }

    /**
     * Inserta una cadena en el texto actual.
     * 
     * @param offs
     *            Posicion
     * @param str
     *            Expresion regular a buscar y reemplazar
     */
    public void insert(int offs, String str) {
        try {
            cambiando = true;

            int car = getCaretPosition() + str.length();
            ((JavascriptDocument) getDocument()).insert(offs, str);

            if (car >= offs) {
                setCaretPosition(car);
            }
            cambiando = false;
        } catch (BadLocationException e) {
            System.err.println("Error al insertar");
        }
    }

    /**
     * Remueve una cadena en el texto actual.
     * 
     * @param offs
     *            Posicion
     * @param len
     *            tamaño de la cadena
     */
    public void remove(int offs, int len) {
        try {
            cambiando = true;

            int car = getCaretPosition() - len;
            ((JavascriptDocument) getDocument()).remove(offs, len);

            if (car > offs) {
                setCaretPosition(car);
            }
            cambiando = false;
        } catch (BadLocationException e) {
            System.err.println("Error al remover");
        }
    }

    /**
     * Procesa eventos del teclado.
     * 
     * @param e
     */
    @Override
    protected void processKeyEvent(KeyEvent e) {
        cambiando = true;

        if (e.getID() == KeyEvent.KEY_PRESSED) {
            try {
                String doc = this.getText();
                int pos = getCaretPosition();

                if (getSelectionStart() == getSelectionEnd()) {
                    if (e.getKeyChar() == '}') {
                        int a = 0;

                        while (doc.charAt(--pos) == ' ' && a++ < TABSPACE.length()) {
                            this.remove(pos, 1);
                        }
                    }

                    if (e.getModifiers() == 0) {
                        switch (e.getKeyCode()) {
                        case KeyEvent.VK_ENTER:

                            int tabs = this.getText(0, pos).lastIndexOf("\n") + 1;
                            if (tabs != 0) {
                                while (tabs < doc.length()
                                        && doc.charAt(tabs++) == ' ') {
                                    insert(pos, " ");
                                }

                                if (this.getText(pos - 1, 1).equals("{")) {
                                    insert(pos, TABSPACE);
                                }
                            }
                            insert(pos, "\n");

                            e.consume();

                            break;
                        }
                    }
                } else if (getSelectionStart() != getSelectionEnd() && e.getKeyCode() == KeyEvent.VK_TAB) {
                    int inicio;

                    if (this.getText(getSelectionStart(), 1).equals("\n")) {
                        inicio = getSelectionStart();
                    } else {
                        inicio = this.getText(0, getSelectionStart())
                                .lastIndexOf("\n");
                        inicio = inicio > 0 ? inicio : 0;
                    }

                    int fin = getSelectionEnd();

                    if (fin < getLength()
                            && !this.getText(fin, 1).equals("\n")) {
                        int desp = this.getText(fin, getLength() - fin)
                                .indexOf("\n");

                        if (desp > 0) {
                            fin += desp;
                        } else {
                            fin = getLength();
                        }
                    }

                    int tabs = 0;

                    if (e.getModifiers() == 0) {
                        tabs = tab(inicio, fin);
                        e.consume();
                        setSelectionStart(inicio);
                        setSelectionEnd(fin + tabs);
                    } else if (e.getModifiers() == InputEvent.SHIFT_MASK) {
                        Pattern p = Pattern.compile("\\n" + TABSPACE);
                        Matcher m = p.matcher(this.getText(inicio, fin - 1
                                - inicio));

                        if (inicio == 0 && this.getText(0, 2).equals(TABSPACE)) {
                            this.remove(0, TABSPACE.length());
                            tabs -= TABSPACE.length();
                        }

                        while (m.find()) {
                            this.remove(m.start() + inicio + tabs + 1, TABSPACE
                                    .length());
                            tabs -= TABSPACE.length();
                        }

                        e.consume();
                        setSelectionStart(inicio);
                        setSelectionEnd(fin + tabs);
                    }
                }
            } catch (BadLocationException b) {
                System.err.println("Error al procesar el teclado");
            }
        }

        cambiando = false;
        super.processKeyEvent(e);
        ((JavascriptDocument) getDocument()).pintar(getCaretPosition());
    }

    /**
     * Añade tab a una seccion del texto
     * 
     * @param inicio
     * @param fin
     * @return devuelve el numero de espacios que se aumentaron en total
     * @throws javax.swing.text.BadLocationException
     */
    private int tab(int inicio, int fin) throws BadLocationException {
        int tabs = 0;
        Pattern p = Pattern.compile("\\n");
        Matcher m = p.matcher(this.getText(inicio, fin - 1 - inicio));

        if (inicio == 0) {
            insert(0, TABSPACE);
            tabs += TABSPACE.length();
        }

        while (m.find()) {
            insert(m.start() + inicio + tabs + 1, TABSPACE);
            tabs += TABSPACE.length();
        }

        ((JavascriptDocument) getDocument()).pintar();

        return tabs;
    }

    /**
     * Busca la pareja de un caracter
     * 
     * @param i
     *            Caracter
     * @param o
     *            Pareja
     * @param inicio
     *            Posicion inicio
     * @return posicion de la pareja
     */
    private int buscarPareja(String i, String o, int inicio) {
        int abre = inicio;
        int cierra = inicio;

        do {
            abre++;
            cierra++;
        } while ((abre = this.getText().indexOf(i, abre)) < (cierra = this
                .getText().indexOf(o, cierra))
                && abre != -1 && cierra != -1);

        return cierra;
    }

    /**
     * Busca la pareja anterior de un caracter
     * 
     * @param i
     *            Caracter
     * @param o
     *            Pareja
     * @param inicio
     *            Posicion inicio
     * @return posicion de la pareja
     */
    private int buscarParejaAnterior(String i, String o, int inicio) {
        int abre = inicio;
        int cierra = inicio;

        do {
            abre--;
            cierra--;
        } while ((abre = this.getText().lastIndexOf(i, abre)) < (cierra = this
                .getText().lastIndexOf(o, cierra))
                && abre != -1 && cierra != -1);

        return abre;
    }

    /**
     * Esta funcion da formato al texto.
     */
    public void formatear(boolean multiEvento) {
        JavascriptFormater javascriptFormater = new JavascriptFormater(this
                .getText(), false, multiEvento);
        setText(javascriptFormater.formatear());
        editarEventos = multiEvento;
        setCaretPosition(0);
        limpiarHistorial();

        ((JavascriptDocument) getDocument()).pintarTodo();
    }

    /**
     * Cambia el texto de este control.
     * 
     * @param t
     *            Es el nuevo texto
     */
    @Override
    public void setText(String t) {
        cambiando = true;
        t = t.replaceAll("\r\n", "\n");
        super.setText(t);
        cambiando = false;
        ((JavascriptDocument) getDocument()).pintarTodo();
    }

    /**
     * Devuelve el tamaño del texto.
     * 
     * @return el tamaño
     */
    public int getLength() {
        return this.getText().length();
    }

    /**
     * Maneja eventos del mouse.
     * 
     * @param e
     */
    @Override
    protected void processMouseEvent(MouseEvent e) {
        if (e.isPopupTrigger()) {
            this.requestFocus();
            menu.show(e.getComponent(), e.getX(), e.getY());
        } else {
            super.processMouseEvent(e);
        }
    }

    /**
     * Limpia el historial de deshacer.
     */
    public void limpiarHistorial() {
        manejador.discardAllEdits();
    }

    /**
     * Agrega un submenu al menu contextual.
     * 
     * @param submenu
     *            A ser añadido
     */
    public void agregarSubMenu(JMenu submenu) {
        menu.add(submenu);
    }

    /**
     * Quita el submenu del menu contextual.
     * 
     * @param submenu
     *            A ser quitado
     */
    public void quitarSubMenu(JMenu submenu) {
        menu.remove(submenu);
    }

    /**
     * Metodo sobreescrito.
     */
    @Override
    public void cut() {
        super.cut();
        ((JavascriptDocument) getDocument()).pintarTodo();
    }

    /**
     * Metodo sobreescrito.
     */
    @Override
    public void paste() {
        super.paste();
        ((JavascriptDocument) getDocument()).pintarTodo();
    }

    /**
     * Destruye el objeto.
     */
    public void dispose() {
        CaretListener[] cl = getCaretListeners();
        for (CaretListener element : cl) {
            removeCaretListener(element);
        }
        getDocument().removeUndoableEditListener(manejador);
    }

    public void setEditarEventos(boolean editarEventos) {
        this.editarEventos = editarEventos;
    }
}
