public class MappedList<E, F> extends TransformationList<E, F> {

    private final Function<F, E> mapper;

    /**
     * Creates a new mapped list containing the items of the given source transformed with the given mapper.
     *
     * @param source
     *         the source list to wrap
     * @param mapper
     *         the mapper to use to transform the items of the source into items of this list
     */
    public MappedList(ObservableList<F> source, Function<F, E> mapper) {
        super(source);
        this.mapper = mapper;
    }

    @Override
    public int getSourceIndex(int index) {
        return index;
    }

    @Override
    public E get(int index) {
        return mapper.apply(getSource().get(index));
    }

    @Override
    public int size() {
        return getSource().size();
    }

    @Override
    protected void sourceChanged(Change<? extends F> c) {
        fireChange(new Change<E>(this) {

            @Override
            public boolean wasAdded() {
                return c.wasAdded();
            }

            @Override
            public boolean wasRemoved() {
                return c.wasRemoved();
            }

            @Override
            public boolean wasReplaced() {
                return c.wasReplaced();
            }

            @Override
            public boolean wasUpdated() {
                return c.wasUpdated();
            }

            @Override
            public boolean wasPermutated() {
                return c.wasPermutated();
            }

            @Override
            public int getPermutation(int i) {
                return c.getPermutation(i);
            }

            @Override
            protected int[] getPermutation() {
                // This method is only called by the superclass methods wasPermutated() and getPermutation(int), which
                // are both overriden by this class. There is no other way this method can be called.
                throw new AssertionError("Unreachable code");
            }

            @Override
            public List<E> getRemoved() {
                return c.getRemoved().stream().map(mapper).collect(Collectors.toList());
            }

            @Override
            public int getFrom() {
                return c.getFrom();
            }

            @Override
            public int getTo() {
                return c.getTo();
            }

            @Override
            public boolean next() {
                return c.next();
            }

            @Override
            public void reset() {
                c.reset();
            }
        });
    }
} public class UnorderedMergedList<E> extends ObservableListWrapper<E> {

    private final ListChangeListener<E> updater = c -> {
        while (c.next()) {
            if (c.wasRemoved()) {
                removeAll(c.getRemoved());
            }
            if (c.wasAdded()) {
                addAll(c.getAddedSubList());
            }
        }
    };

    // to avoid garbage collection
    @SuppressWarnings("FieldCanBeLocal")
    private final ListChangeListener<ObservableList<? extends E>> subscriber = c -> {
        while (c.next()) {
            if (c.wasRemoved()) {
                c.getRemoved().forEach(this::unsubscribeFrom);
            }
            if (c.wasAdded()) {
                c.getAddedSubList().forEach(this::subscribeTo);
            }
        }
    };

    @SafeVarargs
    public UnorderedMergedList(ObservableList<? extends E>... lists) {
        super(new ArrayList<>(Arrays.stream(lists).mapToInt(List::size).sum()));

        for (ObservableList<? extends E> list : lists) {
            subscribeTo(list);
        }
    }

    public UnorderedMergedList(Collection<? extends ObservableList<? extends E>> lists) {
        super(new ArrayList<>(lists.stream().mapToInt(List::size).sum()));

        lists.forEach(this::subscribeTo);
    }

    public UnorderedMergedList(ObservableList<? extends ObservableList<? extends E>> lists) {
        this((Collection<? extends ObservableList<? extends E>>) lists);

        // the list of dependencies can change
        lists.addListener(subscriber);
    }

    private void subscribeTo(ObservableList<? extends E> dependency) {
        addAll(dependency);
        dependency.addListener(updater);
    }

    private void unsubscribeFrom(ObservableList<? extends E> dependency) {
        removeAll(dependency);
        dependency.removeListener(updater);
    }
} public class BaseEditableListPane<T extends Named> extends EditableListPane<T> {

    public BaseEditableListPane() {
        super();
        setNewItemValidator(s -> !s.isEmpty());
    }

    public void setItemFactory(Function<String, T> itemFactory) {
        super.setItemFactory(itemFactory);
        getList().setConverter(itemFactory, T::getName, s -> !s.isEmpty());
        getList().setUpdater(T::setName);
    }
} public class Colorizer extends RuleSet<LogEntry, Style, Filter, StyleRule> implements Named {

    private final StringProperty name;

    /**
     * Creates a new Colorizer with no rules.
     *
     * @param name
     *         a name for this Colorizer
     */
    public Colorizer(@NotNull String name) {
        this(name, FXCollections.observableArrayList());
    }

    /**
     * Creates a new Colorizer with the given rules.
     *
     * @param name
     *         a name for this Colorizer
     * @param styleRules
     *         the list of StyleRules to use when styling a Node
     */
    public Colorizer(@NotNull String name, @NotNull ObservableList<StyleRule> styleRules) {
        super(styleRules);
        this.name = new SimpleStringProperty(name);
    }

    /**
     * Creates a copy of the given Colorizer.
     *
     * @param source
     *         the colorizer to create a copy of
     */
    public Colorizer(@NotNull Colorizer source) {
        this(source.getName());
        this.getRules().setAll(source.getRules());
    }

    @Override
    public String getName() {
        return name.get();
    }

    @SuppressWarnings("unused")
    public StringProperty nameProperty() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name.set(name);
    }

    @Override
    public String toString() {
        return getName();
    }
} public class StyleRule extends Rule<LogEntry, Style, Filter> implements Named {

    private final StringProperty name;

    /**
     * Creates a StyleRule with no style override.
     *
     * @param name
     *         a name for this rule
     */
    public StyleRule(@NotNull String name) {
        this(name, Filter.findInRawLog(""), new Style(Style.DEFAULT));
    }

    /**
     * Creates a StyleRule with the given filter and style.
     *
     * @param name
     *         a name for this rule
     * @param filter
     *         the filter to use to match logs
     * @param style
     *         the style associated with this rule
     */
    public StyleRule(@NotNull String name, @NotNull Filter filter, @Nullable Style style) {
        super(filter, style);
        this.name = new SimpleStringProperty(name);
    }

    /**
     * Creates a copy of the given StyleRule. The Style is also copied from the given rule's style, it is not reused.
     *
     * @param source
     *         the rule to copy
     */
    public StyleRule(@NotNull StyleRule source) {
        this(source.getName(), new Filter(source.getMatcher()), new Style(source.getResult()));
    }

    @Override
    public String getName() {
        return name.get();
    }

    public StringProperty nameProperty() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name.set(name);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " '" + getName() + "'";
    }
} public class ColumnDefinition {

    private static final double DEFAULT_WIDTH = 80d;

    private final StringProperty headerLabel;

    private final StringProperty capturingGroupName;

    private final DoubleProperty width;

    private final BooleanProperty visible;

    private final StringProperty description;

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     */
    public ColumnDefinition(String headerLabel, String capturingGroupName) {
        this(headerLabel, capturingGroupName, null, true, DEFAULT_WIDTH);
    }

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     * @param description
     *         the description of this column for the user
     */
    public ColumnDefinition(String headerLabel, String capturingGroupName, String description) {
        this(headerLabel, capturingGroupName, description, true, DEFAULT_WIDTH);
    }

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     * @param description
     *         the description of this column for the user
     * @param visible
     *         whether this column is initially visible before the user customizes it
     */
    public ColumnDefinition(String headerLabel, String capturingGroupName, String description, boolean visible) {
        this(headerLabel, capturingGroupName, description, visible, DEFAULT_WIDTH);
    }

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     * @param initialWidth
     *         the initial width to give this column before the user customizes it
     */
    public ColumnDefinition(String headerLabel, String capturingGroupName, double initialWidth) {
        this(headerLabel, capturingGroupName, null, true, initialWidth);
    }

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     * @param description
     *         the description of this column for the user
     * @param initialWidth
     *         the initial width to give this column before the user customizes it
     */
    public ColumnDefinition(String headerLabel, String capturingGroupName, String description, double initialWidth) {
        this(headerLabel, capturingGroupName, description, true, initialWidth);
    }

    /**
     * Creates a new ColumnDefinition for the given field.
     *
     * @param headerLabel
     *         the label of this column
     * @param capturingGroupName
     *         the name of the capturing group of the regexp to put inside this column
     * @param description
     *         the description of this column for the user
     * @param visible
     *         whether this column is initially visible before the user customizes it
     * @param initialWidth
     *         the initial width to give this column before the user customizes it
     */
    private ColumnDefinition(String headerLabel, String capturingGroupName, String description, boolean visible, double
            initialWidth) {
        this.capturingGroupName = new SimpleStringProperty(capturingGroupName);
        this.headerLabel = new SimpleStringProperty(headerLabel);
        this.visible = new SimpleBooleanProperty(visible);
        this.width = new SimpleDoubleProperty(initialWidth);
        this.description = new SimpleStringProperty(description);
    }

    Label createBoundHeaderLabel() {
        Label header = new Label();
        header.textProperty().bind(headerLabel);
        header.tooltipProperty().bind(createTooltipBinding());
        // allows to show this label while hiding the default header text
        header.getStyleClass().add("column-header-label");
        // makes it take up the full width of the table column header so that the tooltip is shown more easily
        header.setMaxWidth(Double.MAX_VALUE);
        return header;
    }

    private Binding<Tooltip> createTooltipBinding() {
        Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(description);

        BooleanBinding descriptionIsNull = description.isNull();
        Callable<Tooltip> tooltipCallable = () -> {
            if (descriptionIsNull.get()) {
                // no tooltip when no description
                return null;
            }
            return tooltip;
        };
        return Bindings.createObjectBinding(tooltipCallable, descriptionIsNull);
    }

    /**
     * @return the header of this column
     */
    public String getHeaderLabel() {
        return headerLabel.get();
    }

    public StringProperty headerLabelProperty() {
        return headerLabel;
    }

    /**
     * @return the name of the capturing group to get the data from for this column
     */
    public String getCapturingGroupName() {
        return capturingGroupName.get();
    }

    public StringProperty capturingGroupNameProperty() {
        return capturingGroupName;
    }

    /**
     * @return the preferred width for this column
     */
    double getWidth() {
        return width.get();
    }

    /**
     * @return the preferred width for this column (property)
     */
    DoubleProperty widthProperty() {
        return width;
    }

    /**
     * @return whether this column is visible
     */
    public boolean isVisible() {
        return visible.get();
    }

    /**
     * @return whether this column is visible (property)
     */
    public BooleanProperty visibleProperty() {
        return visible;
    }

    public String getDescription() {
        return description.get();
    }

    public StringProperty descriptionProperty() {
        return description;
    }

    public void setDescription(String description) {
        this.description.set(description);
    }
} public class Config {

    /**
     * Holder class for thread-safe singleton pattern.
     */
    private static class Holder {
        private static final Config INSTANCE = ConfigLoader.getUserConfig();
    }

    /**
     * Returns the current configuration.
     *
     * @return the current configuration
     */
    @NotNull
    public static Config getInstance() {
        return Holder.INSTANCE;
    }

    /**
     * The version of the format of the config. This needs to be increased each time the serialization format of this
     * class is changed.
     */
    static final int FORMAT_VERSION = 5;

    private final Integer version;

    private final Preferences preferences;

    private final State state;

    private final ObservableList<Columnizer> columnizers;

    private final ObservableList<Colorizer> colorizers;

    /**
     * Creates an empty configuration.
     */
    public Config() {
        this.version = FORMAT_VERSION;
        this.preferences = new Preferences();
        this.state = new State();
        this.columnizers = FXCollections.observableArrayList();
        this.colorizers = FXCollections.observableArrayList();
    }

    public Integer getVersion() {
        return version;
    }

    public Preferences getPreferences() {
        return preferences;
    }

    public State getState() {
        return state;
    }

    /**
     * Returns the list of available {@link Columnizer}s.
     *
     * @return the list of available {@link Columnizer}s, potentially empty.
     */
    @NotNull
    public ObservableList<Columnizer> getColumnizers() {
        return columnizers;
    }

    /**
     * Returns the list of available {@link Colorizer}s.
     *
     * @return the list of available {@link Colorizer}s, potentially empty.
     */
    @NotNull
    public ObservableList<Colorizer> getColorizers() {
        return colorizers;
    }

    /**
     * Writes this config to the default user config file.
     *
     * @throws IOException
     *         if an I/O error occurs while writing to the file
     */
    public void persist() throws IOException {
        ConfigLoader.writeUserConfig(this);
    }
} public class Filter implements Matcher<LogEntry> {

    public static final Filter MATCH_ALL = findInRawLog(".*");

    public static final Filter ERROR_SEVERITY = findInColumn("severity", "[Ee]rror|ERROR");

    public static final Filter WARN_SEVERITY = findInColumn("severity", "[Ww]arn(ing)?|WARN(ING)?");

    public static final Filter INFO_SEVERITY = findInColumn("severity", "[Ii]nfo|INFO");

    public static final Filter DEBUG_SEVERITY = findInColumn("severity", "[Dd]ebug|DEBUG");

    public static final Filter NOTICE_SEVERITY = findInColumn("severity", "[Nn]otice|NOTICE");

    public static final Filter STACKTRACE_HEAD = findInColumn("msg", "(^\\s*Caused By.*)|^((\\S+\\.)+\\S*Exception.*)");

    public static final Filter STACKTRACE_BODY = findInColumn("msg", "^\\s*at \\S.*");

    private final StringProperty columnName;

    private final Property<Pattern> pattern;

    /**
     * Creates a new filter.
     *
     * @param columnName
     *         the capturing group name corresponding to the column to apply this filter to, or null to apply this
     *         filter to the whole raw log line
     * @param regex
     *         the regex that the given column (or whole line) should match
     * @param flags
     *         the match flags, a bit mask that may include {@link Pattern#CASE_INSENSITIVE}, {@link Pattern#MULTILINE},
     *         {@link Pattern#DOTALL}, {@link Pattern#UNICODE_CASE}, {@link Pattern#CANON_EQ}, {@link
     *         Pattern#UNIX_LINES}, {@link Pattern#LITERAL}, {@link Pattern#UNICODE_CHARACTER_CLASS} and {@link
     *         Pattern#COMMENTS}
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    private Filter(String columnName, @RegExp String regex, int flags) throws PatternSyntaxException {
        this.pattern = new SimpleObjectProperty<>(Pattern.compile(regex, flags));
        this.columnName = new SimpleStringProperty(columnName);
    }

    /**
     * Creates a copy of the given Filter.
     *
     * @param source
     *         the filter to copy
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    public Filter(Filter source) throws PatternSyntaxException {
        this.pattern = new SimpleObjectProperty<>(source.pattern.getValue());
        this.columnName = new SimpleStringProperty(source.getColumnName());
    }

    /**
     * Creates a new filter which applies on the raw log line.
     *
     * @param regex
     *         the regex that the whole raw log line should match
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    @NotNull
    public static Filter findInRawLog(@NotNull @RegExp String regex) throws PatternSyntaxException {
        return new Filter(null, regex, 0);
    }

    /**
     * Creates a new filter which applies on the raw log line.
     *
     * @param regex
     *         the regex that the whole raw log line should match
     * @param flags
     *         the match flags, a bit mask that may include {@link Pattern#CASE_INSENSITIVE}, {@link Pattern#MULTILINE},
     *         {@link Pattern#DOTALL}, {@link Pattern#UNICODE_CASE}, {@link Pattern#CANON_EQ}, {@link
     *         Pattern#UNIX_LINES}, {@link Pattern#LITERAL}, {@link Pattern#UNICODE_CHARACTER_CLASS} and {@link
     *         Pattern#COMMENTS}
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    @NotNull
    public static Filter findInRawLog(@NotNull @RegExp String regex, int flags) throws PatternSyntaxException {
        return new Filter(null, regex, flags);
    }

    /**
     * Creates a new filter which applies on the value of the given column.
     *
     * @param columnName
     *         the capturing group name corresponding to the column to apply this filter to
     * @param regex
     *         the regex that the whole value of the given column should match
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    @NotNull
    public static Filter findInColumn(@NotNull String columnName, @NotNull @RegExp String regex) throws
            PatternSyntaxException {
        return findInColumn(columnName, regex, 0);
    }

    /**
     * Creates a new filter which applies on the value of the given column.
     *
     * @param columnName
     *         the capturing group name corresponding to the column to apply this filter to
     * @param regex
     *         the regex that the whole value of the given column should match
     * @param flags
     *         the match flags, a bit mask that may include {@link Pattern#CASE_INSENSITIVE}, {@link Pattern#MULTILINE},
     *         {@link Pattern#DOTALL}, {@link Pattern#UNICODE_CASE}, {@link Pattern#CANON_EQ}, {@link
     *         Pattern#UNIX_LINES}, {@link Pattern#LITERAL}, {@link Pattern#UNICODE_CHARACTER_CLASS} and {@link
     *         Pattern#COMMENTS}
     *
     * @throws PatternSyntaxException
     *         if the given regex is not well formed
     */
    @NotNull
    private static Filter findInColumn(@NotNull String columnName, @NotNull @RegExp String regex, int flags) throws
            PatternSyntaxException {
        return new Filter(columnName, regex, flags);
    }

    public String getColumnName() {
        return columnName.get();
    }

    public StringProperty columnNameProperty() {
        return columnName;
    }

    @SuppressWarnings("unused")
    public void setColumnName(String columnName) {
        this.columnName.set(columnName);
    }

    public Pattern getPattern() {
        return pattern.getValue();
    }

    public Property<Pattern> patternProperty() {
        return pattern;
    }

    @SuppressWarnings("unused")
    public void setPattern(Pattern pattern) {
        this.pattern.setValue(pattern);
    }

    @Override
    public boolean test(@Nullable LogEntry log) {
        if (log == null) {
            return false;
        }
        if (columnName.get() == null) {
            return pattern.getValue().matcher(log.rawLine()).find();
        } else {
            String columnValue = log.getColumnValues().get(columnName.get());
            return columnValue != null && pattern.getValue().matcher(columnValue).find();
        }
    }

    @Override
    public Binding<Boolean> matches(ObservableValue<LogEntry> logObservable) {
        return Bindings.createBooleanBinding(() -> test(logObservable.getValue()), logObservable, pattern, columnName);
    }
} public class Css {

    public static final PseudoClass INVALID = PseudoClass.getPseudoClass("invalid");

} public class UIUtils {

    /**
     * Adds a clear button to the right of the text field. The little cross appears only when the field is not empty.
     *
     * @param customTextField
     *         the text field to decorate
     */
    public static void makeClearable(CustomTextField customTextField) {
        try {
            Method method =
                    TextFields.class.getDeclaredMethod("setupClearButtonField", TextField.class, ObjectProperty.class);
            method.setAccessible(true);
            method.invoke(null, customTextField, customTextField.rightProperty());
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public static BooleanBinding noItemIsSelected(TableView<?> tableView) {
        return tableView.getSelectionModel().selectedItemProperty().isNull();
    }

    public static BooleanBinding firstItemIsSelected(TableView<?> tableView) {
        return tableView.getSelectionModel().selectedIndexProperty().isEqualTo(0);
    }

    public static BooleanBinding lastItemIsSelected(TableView<?> tableView) {
        IntegerExpression lastIndex =
                Bindings.createIntegerBinding(() -> tableView.getItems().size() - 1, tableView.itemsProperty());
        return tableView.getSelectionModel().selectedIndexProperty().isEqualTo(lastIndex);
    }

    /**
     * Scrolls the given table view to the given item. This is nicer than {@link TableView#scrollTo(int)} because it
     * doesn't put the target item at the top, but rather at about a third of the screen.
     *
     * @param table
     *         the table view to scroll
     * @param index
     *         the index to scroll to
     */
    @SuppressWarnings("WeakerAccess")
    public static void scrollTo(TableView table, int index) {
        int numberOfVisibleItems = getNumberOfVisibleItems(table);
        table.scrollTo(index - numberOfVisibleItems / 3);
    }

    @SuppressWarnings("WeakerAccess")
    public static int getNumberOfVisibleItems(TableView table) {
        return getLastVisibleRowIndex(table) - getFirstVisibleRowIndex(table);
    }

    public static int getFirstVisibleRowIndex(TableView table) {
        VirtualFlow<?> flow = getVirtualFlow(table);
        if (flow == null || flow.getFirstVisibleCellWithinViewPort() == null) {
            return 0;
        }
        int indexFirst = flow.getFirstVisibleCellWithinViewPort().getIndex();
        if (indexFirst >= table.getItems().size()) {
            return table.getItems().size() - 1;
        } else {
            return indexFirst;
        }
    }

    @SuppressWarnings("WeakerAccess")
    public static int getLastVisibleRowIndex(TableView table) {
        VirtualFlow<?> flow = getVirtualFlow(table);
        if (flow == null || flow.getLastVisibleCellWithinViewPort() == null) {
            return 0;
        }
        int index = flow.getLastVisibleCellWithinViewPort().getIndex();
        if (index >= table.getItems().size()) {
            return table.getItems().size() - 1;
        } else {
            return index;
        }
    }

    private static VirtualFlow<?> getVirtualFlow(TableView table) {
        TableViewSkin<?> skin = (TableViewSkin) table.getSkin();
        if (skin == null) {
            return null;
        }
        return (VirtualFlow) skin.getChildren().get(1);
    }

    public static void whenWindowReady(Node anyChild, Consumer<? super Window> handler) {
        ChangeListener<Scene> onNewScene = (obsScene, oldScene, newScene) -> {
            ChangeListener<? super Window> oneShotWindowListener = asOneShotListener(asListener(handler));
            newScene.windowProperty().addListener(oneShotWindowListener);
        };

        anyChild.sceneProperty().addListener(asOneShotListener(onNewScene));
    }

    private static <T> ChangeListener<T> asListener(Consumer<T> handler) {
        return (obs, oldValue, newValue) -> handler.accept(newValue);
    }

    private static <T> ChangeListener<T> asOneShotListener(ChangeListener<T> handler) {
        return new ChangeListener<T>() {
            public void changed(ObservableValue<? extends T> obs, T oldValue, T newValue) {
                handler.changed(obs, oldValue, newValue);
                obs.removeListener(this);
            }
        };
    }

    public static <S, C> ListBinding<C> selectList(ObservableValue<S> source, Function<S, ObservableList<C>> getList) {
        return new ListBinding<C>() {
            {
                bind(source);
            }

            @Override
            protected ObservableList<C> computeValue() {
                S sourceValue = source.getValue();
                if (sourceValue != null) {
                    return getList.apply(sourceValue);
                } else {
                    return FXCollections.emptyObservableList();
                }
            }
        };
    }
}
import java.net.URL;
import java.util.ResourceBundle;
import java.util.concurrent.Callable;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ListBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableObjectValue;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.paint.Color;
import impl.org.controlsfx.autocompletion.AutoCompletionTextFieldBinding;
import org.hildan.fx.bindings.lists.MappedList;
import org.hildan.fx.bindings.lists.UnorderedMergedList;
import org.hildan.fx.components.list.BaseEditableListPane;
import org.hildan.fxlog.coloring.Colorizer;
import org.hildan.fxlog.coloring.StyleRule;
import org.hildan.fxlog.columns.ColumnDefinition;
import org.hildan.fxlog.config.Config;
import org.hildan.fxlog.filtering.Filter;
import org.hildan.fxlog.themes.Css;
import org.hildan.fxlog.view.UIUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
package org.hildan.fxlog.controllers;




/**
 * Controller associated to the colorizers customization view.
 */
public class ColorizersController implements Initializable {

    private Config config;

    @FXML
    private BaseEditableListPane<Colorizer> colorizersPane;

    @FXML
    private SplitPane selectedColorizerPane;

    @FXML
    private BaseEditableListPane<StyleRule> rulesPane;

    @FXML
    private ScrollPane selectedRulePane;

    @FXML
    private TextField filterRegexField;

    @FXML
    private ToggleGroup filterType;

    @FXML
    private RadioButton matchRawButton;

    @FXML
    private RadioButton matchColumnButton;

    @FXML
    private TextField filterColumnNameField;

    @FXML
    private CheckBox overrideTextForeground;

    @FXML
    private CheckBox overrideTextBackground;

    @FXML
    private ColorPicker foregroundColorPicker;

    @FXML
    private ColorPicker backgroundColorPicker;

    private Property<Pattern> filterRegexFieldPatternBinding;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        config = Config.getInstance();
        initializeColorizersPane();
        initializeRulesPane();
        initializeStyleEditionPane();
    }

    private void initializeColorizersPane() {
        colorizersPane.setItemFactory(Colorizer::new);
        colorizersPane.setItemDuplicator(Colorizer::new);
        colorizersPane.getList().setItems(config.getColorizers());
        colorizersPane.itemInUseIndexProperty().bindBidirectional(config.getState().selectedColorizerIndexProperty());
    }

    private void initializeRulesPane() {
        ObservableObjectValue<Colorizer> selectedColorizer = colorizersPane.selectedItemProperty();
        selectedColorizerPane.disableProperty().bind(Bindings.isNull(selectedColorizer));

        ListBinding<StyleRule> selectedColorizerRules = UIUtils.selectList(selectedColorizer, Colorizer::getRules);
        rulesPane.setItemFactory(StyleRule::new);
        rulesPane.setItemDuplicator(StyleRule::new);
        rulesPane.getList().itemsProperty().bind(selectedColorizerRules);
    }

    private void initializeStyleEditionPane() {
        selectedRulePane.disableProperty().bind(rulesPane.selectedItemProperty().isNull());

        configureActivableColorPicker(foregroundColorPicker, overrideTextForeground);
        configureActivableColorPicker(backgroundColorPicker, overrideTextBackground);

        filterRegexFieldPatternBinding = new SimpleObjectProperty<>();
        createRegexFieldPatternBinding(filterRegexField, filterRegexFieldPatternBinding);

        bindRuleToUI(null); // initialize pane with empty values
        rulesPane.selectedItemProperty().addListener((obsRule, oldRule, newRule) -> {
            unbindRuleFromUI(oldRule);
            bindRuleToUI(newRule);
        });

        filterColumnNameField.disableProperty().bind(filterType.selectedToggleProperty().isEqualTo(matchRawButton));

        ObservableList<ObservableList<String>> columnGroupNames = new MappedList<>(config.getColumnizers(), c -> {
            return new MappedList<>(c.getColumnDefinitions(), ColumnDefinition::getCapturingGroupName);
        });
        ObservableList<String> autoCompleteList = new UnorderedMergedList<>(columnGroupNames);
        new AutoCompletionTextFieldBinding<>(filterColumnNameField, param -> {
            return autoCompleteList.stream().filter(s -> s.contains(param.getUserText())).collect(Collectors.toSet());
        });
    }

    private static void configureActivableColorPicker(@NotNull ColorPicker picker, @NotNull CheckBox checkbox) {
        picker.disableProperty().bind(checkbox.selectedProperty().not());
    }

    private static void createRegexFieldPatternBinding(TextField regexField, Property<Pattern> patternProperty) {
        ChangeListener<String> patternCreationListener = (obs, oldVal, newVal) -> {
            regexField.pseudoClassStateChanged(Css.INVALID, false);
            try {
                patternProperty.setValue(Pattern.compile(newVal));
            } catch (PatternSyntaxException e) {
                System.out.println("Invalid pattern, using the old one");
                regexField.pseudoClassStateChanged(Css.INVALID, true);
            }
        };
        regexField.textProperty().addListener(patternCreationListener);
    }

    private void bindRuleToUI(@Nullable StyleRule rule) {
        if (rule == null) {
            overrideTextBackground.setSelected(false);
            overrideTextForeground.setSelected(false);
            backgroundColorPicker.setValue(null);
            foregroundColorPicker.setValue(null);
            filterType.selectToggle(matchRawButton);
            filterRegexField.setText("");
            filterColumnNameField.setText("");
            return;
        }

        Filter ruleFilter = rule.getMatcher();