package com.scrivenvar.editors;
import com.scrivenvar.FileEditorTab;
import com.scrivenvar.Services;
import com.scrivenvar.decorators.VariableDecorator;
import com.scrivenvar.definition.DefinitionPane;
import com.scrivenvar.definition.VariableTreeItem;
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
import com.scrivenvar.service.Settings;
import static com.scrivenvar.util.Lists.getFirst;
import static com.scrivenvar.util.Lists.getLast;
import static java.lang.Character.isSpaceChar;
import static java.lang.Character.isWhitespace;
import static java.lang.Math.min;
import java.nio.file.Path;
import java.util.function.Consumer;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.scene.control.IndexRange;
import javafx.scene.control.TreeItem;
import javafx.scene.input.InputEvent;
import javafx.scene.input.KeyCode;
import static javafx.scene.input.KeyCode.AT;
import static javafx.scene.input.KeyCode.DIGIT2;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.MINUS;
import static javafx.scene.input.KeyCode.SPACE;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
import javafx.scene.input.KeyEvent;
import org.fxmisc.richtext.StyledTextArea;
import org.fxmisc.wellbehaved.event.EventPattern;
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
import org.fxmisc.wellbehaved.event.InputMap;
import static org.fxmisc.wellbehaved.event.InputMap.consume;
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
public class VariableNameInjector {
public static final int DEFAULT_MAX_VAR_LENGTH = 64;
private static final int NO_DIFFERENCE = -1;
private final Settings settings = Services.load( Settings.class );
private InputMap<InputEvent> keyboardMap;
private FileEditorTab tab;
private DefinitionPane definitionPane;
private int initialCaretPosition;
private VariableNameInjector() {
}
public static void listen( final FileEditorTab tab, final DefinitionPane pane ) {
VariableNameInjector vni = new VariableNameInjector();
vni.setFileEditorTab( tab );
vni.setDefinitionPane( pane );
vni.initKeyboardEventListeners();
}
private void initKeyboardEventListeners() {
addEventListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
addEventListener( keyPressed( AT ), this::vMode );
}
private void vMode( KeyEvent e ) {
setInitialCaretPosition();
vModeStart();
vModeAutocomplete();
}
private void vModeKeyPressed( KeyEvent e ) {
final KeyCode keyCode = e.getCode();
switch( keyCode ) {
case BACK_SPACE:
vModeBackspace();
break;
case ESCAPE:
vModeStop();
break;
case ENTER:
case PERIOD:
case RIGHT:
case END:
if( vModeConditionalComplete() && keyCode == ENTER ) {
vModeStop();
decorateVariable();
}
break;
case UP:
cyclePathPrev();
break;
case DOWN:
cyclePathNext();
break;
default:
vModeFilterKeyPressed( e );
break;
}
e.consume();
}
private void vModeBackspace() {
deleteSelection();
if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
vModeAutocomplete();
}
else {
vModeStop();
}
}
private void vModeAutocomplete() {
final TreeItem<String> node = getCurrentNode();
if( node != null && !node.isLeaf() ) {
final String word = getLastPathWord();
final String label = node.getValue();
final int delta = difference( label, word );
final String remainder = delta == NO_DIFFERENCE
? label
: label.substring( delta );
final StyledTextArea textArea = getEditor();
final int posBegan = getCurrentCaretPosition();
final int posEnded = posBegan + remainder.length();
textArea.replaceSelection( remainder );
if( posEnded - posBegan > 0 ) {
textArea.selectRange( posEnded, posBegan );
}
expand( node );
}
}
private void vModeFilterKeyPressed( final KeyEvent e ) {
if( isVariableNameKey( e ) ) {
typed( e.getText() );
}
}
private boolean vModeConditionalComplete() {
acceptPath();
final TreeItem<String> node = getCurrentNode();
final boolean terminal = isTerminal( node );
if( !terminal ) {
typed( SEPARATOR );
}
return terminal;
}
private void autocomplete( final KeyEvent e ) {
final String paragraph = getCaretParagraph();
final int[] boundaries = getWordBoundaries( paragraph );
final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
VariableTreeItem<String> leaf = findLeaf( word );
if( leaf == null ) {
leaf = findLeaf( word, true );
}
if( leaf != null ) {
replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
decorateVariable();
expand( leaf );
}
}
private void decorateVariable() {
final String paragraph = getCaretParagraph();
final int[] boundaries = getWordBoundaries( paragraph );
final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
final String newVariable = getVariableDecorator().decorate( old );
final int posEnded = getCurrentCaretPosition();
final int posBegan = posEnded - old.length();
getEditor().replaceText( posBegan, posEnded, newVariable );
}
private void replaceText(
final int posBegan, final int posEnded, final String text ) {
final int p = getCurrentParagraph();
getEditor().replaceText( p, posBegan, p, posEnded, text );
}
private int getCurrentParagraph() {
return getEditor().getCurrentParagraph();
}
private int[] getWordBoundaries( final String p, final int offset ) {
final String paragraph = p.replace( "---", " " ).replace( "--", " " );
return getWordAt( paragraph, offset );
}
private int[] getWordBoundaries( final String paragraph ) {
return getWordBoundaries( paragraph, getCurrentCaretColumn() );
}
private int[] getWordAt( final String p, final int offset ) {
return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) };
}
private int getWordBegan( final String s, int offset ) {
while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
offset--;
}
return offset;
}
private int getWordEnded( final String s, int offset ) {
final int length = s.length();
while( offset < length && isBoundary( s.charAt( offset ) ) ) {
offset++;
}
return offset;
}
private boolean isBoundary( final char c ) {
return !isSpaceChar( c );
}
private String getCaretParagraph() {
return getEditor().getText( getCurrentParagraph() );
}
private <T> boolean isTerminal( final TreeItem<T> node ) {
final ObservableList<TreeItem<T>> branches = node.getChildren();
return branches.size() == 1 && branches.get( 0 ).isLeaf();
}
private void typed( final String text ) {
getEditor().replaceSelection( text );
vModeAutocomplete();
}
private void acceptPath() {
final IndexRange range = getSelectionRange();
if( range != null ) {
final int rangeEnd = range.getEnd();
final StyledTextArea textArea = getEditor();
textArea.deselect();
textArea.moveTo( rangeEnd );
}
}
private void replacePath( final String oldPath, final String newPath ) {
final StyledTextArea textArea = getEditor();
final int posBegan = getInitialCaretPosition();
final int posEnded = posBegan + oldPath.length();
textArea.deselect();
textArea.replaceText( posBegan, posEnded, newPath );
}
private void deleteSelection() {
final StyledTextArea textArea = getEditor();
textArea.replaceSelection( "" );
textArea.deletePreviousChar();
}
private void cycleSelection( final boolean direction ) {
final TreeItem<String> node = getCurrentNode();
TreeItem< String> cycled = direction
? node.nextSibling()
: node.previousSibling();
if( cycled == null ) {
cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
}
final String path = getCurrentPath();
final String cycledWord = cycled.getValue();
final String word = getLastPathWord();
final int index = path.indexOf( word );
final String cycledPath = path.substring( 0, index ) + cycledWord;
expand( cycled );
replacePath( path, cycledPath );
}
private void cyclePathNext() {
cycleSelection( true );
}
private void cyclePathPrev() {
cycleSelection( false );
}
private String getCurrentPath() {
final String s = extractTextChunk();
final int length = s.length();
int i = 0;
while( i < length && !isWhitespace( s.charAt( i ) ) ) {
i++;
}
return s.substring( 0, i );
}
private <T> ObservableList<TreeItem<T>> getSiblings(
final TreeItem<T> item ) {
final TreeItem<T> parent = item.getParent();
return parent == null ? item.getChildren() : parent.getChildren();
}
private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
return getFirst( getSiblings( item ), item );
}
private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
return getLast( getSiblings( item ), item );
}
private int getCurrentCaretPosition() {
return getEditor().getCaretPosition();
}
private int getCurrentCaretColumn() {
return getEditor().getCaretColumn();
}
private String getLastPathWord() {
String path = getCurrentPath();
int i = path.indexOf( SEPARATOR_CHAR );
while( i > 0 ) {
path = path.substring( i + 1 );
i = path.indexOf( SEPARATOR_CHAR );
}
return path;
}
private String extractTextChunk() {
final StyledTextArea textArea = getEditor();
final int textBegan = getInitialCaretPosition();
final int remaining = textArea.getLength() - textBegan;
final int textEnded = min( remaining, getMaxVarLength() );
try {
return textArea.getText( textBegan, textEnded );
} catch( final Exception e ) {
return textArea.getText();
}
}
private TreeItem<String> getCurrentNode() {
return findNode( getCurrentPath() );
}
private TreeItem<String> findNode( final String path ) {
return getDefinitionPane().findNode( path );
}
private VariableTreeItem<String> findLeaf( final String text ) {
return getDefinitionPane().findLeaf( text, false );
}
private VariableTreeItem<String> findLeaf(
final String text,
final boolean contains ) {
return getDefinitionPane().findLeaf( text, contains );
}
private void vModeKeyTyped( KeyEvent e ) {
e.consume();
}
protected InputMap<InputEvent> createKeyboardMap() {
return sequence(
consume( keyTyped(), this::vModeKeyTyped ),
consume( keyPressed(), this::vModeKeyPressed )
);
}
private InputMap<InputEvent> getKeyboardMap() {
if( this.keyboardMap == null ) {
this.keyboardMap = createKeyboardMap();
}
return this.keyboardMap;
}
private void expand( final TreeItem<String> node ) {
final DefinitionPane pane = getDefinitionPane();
pane.collapse();
pane.expand( node );
pane.select( node );
}
private boolean isVariableNameKey( final KeyEvent keyEvent ) {
final KeyCode kc = keyEvent.getCode();
return (kc.isLetterKey()
|| kc.isDigitKey()
|| (keyEvent.isShiftDown() && kc == MINUS))
&& !keyEvent.isControlDown();
}
private void vModeStart() {
addEventListener( getKeyboardMap() );
}
private void vModeStop() {
removeEventListener( getKeyboardMap() );
}
private VariableDecorator getVariableDecorator() {
return VariableNameDecoratorFactory.newInstance( getFilename() );
}
private Path getFilename() {
return getFileEditorTab().getPath();
}
@SuppressWarnings( "StringEquality" )
private int difference( final CharSequence s1, final CharSequence s2 ) {
if( s1 == s2 ) {
return NO_DIFFERENCE;
}
if( s1 == null || s2 == null ) {
return 0;
}
int i = 0;
final int limit = min( s1.length(), s2.length() );
while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
i++;
}
return i;
}
private EditorPane getEditorPane() {
return getFileEditorTab().getEditorPane();
}
private <T extends Event, U extends T> void addEventListener(
final EventPattern<? super T, ? extends U> event,
final Consumer<? super U> consumer ) {
getEditorPane().addEventListener( event, consumer );
}
private void addEventListener( final InputMap<InputEvent> map ) {
getEditorPane().addEventListener( map );
}
private void removeEventListener( final InputMap<InputEvent> map ) {
getEditorPane().removeEventListener( map );
}
private int getInitialCaretPosition() {
return this.initialCaretPosition;
}
private void setInitialCaretPosition() {
this.initialCaretPosition = getEditor().getCaretPosition();
}
private StyledTextArea getEditor() {
return getFileEditorTab().getEditorPane().getEditor();
}
public FileEditorTab getFileEditorTab() {
return this.tab;
}
private void setFileEditorTab( final FileEditorTab editorTab ) {
this.tab = editorTab;
}
private DefinitionPane getDefinitionPane() {
return this.definitionPane;
}
private void setDefinitionPane( final DefinitionPane definitionPane ) {
this.definitionPane = definitionPane;
}
private IndexRange getSelectionRange() {
return getEditor().getSelection();
}
private int getMaxVarLength() {
return getSettings().getSetting(
"editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
}
private Settings getSettings() {
return this.settings;
}
}