/*
 * 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.Symbols;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.graphics.ThemeDefinition;

/**
 * This GUI element gives a visual indication of how far a process of some sort has progressed at any given time. It's
 * a classic user interface component that most people are familiar with. It works based on a scale expressed as having
 * a <i>minimum</i>, a <i>maximum</i> and a current <i>value</i> somewhere along that range. When the current
 * <i>value</i> is the same as the <i>minimum</i>, the progress indication is empty, at 0%. If the <i>value</i> is the
 * same as the <i>maximum</i>, the progress indication is filled, at 100%. Any <i>value</i> in between the
 * <i>minimum</i> and the <i>maximum</i> will be indicated proportionally to where on this range between <i>minimum</i>
 * and <i>maximum</i> it is.
 * <p>
 * In order to add a label to the progress bar, for example to print the % completed, this class supports adding a
 * format specification. This label format, before drawing, will be passed in through a {@code String.format(..)} with
 * the current progress of <i>value</i> from <i>minimum</i> to <i>maximum</i> expressed as a {@code float} passed in as
 * a single vararg parameter. This parameter will be scaled from 0.0f to 100.0f. By default, the label format is set to
 * "%2.0f%%" which becomes a simple percentage string when formatted.
 * @author Martin
 */
public class ProgressBar extends AbstractComponent<ProgressBar> {

    private int min;
    private int max;
    private int value;
    private int preferredWidth;
    private String labelFormat;

    /**
     * Creates a new progress bar initially defined with a range from 0 to 100. The
     */
    public ProgressBar() {
        this(0, 100);
    }

    /**
     * Creates a new progress bar with a defined range of minimum to maximum
     * @param min The minimum value of this progress bar
     * @param max The maximum value of this progress bar
     */
    public ProgressBar(int min, int max) {
        this(min, max, 0);
    }

    /**
     * Creates a new progress bar with a defined range of minimum to maximum and also with a hint as to how wide the
     * progress bar should be drawn
     * @param min The minimum value of this progress bar
     * @param max The maximum value of this progress bar
     * @param preferredWidth Width size hint, in number of columns, for this progress bar. The renderer may choose to
     *                       not use this hint. 0 or less means that there is no hint.
     */
    public ProgressBar(int min, int max, int preferredWidth) {
        if(min > max) {
            min = max;
        }
        this.min = min;
        this.max = max;
        this.value = min;
        this.labelFormat = "%2.0f%%";

        if(preferredWidth < 1) {
            preferredWidth = 1;
        }
        this.preferredWidth = preferredWidth;
    }

    /**
     * Returns the current <i>minimum</i> value for this progress bar
     * @return The <i>minimum</i> value of this progress bar
     */
    public int getMin() {
        return min;
    }

    /**
     * Updates the <i>minimum</i> value of this progress bar. If the current <i>maximum</i> and/or <i>value</i> are
     * smaller than this new <i>minimum</i>, they are automatically adjusted so that the range is still valid.
     * @param min New <i>minimum</i> value to assign to this progress bar
     * @return Itself
     */
    public synchronized ProgressBar setMin(int min) {
        if(min > max) {
            setMax(min);
        }
        if(min > value) {
            setValue(min);
        }
        if(this.min != min) {
            this.min = min;
            invalidate();
        }
        return this;
    }

    /**
     * Returns the current <i>maximum</i> value for this progress bar
     * @return The <i>maximum</i> value of this progress bar
     */
    public int getMax() {
        return max;
    }

    /**
     * Updates the <i>maximum</i> value of this progress bar. If the current <i>minimum</i> and/or <i>value</i> are
     * greater than this new <i>maximum</i>, they are automatically adjusted so that the range is still valid.
     * @param max New <i>maximum</i> value to assign to this progress bar
     * @return Itself
     */
    public synchronized ProgressBar setMax(int max) {
        if(max < min) {
            setMin(max);
        }
        if(max < value) {
            setValue(max);
        }
        if(this.max != max) {
            this.max = max;
            invalidate();
        }
        return this;
    }

    /**
     * Returns the current <i>value</i> of this progress bar, which represents how complete the progress indication is.
     * @return The progress value of this progress bar
     */
    public int getValue() {
        return value;
    }

    /**
     * Updates the <i>value</i> of this progress bar, which will update the visual state. If the value passed in is
     * outside the <i>minimum-maximum</i> range, it is automatically adjusted.
     * @param value New value of the progress bar
     * @return Itself
     */
    public synchronized ProgressBar setValue(int value) {
        if(value < min) {
            value = min;
        }
        if(value > max) {
            value = max;
        }
        if(this.value != value) {
            this.value = value;
            invalidate();
        }
        return this;
    }

    /**
     * Returns the preferred width of the progress bar component, in number of columns. If 0 or less, it should be
     * interpreted as no preference on width and it's up to the renderer to decide.
     * @return Preferred width this progress bar would like to have, or 0 (or less) if no preference
     */
    public int getPreferredWidth() {
        return preferredWidth;
    }

    /**
     * Updated the preferred width hint, which tells the renderer how wide this progress bar would like to be. If called
     * with 0 (or less), it means no preference on width and the renderer will have to figure out on its own how wide
     * to make it.
     * @param preferredWidth New preferred width in number of columns, or 0 if no preference
     */
    public void setPreferredWidth(int preferredWidth) {
        this.preferredWidth = preferredWidth;
    }

    /**
     * Returns the current label format string which is the template for what the progress bar would like to be the
     * label printed. Exactly how this label is printed depends on the renderer, but the default renderer will print
     * the label centered in the middle of the progress indication.
     * @return The label format template string this progress bar is currently using
     */
    public String getLabelFormat() {
        return labelFormat;
    }

    /**
     * Sets the label format this progress bar should use when the component is drawn. The string would be compatible
     * with {@code String.format(..)}, the class will pass the string through that method and pass in the current
     * progress as a single vararg parameter (passed in as a {@code float} in the range of 0.0f to 100.0f). Setting this
     * format string to null or empty string will turn off the label rendering.
     * @param labelFormat Label format to use when drawing the progress bar, or {@code null} to disable
     * @return Itself
     */
    public synchronized ProgressBar setLabelFormat(String labelFormat) {
        this.labelFormat = labelFormat;
        invalidate();
        return this;
    }

    /**
     * Returns the current progress of this progress bar's <i>value</i> from <i>minimum</i> to <i>maximum</i>, expressed
     * as a float from 0.0f to 1.0f.
     * @return current progress of this progress bar expressed as a float from 0.0f to 1.0f.
     */
    public synchronized float getProgress() {
        return (float)(value - min) / (float)max;
    }

    /**
     * Returns the label of this progress bar formatted through {@code String.format(..)} with the current progress
     * value.
     * @return The progress bar label formatted with the current progress
     */
    public synchronized String getFormattedLabel() {
        if(labelFormat == null) {
            return "";
        }
        return String.format(labelFormat, getProgress() * 100.0f);
    }

    @Override
    protected ComponentRenderer<ProgressBar> createDefaultRenderer() {
        return new DefaultProgressBarRenderer();
    }

    /**
     * Default implementation of the progress bar GUI component renderer. This renderer will draw the progress bar
     * on a single line and gradually fill up the space with a different color as the progress is increasing.
     */
    public static class DefaultProgressBarRenderer implements ComponentRenderer<ProgressBar> {
        @Override
        public TerminalSize getPreferredSize(ProgressBar component) {
            int preferredWidth = component.getPreferredWidth();
            if(preferredWidth > 0) {
                return new TerminalSize(preferredWidth, 1);
            }
            else if(component.getLabelFormat() != null && !component.getLabelFormat().trim().isEmpty()) {
                return new TerminalSize(TerminalTextUtils.getColumnWidth(String.format(component.getLabelFormat(), 100.0f)) + 2, 1);
            }
            else {
                return new TerminalSize(10, 1);
            }
        }

        @Override
        public void drawComponent(TextGUIGraphics graphics, ProgressBar component) {
            TerminalSize size = graphics.getSize();
            if(size.getRows() == 0 || size.getColumns() == 0) {
                return;
            }
            ThemeDefinition themeDefinition = component.getThemeDefinition();
            int columnOfProgress = (int)(component.getProgress() * size.getColumns());
            String label = component.getFormattedLabel();
            int labelRow = size.getRows() / 2;

            // Adjust label so it fits inside the component
            int labelWidth = TerminalTextUtils.getColumnWidth(label);

            // Can't be too smart about this, because of CJK characters
            if(labelWidth > size.getColumns()) {
                boolean tail = true;
                while (labelWidth > size.getColumns()) {
                    if(tail) {
                        label = label.substring(0, label.length() - 1);
                    }
                    else {
                        label = label.substring(1);
                    }
                    tail = !tail;
                    labelWidth = TerminalTextUtils.getColumnWidth(label);
                }
            }
            int labelStartPosition = (size.getColumns() - labelWidth) / 2;

            for(int row = 0; row < size.getRows(); row++) {
                graphics.applyThemeStyle(themeDefinition.getActive());
                for(int column = 0; column < size.getColumns(); column++) {
                    if(column == columnOfProgress) {
                        graphics.applyThemeStyle(themeDefinition.getNormal());
                    }
                    if(row == labelRow && column >= labelStartPosition && column < labelStartPosition + labelWidth) {
                        char character = label.charAt(TerminalTextUtils.getStringCharacterIndex(label, column - labelStartPosition));
                        graphics.setCharacter(column, row, character);
                        if(TerminalTextUtils.isCharDoubleWidth(character)) {
                            column++;
                            if(column == columnOfProgress) {
                                graphics.applyThemeStyle(themeDefinition.getNormal());
                            }
                        }
                    }
                    else {
                        graphics.setCharacter(column, row, themeDefinition.getCharacter("FILLER", ' '));
                    }
                }
            }
        }
    }

    /**
     * This progress bar renderer implementation takes slightly more space (three rows) and draws a slightly more
     * complicates progress bar with fixed measurers to mark 25%, 50% and 75%. Maybe you have seen this one before
     * somewhere?
     */
    public static class LargeProgressBarRenderer implements ComponentRenderer<ProgressBar> {
        @Override
        public TerminalSize getPreferredSize(ProgressBar component) {
            int preferredWidth = component.getPreferredWidth();
            if(preferredWidth > 0) {
                return new TerminalSize(preferredWidth, 3);
            }
            else {
                return new TerminalSize(42, 3);
            }
        }

        @Override
        public void drawComponent(TextGUIGraphics graphics, ProgressBar component) {
            TerminalSize size = graphics.getSize();
            if(size.getRows() == 0 || size.getColumns() == 0) {
                return;
            }
            ThemeDefinition themeDefinition = component.getThemeDefinition();
            int columnOfProgress = (int)(component.getProgress() * (size.getColumns() - 4));
            int mark25 = -1;
            int mark50 = -1;
            int mark75 = -1;

            if(size.getColumns() > 9) {
                mark50 = (size.getColumns() - 2) / 2;
            }
            if(size.getColumns() > 16) {
                mark25 = (size.getColumns() - 2) / 4;
                mark75 = mark50 + mark25;
            }

            // Draw header, if there are at least 3 rows available
            int rowOffset = 0;
            if(size.getRows() >= 3) {
                graphics.applyThemeStyle(themeDefinition.getNormal());
                graphics.drawLine(0, 0, size.getColumns(), 0, ' ');
                if(size.getColumns() > 1) {
                    graphics.setCharacter(1, 0, '0');
                }
                if(mark25 != -1) {
                    if(component.getProgress() < 0.25f) {
                        graphics.applyThemeStyle(themeDefinition.getInsensitive());
                    }
                    graphics.putString(1 + mark25, 0, "25");
                }
                if(mark50 != -1) {
                    if(component.getProgress() < 0.50f) {
                        graphics.applyThemeStyle(themeDefinition.getInsensitive());
                    }
                    graphics.putString(1 + mark50, 0, "50");
                }
                if(mark75 != -1) {
                    if(component.getProgress() < 0.75f) {
                        graphics.applyThemeStyle(themeDefinition.getInsensitive());
                    }
                    graphics.putString(1 + mark75, 0, "75");
                }
                if(size.getColumns() >= 7) {
                    if(component.getProgress() < 1.0f) {
                        graphics.applyThemeStyle(themeDefinition.getInsensitive());
                    }
                    graphics.putString(size.getColumns() - 3, 0, "100");
                }
                rowOffset++;
            }

            // Draw the main indicator
            for(int i = 0; i < Math.max(1, size.getRows() - 2); i++) {
                graphics.applyThemeStyle(themeDefinition.getNormal());
                graphics.drawLine(0, rowOffset, size.getColumns(), rowOffset, ' ');
                if (size.getColumns() > 2) {
                    graphics.setCharacter(1, rowOffset, Symbols.SINGLE_LINE_VERTICAL);
                }
                if (size.getColumns() > 3) {
                    graphics.setCharacter(size.getColumns() - 2, rowOffset, Symbols.SINGLE_LINE_VERTICAL);
                }
                if (size.getColumns() > 4) {
                    graphics.applyThemeStyle(themeDefinition.getActive());
                    for(int columnOffset = 2; columnOffset < size.getColumns() - 2; columnOffset++) {
                        if(columnOfProgress + 2 == columnOffset) {
                            graphics.applyThemeStyle(themeDefinition.getNormal());
                        }
                        if(mark25 == columnOffset - 1) {
                            graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL);
                        }
                        else if(mark50 == columnOffset - 1) {
                            graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL);
                        }
                        else if(mark75 == columnOffset - 1) {
                            graphics.setCharacter(columnOffset, rowOffset, Symbols.SINGLE_LINE_VERTICAL);
                        }
                        else {
                            graphics.setCharacter(columnOffset, rowOffset, ' ');
                        }
                    }
                }

                if(((int)(component.getProgress() * ((size.getColumns() - 4) * 2))) % 2 == 1) {
                    graphics.applyThemeStyle(themeDefinition.getPreLight());
                    graphics.setCharacter(columnOfProgress + 2, rowOffset, '|');
                }

                rowOffset++;
            }

            // Draw footer if there are at least 2 rows, this one is easy
            if(size.getRows() >= 2) {
                graphics.applyThemeStyle(themeDefinition.getNormal());
                graphics.drawLine(0, rowOffset, size.getColumns(), rowOffset, Symbols.SINGLE_LINE_T_UP);
                graphics.setCharacter(0, rowOffset, ' ');
                if (size.getColumns() > 1) {
                    graphics.setCharacter(size.getColumns() - 1, rowOffset, ' ');
                }
                if (size.getColumns() > 2) {
                    graphics.setCharacter(1, rowOffset, Symbols.SINGLE_LINE_BOTTOM_LEFT_CORNER);
                }
                if (size.getColumns() > 3) {
                    graphics.setCharacter(size.getColumns() - 2, rowOffset, Symbols.SINGLE_LINE_BOTTOM_RIGHT_CORNER);
                }
            }
        }
    }
}