/*
 * This file is part of lanterna (https://github.com/mabe02/lanterna).
 * 
 * lanterna is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Copyright (C) 2010-2020 Martin Berglund
 */
package com.googlecode.lanterna.gui2;

import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TextColor;
import com.googlecode.lanterna.graphics.ThemeDefinition;

import java.util.EnumSet;
import java.util.List;

/**
 * Label is a simple read-only text display component. It supports customized colors and multi-line text.
 * @author Martin
 */
public class Label extends AbstractComponent<Label> {
    private String[] lines;
    private Integer labelWidth;
    private TerminalSize labelSize;
    private TextColor foregroundColor;
    private TextColor backgroundColor;
    private final EnumSet<SGR> additionalStyles;

    /**
     * Main constructor, creates a new Label displaying a specific text.
     * @param text Text the label will display
     */
    public Label(String text) {
        this.lines = null;
        this.labelSize = TerminalSize.ZERO;
        this.labelWidth = 0;
        this.foregroundColor = null;
        this.backgroundColor = null;
        this.additionalStyles = EnumSet.noneOf(SGR.class);
        setText(text);
    }

    /**
     * Protected access to set the internal representation of the text in this label, to be used by sub-classes of label
     * in certain cases where {@code setText(..)} doesn't work. In general, you probably want to stick to
     * {@code setText(..)} instead of this method unless you have a good reason not to.
     * @param lines New lines this label will display
     */
    protected void setLines(String[] lines) {
        this.lines = lines;
    }

    /**
     * Updates the text this label is displaying
     * @param text New text to display
     */
    public synchronized void setText(String text) {
        setLines(splitIntoMultipleLines(text));
        this.labelSize = getBounds(lines, labelSize);
        invalidate();
    }

    /**
     * Returns the text this label is displaying. Multi-line labels will have their text concatenated with \n, even if
     * they were originally set using multi-line text having \r\n as line terminators.
     * @return String of the text this label is displaying
     */
    public synchronized String getText() {
        if(lines.length == 0) {
            return "";
        }
        StringBuilder bob = new StringBuilder(lines[0]);
        for(int i = 1; i < lines.length; i++) {
            bob.append("\n").append(lines[i]);
        }
        return bob.toString();
    }

    /**
     * Utility method for taking a string and turning it into an array of lines. This method is used in order to deal
     * with line endings consistently.
     * @param text Text to split
     * @return Array of strings that forms the lines of the original string
     */
    protected String[] splitIntoMultipleLines(String text) {
        return text.replace("\r", "").split("\n");
    }

    /**
     * Returns the area, in terminal columns and rows, required to fully draw the lines passed in.
     * @param lines Lines to measure the size of
     * @param currentBounds Optional (can pass {@code null}) terminal size to use for storing the output values. If the
     *                      method is called many times and always returning the same value, passing in an external
     *                      reference of this size will avoid creating new {@code TerminalSize} objects every time
     * @return Size that is required to draw the lines
     */
    protected TerminalSize getBounds(String[] lines, TerminalSize currentBounds) {
        if(currentBounds == null) {
            currentBounds = TerminalSize.ZERO;
        }
        currentBounds = currentBounds.withRows(lines.length);
        if(labelWidth == null || labelWidth == 0) {
            int preferredWidth = 0;
            for(String line : lines) {
                int lineWidth = TerminalTextUtils.getColumnWidth(line);
                if(preferredWidth < lineWidth) {
                    preferredWidth = lineWidth;
                }
            }
            currentBounds = currentBounds.withColumns(preferredWidth);
        }
        else {
            List<String> wordWrapped = TerminalTextUtils.getWordWrappedText(labelWidth, lines);
            currentBounds = currentBounds.withColumns(labelWidth).withRows(wordWrapped.size());
        }
        return currentBounds;
    }

    /**
     * Overrides the current theme's foreground color and use the one specified. If called with {@code null}, the
     * override is cleared and the theme is used again.
     * @param foregroundColor Foreground color to use when drawing the label, if {@code null} then use the theme's
     *                        default
     * @return Itself
     */
    public synchronized Label setForegroundColor(TextColor foregroundColor) {
        this.foregroundColor = foregroundColor;
        return this;
    }

    /**
     * Returns the foreground color used when drawing the label, or {@code null} if the color is read from the current
     * theme.
     * @return Foreground color used when drawing the label, or {@code null} if the color is read from the current
     * theme.
     */
    public TextColor getForegroundColor() {
        return foregroundColor;
    }

    /**
     * Overrides the current theme's background color and use the one specified. If called with {@code null}, the
     * override is cleared and the theme is used again.
     * @param backgroundColor Background color to use when drawing the label, if {@code null} then use the theme's
     *                        default
     * @return Itself
     */
    public synchronized Label setBackgroundColor(TextColor backgroundColor) {
        this.backgroundColor = backgroundColor;
        return this;
    }

    /**
     * Returns the background color used when drawing the label, or {@code null} if the color is read from the current
     * theme.
     * @return Background color used when drawing the label, or {@code null} if the color is read from the current
     * theme.
     */
    public TextColor getBackgroundColor() {
        return backgroundColor;
    }

    /**
     * Adds an additional SGR style to use when drawing the label, in case it wasn't enabled by the theme
     * @param sgr SGR style to enable for this label
     * @return Itself
     */
    public synchronized Label addStyle(SGR sgr) {
        additionalStyles.add(sgr);
        return this;
    }

    /**
     * Removes an additional SGR style used when drawing the label, previously added by {@code addStyle(..)}. If the
     * style you are trying to remove is specified by the theme, calling this method will have no effect.
     * @param sgr SGR style to remove
     * @return Itself
     */
    public synchronized Label removeStyle(SGR sgr) {
        additionalStyles.remove(sgr);
        return this;
    }

    /**
     * Use this method to limit how wide the label can grow. If set to {@code null} there is no limit but if set to a
     * positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
     * this label width. This may make the label increase in height as new rows may be requested. Please note that some
     * layout managers might assign more space to the label and because of this the wrapping might not be as you expect
     * it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
     * word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
     * what was requested. By default this is set to 0.
     *
     * @param labelWidth Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
     *                   wrapping should be used if the assigned area is smaller than the requested size, or a positive
     *                   integer setting the requested maximum width at what point word wrapping will begin
     * @return Itself
     */
    public synchronized Label setLabelWidth(Integer labelWidth) {
        this.labelWidth = labelWidth;
        return this;
    }

    /**
     * Returns the limit how wide the label can grow. If set to {@code null} or 0 there is no limit but if set to a
     * positive integer then the preferred size will be calculated using word wrapping for lines that are longer than
     * the label width. This may make the label increase in height as new rows may be requested. Please note that some
     * layout managers might assign more space to the label and because of this the wrapping might not be as you expect
     * it. If set to 0, the label will request the same space as if set to {@code null}, but when drawing it will apply
     * word wrapping instead of truncation in order to fit the label inside the designated area if it's smaller than
     * what was requested.
     * @return Either {@code null} or 0 for no limit on how wide the label can be, where 0 indicates word
     *         wrapping should be used if the assigned area is smaller than the requested size, or a positive
     *         integer setting the requested maximum width at what point word wrapping will begin
     */
    public Integer getLabelWidth() {
        return labelWidth;
    }

    @Override
    protected ComponentRenderer<Label> createDefaultRenderer() {
        return new ComponentRenderer<Label>() {
            @Override
            public TerminalSize getPreferredSize(Label Label) {
                return labelSize;
            }

            @Override
            public void drawComponent(TextGUIGraphics graphics, Label component) {
                ThemeDefinition themeDefinition = component.getThemeDefinition();
                graphics.applyThemeStyle(themeDefinition.getNormal());
                if(foregroundColor != null) {
                    graphics.setForegroundColor(foregroundColor);
                }
                if(backgroundColor != null) {
                    graphics.setBackgroundColor(backgroundColor);
                }
                for(SGR sgr: additionalStyles) {
                    graphics.enableModifiers(sgr);
                }

                String[] linesToDraw;
                if(component.getLabelWidth() == null) {
                    linesToDraw = component.lines;
                }
                else {
                    linesToDraw = TerminalTextUtils.getWordWrappedText(graphics.getSize().getColumns(), component.lines).toArray(new String[0]);
                }

                for(int row = 0; row < Math.min(graphics.getSize().getRows(), linesToDraw.length); row++) {
                    String line = linesToDraw[row];
                    if(graphics.getSize().getColumns() >= labelSize.getColumns()) {
                        graphics.putString(0, row, line);
                    }
                    else {
                        int availableColumns = graphics.getSize().getColumns();
                        String fitString = TerminalTextUtils.fitString(line, availableColumns);
                        graphics.putString(0, row, fitString);
                    }
                }
            }
        };
    }
}
