/*
 * 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
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.chainsaw;

import org.apache.log4j.chainsaw.color.RuleColorizer;
import org.apache.log4j.chainsaw.icons.LevelIconFactory;
import org.apache.log4j.helpers.Constants;
import org.apache.log4j.rule.Rule;
import org.apache.log4j.spi.LoggingEventFieldResolver;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.text.*;
import java.awt.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;


/**
 * A specific TableCellRenderer that colourizes a particular cell based on
 * some ColourFilters that have been stored according to the value for the row
 *
 * @author Claude Duguay
 * @author Scott Deboy &lt;sdeboy@apache.org&gt;
 * @author Paul Smith &lt;psmith@apache.org&gt;
 */
public class TableColorizingRenderer extends DefaultTableCellRenderer {
    private static final DateFormat DATE_FORMATTER = new SimpleDateFormat(Constants.SIMPLE_TIME_PATTERN);
    private final Map<String, Icon> iconMap;
    private RuleColorizer colorizer;
    private boolean levelUseIcons = false;
    private boolean wrap = false;
    private boolean highlightSearchMatchText;
    private DateFormat dateFormatInUse = DATE_FORMATTER;
    private int loggerPrecision = 0;
    private boolean toolTipsVisible;
    private String dateFormatTZ;
    private boolean useRelativeTimesToFixedTime = false;
    private long relativeTimestampBase;

    private static int borderWidth = ChainsawConstants.TABLE_BORDER_WIDTH;

    private final Color borderColor;

    private final JTextPane levelTextPane = new JTextPane();
    private JTextPane singleLineTextPane = new JTextPane();

    private final JPanel multiLinePanel = new JPanel(new BorderLayout());
    private final JPanel generalPanel = new JPanel(new BorderLayout());
    private final JPanel levelPanel = new JPanel(new BorderLayout());
    private ApplicationPreferenceModel applicationPreferenceModel;
    private JTextPane multiLineTextPane;
    private MutableAttributeSet boldAttributeSet;
    private TabSet tabs;
    private int maxHeight;
    private boolean useRelativeTimesToPrevious;
    private EventContainer eventContainer;
    private LogPanelPreferenceModel logPanelPreferenceModel;
    private SimpleAttributeSet insetAttributeSet;
    private boolean colorizeSearch;

    /**
     * Creates a new TableColorizingRenderer object.
     */
    public TableColorizingRenderer(RuleColorizer colorizer, ApplicationPreferenceModel applicationPreferenceModel,
                                   EventContainer eventContainer, LogPanelPreferenceModel logPanelPreferenceModel,
                                   boolean colorizeSearch) {
        this.applicationPreferenceModel = applicationPreferenceModel;
        this.logPanelPreferenceModel = logPanelPreferenceModel;
        this.eventContainer = eventContainer;
        this.colorizeSearch = colorizeSearch;
        multiLinePanel.setLayout(new BoxLayout(multiLinePanel, BoxLayout.Y_AXIS));
        generalPanel.setLayout(new BoxLayout(generalPanel, BoxLayout.Y_AXIS));
        levelPanel.setLayout(new BoxLayout(levelPanel, BoxLayout.Y_AXIS));
        maxHeight = Toolkit.getDefaultToolkit().getScreenSize().height;

        iconMap = new HashMap<>();
        try {
            iconMap.putAll(LevelIconFactory.getInstance().getLevelToIconMap());
        } catch (IllegalStateException ise) {
            //ignore
        }

        if (UIManager.get("Table.selectionBackground") != null) {
            borderColor = (Color) UIManager.get("Table.selectionBackground");
        } else {
            borderColor = Color.BLUE;
        }
        //define the 'bold' attributeset
        boldAttributeSet = new SimpleAttributeSet();
        StyleConstants.setBold(boldAttributeSet, true);

        insetAttributeSet = new SimpleAttributeSet();
        StyleConstants.setLeftIndent(insetAttributeSet, 6);
        //throwable col may have a tab..if so, render the tab as col zero
        int pos = 0;
        int align = TabStop.ALIGN_LEFT;
        int leader = TabStop.LEAD_NONE;
        TabStop tabStop = new TabStop(pos, align, leader);
        tabs = new TabSet(new TabStop[]{tabStop});

        levelTextPane.setOpaque(true);
        levelTextPane.setText("");

        levelPanel.add(levelTextPane);

        this.colorizer = colorizer;
        multiLineTextPane = new JTextPane();
        multiLineTextPane.setEditorKit(new StyledEditorKit());

        singleLineTextPane.setEditorKit(new OneLineEditorKit());
        levelTextPane.setEditorKit(new OneLineEditorKit());

        multiLineTextPane.setEditable(false);
        multiLineTextPane.setFont(levelTextPane.getFont());

        multiLineTextPane.setParagraphAttributes(insetAttributeSet, false);
        singleLineTextPane.setParagraphAttributes(insetAttributeSet, false);
        levelTextPane.setParagraphAttributes(insetAttributeSet, false);
    }

    public void setToolTipsVisible(boolean toolTipsVisible) {
        this.toolTipsVisible = toolTipsVisible;
    }

    public Component getTableCellRendererComponent(
        final JTable table, Object value, boolean isSelected, boolean hasFocus,
        int row, int col) {
        EventContainer container = (EventContainer) table.getModel();
        LoggingEventWrapper loggingEventWrapper = container.getRow(row);
        value = formatField(value, loggingEventWrapper);
        TableColumn tableColumn = table.getColumnModel().getColumn(col);
        int width = tableColumn.getWidth();
        JLabel label = (JLabel) super.getTableCellRendererComponent(table, value,
            isSelected, hasFocus, row, col);
        //chainsawcolumns uses one-based indexing
        int colIndex = tableColumn.getModelIndex() + 1;

        //no event, use default renderer
        if (loggingEventWrapper == null) {
            return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col);
        }
        long delta = 0;
        if (row > 0) {
            LoggingEventWrapper previous = eventContainer.getRow(row - 1);
            delta = Math.min(ChainsawConstants.MILLIS_DELTA_RENDERING_HEIGHT_MAX, Math.max(0, (long) ((loggingEventWrapper.getLoggingEvent().getTimeStamp() - previous.getLoggingEvent().getTimeStamp()) * ChainsawConstants.MILLIS_DELTA_RENDERING_FACTOR)));
        }

        Map matches = loggingEventWrapper.getSearchMatches();

        JComponent component;
        switch (colIndex) {
            case ChainsawColumns.INDEX_THROWABLE_COL_NAME:
                if (value instanceof String[] && ((String[]) value).length > 0) {
                    Style tabStyle = singleLineTextPane.getLogicalStyle();
                    StyleConstants.setTabSet(tabStyle, tabs);
                    //set the 1st tab at position 3
                    singleLineTextPane.setLogicalStyle(tabStyle);
                    //exception string is split into an array..just highlight the first line completely if anything in the exception matches if we have a match for the exception field
                    Set exceptionMatches = (Set) matches.get(LoggingEventFieldResolver.EXCEPTION_FIELD);
                    if (exceptionMatches != null && exceptionMatches.size() > 0) {
                        singleLineTextPane.setText(((String[]) value)[0]);
                        boldAll((StyledDocument) singleLineTextPane.getDocument());
                    } else {
                        singleLineTextPane.setText(((String[]) value)[0]);
                    }
                } else {
                    singleLineTextPane.setText("");
                }
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_LOGGER_COL_NAME:
                String logger = value.toString();
                int startPos = -1;

                for (int i = 0; i < loggerPrecision; i++) {
                    startPos = logger.indexOf(".", startPos + 1);
                    if (startPos < 0) {
                        break;
                    }
                }
                singleLineTextPane.setText(logger.substring(startPos + 1));
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.LOGGER_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_ID_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.PROP_FIELD + "LOG4JID"), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_CLASS_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.CLASS_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_FILE_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.FILE_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_LINE_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.LINE_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_NDC_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.NDC_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_THREAD_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.THREAD_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_TIMESTAMP_COL_NAME:
                //timestamp matches contain the millis..not the display text..just highlight if we have a match for the timestamp field
                Set timestampMatches = (Set) matches.get(LoggingEventFieldResolver.TIMESTAMP_FIELD);
                if (timestampMatches != null && timestampMatches.size() > 0) {
                    singleLineTextPane.setText(value.toString());
                    boldAll((StyledDocument) singleLineTextPane.getDocument());
                } else {
                    singleLineTextPane.setText(value.toString());
                }
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_METHOD_COL_NAME:
                singleLineTextPane.setText(value.toString());
                setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.METHOD_FIELD), (StyledDocument) singleLineTextPane.getDocument());
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
            case ChainsawColumns.INDEX_LOG4J_MARKER_COL_NAME:
            case ChainsawColumns.INDEX_MESSAGE_COL_NAME:
                String thisString = value.toString().trim();
                JTextPane textPane = wrap ? multiLineTextPane : singleLineTextPane;
                JComponent textPaneContainer = wrap ? multiLinePanel : generalPanel;
                textPane.setText(thisString);

                if (colIndex == ChainsawColumns.INDEX_LOG4J_MARKER_COL_NAME) {
                    //property keys are set as all uppercase
                    setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.PROP_FIELD + ChainsawConstants.LOG4J_MARKER_COL_NAME_LOWERCASE.toUpperCase()), (StyledDocument) textPane.getDocument());
                } else {
                    setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.MSG_FIELD), (StyledDocument) textPane.getDocument());
                }
                textPaneContainer.removeAll();
                if (delta > 0 && logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
                    JPanel newPanel = new JPanel();
                    newPanel.setOpaque(true);
                    newPanel.setBackground(applicationPreferenceModel.getDeltaColor());
                    newPanel.setPreferredSize(new Dimension(width, (int) delta));
                    textPaneContainer.add(newPanel, BorderLayout.NORTH);
                }
                textPaneContainer.add(textPane, BorderLayout.SOUTH);

                if (delta == 0 || !logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
                    if (col == 0) {
                        textPane.setBorder(getLeftBorder(isSelected, delta));
                    } else if (col == table.getColumnCount() - 1) {
                        textPane.setBorder(getRightBorder(isSelected, delta));
                    } else {
                        textPane.setBorder(getMiddleBorder(isSelected, delta));
                    }
                } else {
                    if (col == 0) {
                        textPane.setBorder(getLeftBorder(isSelected, 0));
                    } else if (col == table.getColumnCount() - 1) {
                        textPane.setBorder(getRightBorder(isSelected, 0));
                    } else {
                        textPane.setBorder(getMiddleBorder(isSelected, 0));
                    }
                }
                int currentMarkerHeight = loggingEventWrapper.getMarkerHeight();
                int currentMsgHeight = loggingEventWrapper.getMsgHeight();
                int newRowHeight = ChainsawConstants.DEFAULT_ROW_HEIGHT;
                boolean setHeight = false;

                if (wrap) {
            /*
            calculating the height -would- be the correct thing to do, but setting the size to screen size works as well and
            doesn't incur massive overhead, like calculateHeight does
            Map paramMap = new HashMap();
            paramMap.put(TextAttribute.FONT, multiLineTextPane.getFont());

            int calculatedHeight = calculateHeight(thisString, width, paramMap);
             */
                    //instead, set size to max height
                    textPane.setSize(new Dimension(width, maxHeight));
                    int multiLinePanelPrefHeight = textPaneContainer.getPreferredSize().height;
                    newRowHeight = Math.max(ChainsawConstants.DEFAULT_ROW_HEIGHT, multiLinePanelPrefHeight);

                }
                if (!wrap && logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
                    textPane.setSize(new Dimension(Integer.MAX_VALUE, ChainsawConstants.DEFAULT_ROW_HEIGHT));
                    newRowHeight = (int) (ChainsawConstants.DEFAULT_ROW_HEIGHT + delta);
                }

                if (colIndex == ChainsawColumns.INDEX_LOG4J_MARKER_COL_NAME) {
                    loggingEventWrapper.setMarkerHeight(newRowHeight);
                    if (newRowHeight != currentMarkerHeight && newRowHeight >= loggingEventWrapper.getMsgHeight()) {
                        setHeight = true;
                    }
                }

                if (colIndex == ChainsawColumns.INDEX_MESSAGE_COL_NAME) {
                    loggingEventWrapper.setMsgHeight(newRowHeight);
                    if (newRowHeight != currentMsgHeight && newRowHeight >= loggingEventWrapper.getMarkerHeight()) {
                        setHeight = true;
                    }
                }
                if (setHeight) {
                    table.setRowHeight(row, newRowHeight);
                }

                component = textPaneContainer;
                break;
            case ChainsawColumns.INDEX_LEVEL_COL_NAME:
                if (levelUseIcons) {
                    levelTextPane.setText("");
                    levelTextPane.insertIcon(iconMap.get(value.toString()));
                    if (!toolTipsVisible) {
                        levelTextPane.setToolTipText(value.toString());
                    }
                } else {
                    levelTextPane.setText(value.toString());
                    setHighlightAttributesInternal(matches.get(LoggingEventFieldResolver.LEVEL_FIELD), (StyledDocument) levelTextPane.getDocument());
                    if (!toolTipsVisible) {
                        levelTextPane.setToolTipText(null);
                    }
                }
                if (toolTipsVisible) {
                    levelTextPane.setToolTipText(label.getToolTipText());
                }
                levelTextPane.setForeground(label.getForeground());
                levelTextPane.setBackground(label.getBackground());
                layoutRenderingPanel(levelPanel, levelTextPane, delta, isSelected, width, col, table);
                component = levelPanel;
                break;

            //remaining entries are properties
            default:
                Set propertySet = loggingEventWrapper.getPropertyKeySet();
                String headerName = tableColumn.getHeaderValue().toString().toLowerCase();
                String thisProp = null;
                //find the property in the property set...case-sensitive
                for (Object aPropertySet : propertySet) {
                    String entry = aPropertySet.toString();
                    if (entry.equalsIgnoreCase(headerName)) {
                        thisProp = entry;
                        break;
                    }
                }
                if (thisProp != null) {
                    String propKey = LoggingEventFieldResolver.PROP_FIELD + thisProp.toUpperCase();
                    Set propKeyMatches = (Set) matches.get(propKey);
                    singleLineTextPane.setText(loggingEventWrapper.getLoggingEvent().getProperty(thisProp));
                    setHighlightAttributesInternal(propKeyMatches, (StyledDocument) singleLineTextPane.getDocument());
                } else {
                    singleLineTextPane.setText("");
                }
                layoutRenderingPanel(generalPanel, singleLineTextPane, delta, isSelected, width, col, table);
                component = generalPanel;
                break;
        }

        Color background;
        Color foreground;
        Rule loggerRule = colorizer.getLoggerRule();
        //use logger colors in table instead of event colors if event passes logger rule
        if (loggerRule != null && loggerRule.evaluate(loggingEventWrapper.getLoggingEvent(), null)) {
            background = applicationPreferenceModel.getSearchBackgroundColor();
            foreground = applicationPreferenceModel.getSearchForegroundColor();
        } else {
            if (colorizeSearch && !applicationPreferenceModel.isBypassSearchColors()) {
                background = loggingEventWrapper.isSearchMatch() ? applicationPreferenceModel.getSearchBackgroundColor() : loggingEventWrapper.getBackground();
                foreground = loggingEventWrapper.isSearchMatch() ? applicationPreferenceModel.getSearchForegroundColor() : loggingEventWrapper.getForeground();
            } else {
                background = loggingEventWrapper.getBackground();
                foreground = loggingEventWrapper.getForeground();
            }
        }

        /**
         * Colourize background based on row striping if the event still has default foreground and background color
         */
        if (background.equals(ChainsawConstants.COLOR_DEFAULT_BACKGROUND) && foreground.equals(ChainsawConstants.COLOR_DEFAULT_FOREGROUND) && (row % 2) != 0) {
            background = applicationPreferenceModel.getAlternatingColorBackgroundColor();
            foreground = applicationPreferenceModel.getAlternatingColorForegroundColor();
        }

        component.setBackground(background);
        component.setForeground(foreground);

        //update the background & foreground of the jtextpane using styles
        if (multiLineTextPane != null) {
            updateColors(multiLineTextPane, background, foreground);
        }
        updateColors(levelTextPane, background, foreground);
        updateColors(singleLineTextPane, background, foreground);

        return component;
    }

    private void layoutRenderingPanel(JComponent container, JComponent bottomComponent, long delta, boolean isSelected,
                                      int width, int col, JTable table) {
        container.removeAll();
        if (delta == 0 || !logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
            if (col == 0) {
                bottomComponent.setBorder(getLeftBorder(isSelected, delta));
            } else if (col == table.getColumnCount() - 1) {
                bottomComponent.setBorder(getRightBorder(isSelected, delta));
            } else {
                bottomComponent.setBorder(getMiddleBorder(isSelected, delta));
            }
        } else {
            JPanel newPanel = new JPanel();
            newPanel.setOpaque(true);
            newPanel.setBackground(applicationPreferenceModel.getDeltaColor());
            newPanel.setPreferredSize(new Dimension(width, (int) delta));
            container.add(newPanel, BorderLayout.NORTH);
            if (col == 0) {
                bottomComponent.setBorder(getLeftBorder(isSelected, 0));
            } else if (col == table.getColumnCount() - 1) {
                bottomComponent.setBorder(getRightBorder(isSelected, 0));
            } else {
                bottomComponent.setBorder(getMiddleBorder(isSelected, 0));
            }
        }

        container.add(bottomComponent, BorderLayout.SOUTH);
    }

    private Border getLeftBorder(boolean isSelected, long delta) {
        Border LEFT_BORDER = BorderFactory.createMatteBorder(borderWidth, borderWidth, borderWidth, 0, borderColor);
        Border LEFT_EMPTY_BORDER = BorderFactory.createEmptyBorder(borderWidth, borderWidth, borderWidth, 0);

        Border innerBorder = isSelected ? LEFT_BORDER : LEFT_EMPTY_BORDER;
        if (delta == 0 || !wrap || !logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
            return innerBorder;
        } else {
            Border outerBorder = BorderFactory.createMatteBorder((int) Math.max(borderWidth, delta), 0, 0, 0, applicationPreferenceModel.getDeltaColor());
            return BorderFactory.createCompoundBorder(outerBorder, innerBorder);
        }
    }

    private Border getRightBorder(boolean isSelected, long delta) {
        Border RIGHT_BORDER = BorderFactory.createMatteBorder(borderWidth, 0, borderWidth, borderWidth, borderColor);
        Border RIGHT_EMPTY_BORDER = BorderFactory.createEmptyBorder(borderWidth, 0, borderWidth, borderWidth);
        Border innerBorder = isSelected ? RIGHT_BORDER : RIGHT_EMPTY_BORDER;
        if (delta == 0 || !wrap || !logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
            return innerBorder;
        } else {
            Border outerBorder = BorderFactory.createMatteBorder((int) Math.max(borderWidth, delta), 0, 0, 0, applicationPreferenceModel.getDeltaColor());
            return BorderFactory.createCompoundBorder(outerBorder, innerBorder);
        }
    }

    private Border getMiddleBorder(boolean isSelected, long delta) {
        Border MIDDLE_BORDER = BorderFactory.createMatteBorder(borderWidth, 0, borderWidth, 0, borderColor);
        Border MIDDLE_EMPTY_BORDER = BorderFactory.createEmptyBorder(borderWidth, 0, borderWidth, 0);
        Border innerBorder = isSelected ? MIDDLE_BORDER : MIDDLE_EMPTY_BORDER;
        if (delta == 0 || !wrap || !logPanelPreferenceModel.isShowMillisDeltaAsGap()) {
            return innerBorder;
        } else {
            Border outerBorder = BorderFactory.createMatteBorder((int) Math.max(borderWidth, delta), 0, 0, 0, applicationPreferenceModel.getDeltaColor());
            return BorderFactory.createCompoundBorder(outerBorder, innerBorder);
        }
    }

    private void updateColors(JTextPane textPane, Color background, Color foreground) {
        StyledDocument styledDocument = textPane.getStyledDocument();
        MutableAttributeSet attributes = textPane.getInputAttributes();
        StyleConstants.setForeground(attributes, foreground);
        styledDocument.setCharacterAttributes(0, styledDocument.getLength() + 1, attributes, false);
        textPane.setBackground(background);
    }

    /**
     * Changes the Date Formatting object to be used for rendering dates.
     *
     * @param formatter
     */
    void setDateFormatter(DateFormat formatter) {
        this.dateFormatInUse = formatter;
        if (dateFormatInUse != null && dateFormatTZ != null && !("".equals(dateFormatTZ))) {
            dateFormatInUse.setTimeZone(TimeZone.getTimeZone(dateFormatTZ));
        } else {
            dateFormatInUse.setTimeZone(TimeZone.getDefault());
        }
    }

    /**
     * Changes the Logger precision.
     *
     * @param loggerPrecisionText
     */
    void setLoggerPrecision(String loggerPrecisionText) {
        try {
            loggerPrecision = Integer.parseInt(loggerPrecisionText);
        } catch (NumberFormatException nfe) {
            loggerPrecision = 0;
        }
    }

    /**
     * Format date field
     *
     * @param field object
     * @return formatted object
     */
    private Object formatField(Object field, LoggingEventWrapper loggingEventWrapper) {
        if (!(field instanceof Date)) {
            return (field == null ? "" : field);
        }

        //handle date field
        if (useRelativeTimesToFixedTime) {
            return "" + (((Date) field).getTime() - relativeTimestampBase);
        }
        if (useRelativeTimesToPrevious) {
            return loggingEventWrapper.getLoggingEvent().getProperty(ChainsawConstants.MILLIS_DELTA_COL_NAME_LOWERCASE);
        }

        return dateFormatInUse.format((Date) field);
    }

    /**
     * Sets the property which determines whether to wrap the message
     *
     * @param wrapMsg
     */
    public void setWrapMessage(boolean wrapMsg) {
        this.wrap = wrapMsg;
    }

    /**
     * Sets the property which determines whether to use Icons or text
     * for the Level column
     *
     * @param levelUseIcons
     */
    public void setLevelUseIcons(boolean levelUseIcons) {
        this.levelUseIcons = levelUseIcons;
    }

    public void setTimeZone(String dateFormatTZ) {
        this.dateFormatTZ = dateFormatTZ;

        if (dateFormatInUse != null && dateFormatTZ != null && !("".equals(dateFormatTZ))) {
            dateFormatInUse.setTimeZone(TimeZone.getTimeZone(dateFormatTZ));
        } else {
            dateFormatInUse.setTimeZone(TimeZone.getDefault());
        }
    }

    public void setUseRelativeTimes(long timeStamp) {
        useRelativeTimesToFixedTime = true;
        useRelativeTimesToPrevious = false;
        relativeTimestampBase = timeStamp;
    }

    public void setUseRelativeTimesToPreviousRow() {
        useRelativeTimesToFixedTime = false;
        useRelativeTimesToPrevious = true;
    }

    public void setUseNormalTimes() {
        useRelativeTimesToFixedTime = false;
        useRelativeTimesToPrevious = false;
    }

  /*
   private int calculateHeight(String string, int width, Map paramMap) {
     if (string.trim().length() == 0) {
         return ChainsawConstants.DEFAULT_ROW_HEIGHT;
     }
     AttributedCharacterIterator paragraph = new AttributedString(string, paramMap).getIterator();
     LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, new FontRenderContext(null, true, true));
     float height = 0;
     lineMeasurer.setPosition(paragraph.getBeginIndex());
     TextLayout layout;
     while (lineMeasurer.getPosition() < paragraph.getEndIndex()) {
       layout = lineMeasurer.nextLayout(width);
         float layoutHeight = layout.getAscent() + layout.getDescent() + layout.getLeading();
         height += layoutHeight;
     }
     return Math.max(ChainsawConstants.DEFAULT_ROW_HEIGHT, (int) height);
    }
    */

    private void setHighlightAttributesInternal(Object matchSet, StyledDocument styledDocument) {
        if (!highlightSearchMatchText) {
            return;
        }
        setHighlightAttributes(matchSet, styledDocument);
    }

    public void setHighlightAttributes(Object matchSet, StyledDocument styledDocument) {
        if (matchSet instanceof Set) {
            Set thisSet = (Set) matchSet;
            for (Object aThisSet : thisSet) {
                String thisEntry = aThisSet.toString();
                bold(thisEntry, styledDocument);
            }
        }
    }

    private void boldAll(StyledDocument styledDocument) {
        if (!highlightSearchMatchText) {
            return;
        }
        styledDocument.setCharacterAttributes(0, styledDocument.getLength(), boldAttributeSet, false);
    }

    private void bold(String textToBold, StyledDocument styledDocument) {
        try {
            String lowerInput = styledDocument.getText(0, styledDocument.getLength()).toLowerCase();
            String lowerTextToBold = textToBold.toLowerCase();
            int textToBoldLength = textToBold.length();
            int firstIndex = 0;
            int currentIndex;
            while ((currentIndex = lowerInput.indexOf(lowerTextToBold, firstIndex)) > -1) {
                styledDocument.setCharacterAttributes(currentIndex, textToBoldLength, boldAttributeSet, false);
                firstIndex = currentIndex + textToBoldLength;
            }
        } catch (BadLocationException e) {
            //ignore
        }
    }

    public void setHighlightSearchMatchText(boolean highlightSearchMatchText) {
        this.highlightSearchMatchText = highlightSearchMatchText;
    }

    private class OneLineEditorKit extends StyledEditorKit {
        private ViewFactory viewFactoryImpl = new ViewFactoryImpl();

        public ViewFactory getViewFactory() {
            return viewFactoryImpl;
        }
    }

    private class ViewFactoryImpl implements ViewFactory {
        public View create(Element elem) {
            String elementName = elem.getName();
            if (elementName != null) {
                switch (elementName) {
                    case AbstractDocument.ParagraphElementName:
                        return new OneLineParagraphView(elem);
                    case AbstractDocument.ContentElementName:
                        return new LabelView(elem);
                    case AbstractDocument.SectionElementName:
                        return new BoxView(elem, View.Y_AXIS);
                    case StyleConstants.ComponentElementName:
                        return new ComponentView(elem);
                    case StyleConstants.IconElementName:
                        return new IconView(elem);
                }
            }
            return new LabelView(elem);
        }
    }

    private class OneLineParagraphView extends ParagraphView {
        public OneLineParagraphView(Element elem) {
            super(elem);
        }

        //this is the main fix - set the flow span to be max val
        public int getFlowSpan(int index) {
            return Integer.MAX_VALUE;
        }
    }
}
