/**
Make_Get_and_Set_Functions.bsh - a BeanShell macro for
the jEdit text editor  that creates simple get() and set()
methods for the variables on selected lines.

Copyright (C)  2004 Thomas Galvin - software@thomas-galvin.com
based on Make_Get_and_Set_Methods.bsh by John Gellene

This macro will work on multiple selected lines; for instance,
selecting

<code>
public int foo;
public int bar;
</code>

and running the macro will produce get and set functions for both
variables, along with comments.  This macro produces c-style
functions, unless the buffer is in java mode.

Modifications by Dale Anson, Dec 2008:

1. Allows variable declarations to have an initial assignment, like
<code>
public int foo = 1;
public int bar = 2;
</code>

2. Allows multiple variables on same line, like
<code>
public int foo, bar;
</code>

3. Use line separator as set in buffer properties rather than always using \n.

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

// Localization
final static String DONE = jEdit.getProperty("macro.rs.MakeGetAndSetMethods.GenerateCode.label", "Generate Code");
final static String CANCEL = jEdit.getProperty("common.cancel");
final static String CreateGetMethodsLabel = jEdit.getProperty("macro.rs.MakeGetAndSetMethods.CreateGetMethods.label", "Create Get Methods");
final static String CreateSetMethodsLabel = jEdit.getProperty("macro.rs.MakeGetAndSetMethods.CreateSetMethods.label", "Create Set Methods");
final static String CreateGetandSetMethodsLabel = jEdit.getProperty("macro.rs.MakeGetAndSetMethods.CreateGetandSetMethods.label", "Create Get and Set Methods");
    
//Process
boolean JAVA_MODE = buffer.getMode().getName().equals( "java" );

// use line separator from current buffer
String LS = buffer.getStringProperty( "lineSeparator" );
if (LS == null) {
    // otherwise, use default line separator
    LS = jEdit.getProperty("buffer.lineSeparator");   
}

boolean createGetMethods = true;
boolean createSetMethods = true;

void setCaret( int selectionStart, int selectionEnd ) {
    textArea.setCaretPosition( selectionStart );
    textArea.moveCaretPosition( selectionEnd );
}

String getClassName() {
    int selectionStart;
    int selectionEnd;
    if(textArea.getSelection().length != 0){  // if there are selections exists
        selectionStart = textArea.getSelection(0).getStart();
        selectionEnd = textArea.getSelection(0).getEnd();
    }
    else {		// if no selection
        selectionStart = textArea.getCaretPosition();
        selectionEnd = textArea.getCaretPosition();
    }

    String text = textArea.getText();
    int index = text.lastIndexOf( "class", selectionStart );
    if ( index != -1 ) {
        textArea.setCaretPosition( index );
        int lineNumber = textArea.getCaretLine();
        int lineEnd = textArea.getLineEndOffset( lineNumber );
        String lineText = text.substring( index, lineEnd );

        StringTokenizer tokenizer = new StringTokenizer( lineText );
        tokenizer.nextToken(); //eat "class"
        if ( tokenizer.hasMoreTokens() ) {
            setCaret( selectionStart, selectionEnd );
            return tokenizer.nextToken();
        }
    }
    setCaret( selectionStart, selectionEnd );

    String fileClassName = buffer.getName();
    int index = fileClassName.lastIndexOf( '.' );
    if ( index != -1 ) {
        fileClassName = fileClassName.substring( 0, index );
        if ( fileClassName.toLowerCase().indexOf( "untitled" ) == -1 ) {
            return fileClassName;
        }
    }

    return "";
}

String createJavaGetMethod( String type, String variableName ) {
    String uppperVariable = Character.toUpperCase( variableName.charAt( 0 ) ) + variableName.substring( 1, variableName.length() );
    String result =
        "\t/**" + LS +
        "\t * Returns the value of " + variableName + "." + LS +
        "\t */" + LS +
        "\tpublic " + type + " get" + uppperVariable + "() {" + LS +
        "\t\treturn " + variableName + ";" + LS +
        "\t}" + LS;

    return result;
}

String createJavaSetMethod( String type, String variableName ) {
    String uppperVariable = Character.toUpperCase( variableName.charAt( 0 ) ) + variableName.substring( 1, variableName.length() );
    String result =
        "\t/**" + LS +
        "\t * Sets the value of " + variableName + "." + LS +
        "\t * @param " + variableName + " The value to assign " + variableName + "." + LS +
        "\t */" + LS +
        "\tpublic void set" + uppperVariable + "(" + type + " " + variableName + ") {" + LS +
        "\t\tthis." + variableName + " = " + variableName + ";" + LS +
        "\t}" + LS;

    return result;
}

String createCppGetMethod( String className, String type, String variableName ) {
    String scopeIndicator = "";
    if ( className != null && className.compareTo( "" ) != 0 ) {
        scopeIndicator = className + "::";
    }
    if (type == null) {
        type = "";   
    }
    String uppperVariable = Character.toUpperCase( variableName.charAt( 0 ) ) + variableName.substring( 1, variableName.length() );
    String result =
        "/*" + LS +
        "function: get" + uppperVariable + "()" + LS +
        "Returns the value of " + variableName + "." + LS +
        "*/" + LS +
        type + (type.length() > 0 ? " " : "") + scopeIndicator + "get" + uppperVariable + "()" + "" + LS +
        "{" + "" + LS +
        "  return " + variableName + ";" + "" + LS +
        "}" + LS;

    return result;
}

String createCppSetMethod( String className, String type, String variableName ) {
    String scopeIndicator = "";
    if ( className != null && className.compareTo( "" ) != 0 ) {
        scopeIndicator = className + "::";
    }

    String uppperVariable = Character.toUpperCase( variableName.charAt( 0 ) ) + variableName.substring( 1, variableName.length() );
    String setVariable = variableName + "Value";
    String result =
        "/*" + LS +
        "function: set" + uppperVariable + "()" + LS +
        "Sets the value of " + variableName + "." + LS +
        "Input: " + setVariable + " The value to assign " + variableName + "." + LS +
        "*/" + LS +
        "void " + scopeIndicator + "set" + uppperVariable + "(const " + type + "& " + setVariable + ")" + LS +
        "{" + "" + LS +
        "  " + variableName + " = " + setVariable + ";" + "" + LS +
        "}" + LS;

    return result;
}

void parseSelection() {
    // offsets from start of buffer
    int selectionStart;
    int selectionEnd;
    if (textArea.getSelection().length() == 0) {
        // no selection, use current line
        selectionStart = textArea.getLineStartOffset(textArea.getCaretLine());
        selectionEnd = textArea.getLineEndOffset(textArea.getCaretLine());
    }
    else {
        selectionStart = textArea.getSelection(0).getStart();
        selectionEnd = textArea.getSelection(0).getEnd();
    }

    StringBuffer code = new StringBuffer();
    String className = getClassName();
    
    // remove comments and blank lines
    ArrayList lines = stripComments(buffer, selectionStart, selectionEnd);
    if (lines.size() == 0) {
        return;   
    }
    
    // parse each line for variable declaration. Lines are already trimmed and
    // have all comments removed.
    for (int i = 0; i < lines.size(); i++) {
        String lineText = lines.get(i);
        
        // combine lines up to next semi-colon into a single line
        while(!lineText.endsWith(";") && i < lines.size()) {
            lineText += lines.get(++i);   
        }
        
        // ensure line contains at least two words, that is, a type
        // and a variable name at minimum. This is a fairly lame check in that
        // "public int" would qualify, but it does mean that I don't have to
        // do so much bounds checking below.
        if (!lineText.matches("\\S+\\s+[_A-Za-z0-9$]+.*?")) {
            continue;   
        }
        
        // remove semi-colon
        if ( lineText.endsWith( ";" ) ) {
            lineText = lineText.substring( 0, lineText.length() - 1 );
        }
        lineText = lineText.trim();
        if ( lineText.length() == 0 ) {
            continue;
        }
        
        // list to hold variable names
        ArrayList variables = new ArrayList();
        
        // the variable type
        String type = "";
        
        // could have declaration like "int x, y = 6;", which is why there
        // needs to be an array for the variable names
        if ( lineText.indexOf( "," ) > 0 ) {
            int comma = lineText.indexOf(",");
            String front = lineText.substring(0, comma);        // everything before the first comma
            if (front.indexOf("=") > 0) {
                front = front.substring(0, front.indexOf("=")); // drop the initial value  
            }
            front = front.trim();
            String[] fronts = front.split("\\s+");
            variables.add(fronts[fronts.length - 1]);           // last item in array is variable name
            type = fronts[fronts.length - 2];                   // next to last item is type

            String back = lineText.substring(comma + 1);        // everything after the first comma
            String[] backs = back.split(",");
            for (back : backs) {
                back = back.trim();
                String[] parts = back.split("\\s+");            // could have initializer with spaces
                String var = extractVar(parts[0]);              // could have i=0, ie, no spaces
                variables.add(var);   
            }
        }
        else {
            // just one variable declared, may be initialized
            if (lineText.indexOf("=") > 0) {
                lineText = lineText.substring(0, lineText.indexOf("="));    // drop the intial value
            }
            lineText = lineText.trim();
            String[] parts = lineText.split("\\s+");
            variables.add(extractVar(parts[parts.length - 1])); // last item in array is variable name
            type = parts[parts.length - 2];                     // next to last item is type
        }
        
        type = type.trim();
        
        if (variables.size() == 0) {
            continue;   
        }
        code.append( LS );

        // create the get and set methods for each variable
        for ( String variable : variables ) {
            if ( createGetMethods ) {
                String tmp = JAVA_MODE ? createJavaGetMethod( type, variable ) : createCppGetMethod( className, type, variable );
                if ( tmp != null && tmp.length() > 0 ) {
                    code.append( tmp ).append( LS );
                }
            }

            if ( createSetMethods && lineText.indexOf( "final " ) == -1 && lineText.indexOf( "const " ) == -1 ) {
                String tmp = JAVA_MODE ? createJavaSetMethod( type, variable ) : createCppSetMethod( className, type, variable );
                if ( tmp != null && tmp.compareTo( "" ) != 0 ) {
                    code.append( LS ).append( tmp ).append( LS );
                }
            }
        }
    }

    String toInsert = code.toString();
    if (toInsert.trim().length() == 0) {
        return;   
    }
    
    // move to the end of the selected text
    textArea.setCaretPosition( selectionEnd );

    // insert get/set methods
    textArea.setSelectedText( toInsert );

    // select the inserted code and indent it
    textArea.setCaretPosition( selectionEnd );
    textArea.moveCaretPosition( selectionEnd + code.length(), true );
    textArea.indentSelectedLines();
}

import org.gjt.sp.jedit.syntax.*;

// Given the buffer and start and end of the selection, remove comments and 
// blank lines, then return a list of the remaining lines. The list contains 
// the trimmed line text.
ArrayList stripComments( Buffer buffer, int startOffset, int endOffset ) {
    int firstLine = buffer.getLineOfOffset(startOffset);
    
    // the last line may not actually have anything selected, just the caret
    // happens to be at the start of the line or include part of the whitespace
    // at the start of the line. In this case, don't include the last line.
    int lastLine = buffer.getLineOfOffset(endOffset);
    String lastLineText = buffer.getText(buffer.getLineStartOffset(lastLine), endOffset - buffer.getLineStartOffset(lastLine));
    if (lastLineText.trim().length() == 0) {
        --lastLine;   
    }
    
    ArrayList lines = new ArrayList();
    DefaultTokenHandler tokenHandler = new DefaultTokenHandler();
    StringBuilder lineBuffer = new StringBuilder();
    
    for ( int lineNum = firstLine; lineNum <= lastLine; lineNum++ ) {
        int lineStart = buffer.getLineStartOffset(lineNum);
        lineBuffer.delete(0, lineBuffer.length());

        tokenHandler.init();
        buffer.markTokens( lineNum, tokenHandler );
        Token token = tokenHandler.getTokens();
        
        // skip comments
        while(token.id != Token.END) {
            if (token.id < Token.COMMENT1 || token.id > Token.COMMENT4) {
                lineBuffer.append(buffer.getText(lineStart + token.offset, token.length));
            }
            token = token.next;   
        }
        
        String line = lineBuffer.toString().trim();
        
        // skip blank lines
        if (line.length() > 0) {
            lines.add(line.trim());
        }
    }
    return lines;
} 

boolean isWhitespace(Token token) {
    String text = buffer.getText(token.offset, token.length);
    return text.trim().length() == 0;
}

// given something line "a = 6", returns just the "a". 
String extractVar(String var) {
    if (var == null) {
        return "";   
    }
    var = var.trim();
    if (var.indexOf("=") > 0) {
        var = var.substring(0, var.indexOf("=")).trim();
    }
    return var;
}

void displayPrompt() {
    JCheckBox getCheckbox = new JCheckBox( CreateGetMethodsLabel, true );
    JCheckBox setCheckbox = new JCheckBox( CreateSetMethodsLabel, true );

    JPanel checkBoxPanel = new JPanel(new BorderLayout());
    checkBoxPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
    checkBoxPanel.add( getCheckbox, BorderLayout.NORTH );
    checkBoxPanel.add( setCheckbox, BorderLayout.SOUTH );

    JButton createButton = new JButton( DONE );
    JButton cancelButton = new JButton( CANCEL );

    JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 6, 0));
    buttonPanel.setBorder(BorderFactory.createEmptyBorder(11, 11, 11, 11));
    buttonPanel.add( createButton, BorderLayout.WEST );
    buttonPanel.add( cancelButton, BorderLayout.EAST );

    JPanel mainPanel = new JPanel();
    mainPanel.setLayout( new BorderLayout() );
    mainPanel.add( checkBoxPanel, BorderLayout.NORTH );
    mainPanel.add( buttonPanel, BorderLayout.SOUTH );

    JDialog dialog = new JDialog( view, CreateGetandSetMethodsLabel, false );
    dialog.setContentPane( mainPanel );

    actionPerformed( ActionEvent e ) {
        if ( e.getSource() == createButton ) {
            createGetMethods = getCheckbox.isSelected();
            createSetMethods = setCheckbox.isSelected();
            parseSelection();
        }
        this.dialog.dispose();
        return ;
    }

    createButton.addActionListener( this );
    cancelButton.addActionListener( this );

    dialog.pack();
    dialog.setLocationRelativeTo( view );
    dialog.setDefaultCloseOperation( JDialog.DISPOSE_ON_CLOSE );
    dialog.setVisible( true );
    createButton.requestFocus();
}


final static String NotEditableMessage = jEdit.getProperty("macro.rs.general.ErrorNotEditableDialog.message", "Buffer is not editable");
if ( buffer.isReadOnly() )
    Macros.error( view, NotEditableMessage );
else
    displayPrompt();