/*
 * 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.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;

import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Simple layout manager the puts all components on a single line, either horizontally or vertically.
 */
public class LinearLayout implements LayoutManager {
    /**
     * This enum type will decide the alignment of a component on the counter-axis, meaning the horizontal alignment on
     * vertical {@code LinearLayout}s and vertical alignment on horizontal {@code LinearLayout}s.
     */
    public enum Alignment {
        /**
         * The component will be placed to the left (for vertical layouts) or top (for horizontal layouts)
         */
        Beginning,
        /**
         * The component will be placed horizontally centered (for vertical layouts) or vertically centered (for
         * horizontal layouts)
         */
        Center,
        /**
         * The component will be placed to the right (for vertical layouts) or bottom (for horizontal layouts)
         */
        End,
        /**
         * The component will be forced to take up all the horizontal space (for vertical layouts) or vertical space
         * (for horizontal layouts)
         */
        Fill,
    }

    /**
     * This enum type will what to do with a component if the container has extra space to offer. This can happen if the
     * window runs in full screen or the window has been programmatically set to a fixed size, above the preferred size
     * of the window.
     */
    public enum GrowPolicy {
        /**
         * This is the default grow policy, the component will not become larger than the preferred size, even if the
         * container can offer more.
         */
        None,
        /**
         * With this grow policy, if the container has more space available then this component will be grown to fill
         * the extra space.
         */
        CanGrow,
    }

    private static class LinearLayoutData implements LayoutData {
        private final Alignment alignment;
        private final GrowPolicy growPolicy;

        public LinearLayoutData(Alignment alignment, GrowPolicy growPolicy) {
            this.alignment = alignment;
            this.growPolicy = growPolicy;
        }
    }

    /**
     * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its
     * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on
     * horizontal {@code LinearLayout}s.
     * @param alignment Alignment to store in the {@code LayoutData} object
     * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment
     * @see Alignment
     */
    public static LayoutData createLayoutData(Alignment alignment) {
        return createLayoutData(alignment, GrowPolicy.None);
    }

    /**
     * Creates a {@code LayoutData} for {@code LinearLayout} that assigns a component to a particular alignment on its
     * counter-axis, meaning the horizontal alignment on vertical {@code LinearLayout}s and vertical alignment on
     * horizontal {@code LinearLayout}s.
     * @param alignment Alignment to store in the {@code LayoutData} object
     * @param growPolicy When policy to apply to the component if the parent container has more space available along
     *                   the main axis.
     * @return {@code LayoutData} object created for {@code LinearLayout}s with the specified alignment
     * @see Alignment
     */
    public static LayoutData createLayoutData(Alignment alignment, GrowPolicy growPolicy) {
        return new LinearLayoutData(alignment, growPolicy);
    }

    private final Direction direction;
    private int spacing;
    private boolean changed;

    /**
     * Default constructor, creates a vertical {@code LinearLayout}
     */
    public LinearLayout() {
        this(Direction.VERTICAL);
    }

    /**
     * Standard constructor that creates a {@code LinearLayout} with a specified direction to position the components on
     * @param direction Direction for this {@code Direction}
     */
    public LinearLayout(Direction direction) {
        this.direction = direction;
        this.spacing = direction == Direction.HORIZONTAL ? 1 : 0;
        this.changed = true;
    }

    /**
     * Sets the amount of empty space to put in between components. For horizontal layouts, this is number of columns
     * (by default 1) and for vertical layouts this is number of rows (by default 0).
     * @param spacing Spacing between components, either in number of columns or rows depending on the direction
     * @return Itself
     */
    public LinearLayout setSpacing(int spacing) {
        this.spacing = spacing;
        this.changed = true;
        return this;
    }

    /**
     * Returns the amount of empty space to put in between components. For horizontal layouts, this is number of columns
     * (by default 1) and for vertical layouts this is number of rows (by default 0).
     * @return Spacing between components, either in number of columns or rows depending on the direction
     */
    public int getSpacing() {
        return spacing;
    }

    @Override
    public TerminalSize getPreferredSize(List<Component> components) {
        // Filter out invisible components
        components = components.stream().filter(Component::isVisible).collect(Collectors.toList());

        if(direction == Direction.VERTICAL) {
            return getPreferredSizeVertically(components);
        }
        else {
            return getPreferredSizeHorizontally(components);
        }
    }

    private TerminalSize getPreferredSizeVertically(List<Component> components) {
        int maxWidth = 0;
        int height = 0;
        for(Component component: components) {
            TerminalSize preferredSize = component.getPreferredSize();
            if(maxWidth < preferredSize.getColumns()) {
                maxWidth = preferredSize.getColumns();
            }
            height += preferredSize.getRows();
        }
        height += spacing * (components.size() - 1);
        return new TerminalSize(maxWidth, Math.max(0, height));
    }

    private TerminalSize getPreferredSizeHorizontally(List<Component> components) {
        int maxHeight = 0;
        int width = 0;
        for(Component component: components) {
            TerminalSize preferredSize = component.getPreferredSize();
            if(maxHeight < preferredSize.getRows()) {
                maxHeight = preferredSize.getRows();
            }
            width += preferredSize.getColumns();
        }
        width += spacing * (components.size() - 1);
        return new TerminalSize(Math.max(0,width), maxHeight);
    }

    @Override
    public boolean hasChanged() {
        return changed;
    }

    @Override
    public void doLayout(TerminalSize area, List<Component> components) {
        // Filter out invisible components
        components = components.stream().filter(Component::isVisible).collect(Collectors.toList());

        if(direction == Direction.VERTICAL) {
            if (Boolean.getBoolean("com.googlecode.lanterna.gui2.LinearLayout.useOldNonFlexLayout")) {
                doVerticalLayout(area, components);
            }
            else {
                doFlexibleVerticalLayout(area, components);
            }
        }
        else {
            if (Boolean.getBoolean("com.googlecode.lanterna.gui2.LinearLayout.useOldNonFlexLayout")) {
                doHorizontalLayout(area, components);
            }
            else {
                doFlexibleHorizontalLayout(area, components);
            }
        }
        this.changed = false;
    }

    @Deprecated
    private void doVerticalLayout(TerminalSize area, List<Component> components) {
        int remainingVerticalSpace = area.getRows();
        int availableHorizontalSpace = area.getColumns();
        for(Component component: components) {
            if(remainingVerticalSpace <= 0) {
                component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
                component.setSize(TerminalSize.ZERO);
            }
            else {
                Alignment alignment = Alignment.Beginning;
                LayoutData layoutData = component.getLayoutData();
                if (layoutData instanceof LinearLayoutData) {
                    alignment = ((LinearLayoutData)layoutData).alignment;
                }

                TerminalSize preferredSize = component.getPreferredSize();
                TerminalSize decidedSize = new TerminalSize(
                        Math.min(availableHorizontalSpace, preferredSize.getColumns()),
                        Math.min(remainingVerticalSpace, preferredSize.getRows()));
                if(alignment == Alignment.Fill) {
                    decidedSize = decidedSize.withColumns(availableHorizontalSpace);
                    alignment = Alignment.Beginning;
                }

                TerminalPosition position = component.getPosition();
                position = position.withRow(area.getRows() - remainingVerticalSpace);
                switch(alignment) {
                    case End:
                        position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns());
                        break;
                    case Center:
                        position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2);
                        break;
                    case Beginning:
                    default:
                        position = position.withColumn(0);
                        break;
                }
                component.setPosition(position);
                component.setSize(component.getSize().with(decidedSize));
                remainingVerticalSpace -= decidedSize.getRows() + spacing;
            }
        }
    }

    private void doFlexibleVerticalLayout(TerminalSize area, List<Component> components) {
        int availableVerticalSpace = area.getRows();
        int availableHorizontalSpace = area.getColumns();
        List<Component> copyOfComponenets = new ArrayList<>(components);
        final Map<Component, TerminalSize> fittingMap = new IdentityHashMap<>();
        int totalRequiredVerticalSpace = 0;

        for (Component component: components) {
            Alignment alignment = Alignment.Beginning;
            LayoutData layoutData = component.getLayoutData();
            if (layoutData instanceof LinearLayoutData) {
                alignment = ((LinearLayoutData)layoutData).alignment;
            }

            TerminalSize preferredSize = component.getPreferredSize();
            TerminalSize fittingSize = new TerminalSize(
                    Math.min(availableHorizontalSpace, preferredSize.getColumns()),
                    preferredSize.getRows());
            if(alignment == Alignment.Fill) {
                fittingSize = fittingSize.withColumns(availableHorizontalSpace);
            }

            fittingMap.put(component, fittingSize);
            totalRequiredVerticalSpace += fittingSize.getRows() + spacing;
        }
        if (!components.isEmpty()) {
            // Remove the last spacing
            totalRequiredVerticalSpace -= spacing;
        }

        // If we can't fit everything, trim the down the size of the largest components until it fits
        if (availableVerticalSpace < totalRequiredVerticalSpace) {
            copyOfComponenets.sort((o1, o2) -> {
                // Reverse sort
                return -Integer.compare(fittingMap.get(o1).getRows(), fittingMap.get(o2).getRows());
            });

            while (availableVerticalSpace < totalRequiredVerticalSpace) {
                int largestSize = fittingMap.get(copyOfComponenets.get(0)).getRows();
                for (Component largeComponent: copyOfComponenets) {
                    TerminalSize currentSize = fittingMap.get(largeComponent);
                    if (largestSize > currentSize.getRows()) {
                        break;
                    }
                    fittingMap.put(largeComponent, currentSize.withRelativeRows(-1));
                    totalRequiredVerticalSpace--;
                }
            }
        }

        // If we have more space available than we need, grow components to fill
        if (availableVerticalSpace > totalRequiredVerticalSpace) {
            boolean resizedOneComponent = false;
            while (availableVerticalSpace > totalRequiredVerticalSpace) {
                for(Component component: components) {
                    final LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData();
                    final TerminalSize currentSize = fittingMap.get(component);
                    if (layoutData != null && layoutData.growPolicy == GrowPolicy.CanGrow) {
                        fittingMap.put(component, currentSize.withRelativeRows(1));
                        availableVerticalSpace--;
                        resizedOneComponent = true;
                    }
                    if (availableVerticalSpace <= totalRequiredVerticalSpace) {
                        break;
                    }
                }
                if (!resizedOneComponent) {
                    break;
                }
            }
        }

        // Assign the sizes and positions
        int topPosition = 0;
        for(Component component: components) {
            Alignment alignment = Alignment.Beginning;
            LayoutData layoutData = component.getLayoutData();
            if (layoutData instanceof LinearLayoutData) {
                alignment = ((LinearLayoutData)layoutData).alignment;
            }

            TerminalSize decidedSize = fittingMap.get(component);
            TerminalPosition position = component.getPosition();
            position = position.withRow(topPosition);
            switch(alignment) {
                case End:
                    position = position.withColumn(availableHorizontalSpace - decidedSize.getColumns());
                    break;
                case Center:
                    position = position.withColumn((availableHorizontalSpace - decidedSize.getColumns()) / 2);
                    break;
                case Beginning:
                default:
                    position = position.withColumn(0);
                    break;
            }
            component.setPosition(component.getPosition().with(position));
            component.setSize(component.getSize().with(decidedSize));
            topPosition += decidedSize.getRows() + spacing;
        }
    }

    @Deprecated
    private void doHorizontalLayout(TerminalSize area, List<Component> components) {
        int remainingHorizontalSpace = area.getColumns();
        int availableVerticalSpace = area.getRows();
        for(Component component: components) {
            if(remainingHorizontalSpace <= 0) {
                component.setPosition(TerminalPosition.TOP_LEFT_CORNER);
                component.setSize(TerminalSize.ZERO);
            }
            else {
                Alignment alignment = Alignment.Beginning;
                LayoutData layoutData = component.getLayoutData();
                if (layoutData instanceof LinearLayoutData) {
                    alignment = ((LinearLayoutData)layoutData).alignment;
                }

                TerminalSize preferredSize = component.getPreferredSize();
                TerminalSize decidedSize = new TerminalSize(
                        Math.min(remainingHorizontalSpace, preferredSize.getColumns()),
                        Math.min(availableVerticalSpace, preferredSize.getRows()));
                if(alignment == Alignment.Fill) {
                    decidedSize = decidedSize.withRows(availableVerticalSpace);
                    alignment = Alignment.Beginning;
                }

                TerminalPosition position = component.getPosition();
                position = position.withColumn(area.getColumns() - remainingHorizontalSpace);
                switch(alignment) {
                    case End:
                        position = position.withRow(availableVerticalSpace - decidedSize.getRows());
                        break;
                    case Center:
                        position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2);
                        break;
                    case Beginning:
                    default:
                        position = position.withRow(0);
                        break;
                }
                component.setPosition(position);
                component.setSize(component.getSize().with(decidedSize));
                remainingHorizontalSpace -= decidedSize.getColumns() + spacing;
            }
        }
    }

    private void doFlexibleHorizontalLayout(TerminalSize area, List<Component> components) {
        int availableVerticalSpace = area.getRows();
        int availableHorizontalSpace = area.getColumns();
        List<Component> copyOfComponenets = new ArrayList<>(components);
        final Map<Component, TerminalSize> fittingMap = new IdentityHashMap<>();
        int totalRequiredHorizontalSpace = 0;

        for (Component component: components) {
            Alignment alignment = Alignment.Beginning;
            LayoutData layoutData = component.getLayoutData();
            if (layoutData instanceof LinearLayoutData) {
                alignment = ((LinearLayoutData)layoutData).alignment;
            }

            TerminalSize preferredSize = component.getPreferredSize();
            TerminalSize fittingSize = new TerminalSize(
                    preferredSize.getColumns(),
                    Math.min(availableVerticalSpace, preferredSize.getRows()));
            if(alignment == Alignment.Fill) {
                fittingSize = fittingSize.withRows(availableVerticalSpace);
            }

            fittingMap.put(component, fittingSize);
            totalRequiredHorizontalSpace += fittingSize.getColumns() + spacing;
        }
        if (!components.isEmpty()) {
            // Remove the last spacing
            totalRequiredHorizontalSpace -= spacing;
        }

        // If we can't fit everything, trim the down the size of the largest components until it fits
        if (availableHorizontalSpace < totalRequiredHorizontalSpace) {
            copyOfComponenets.sort((o1, o2) -> {
                // Reverse sort
                return -Integer.compare(fittingMap.get(o1).getColumns(), fittingMap.get(o2).getColumns());
            });

            while (availableHorizontalSpace < totalRequiredHorizontalSpace) {
                int largestSize = fittingMap.get(copyOfComponenets.get(0)).getColumns();
                for (Component largeComponent: copyOfComponenets) {
                    TerminalSize currentSize = fittingMap.get(largeComponent);
                    if (largestSize > currentSize.getColumns()) {
                        break;
                    }
                    fittingMap.put(largeComponent, currentSize.withRelativeColumns(-1));
                    totalRequiredHorizontalSpace--;
                }
            }
        }

        // If we have more space available than we need, grow components to fill
        if (availableHorizontalSpace > totalRequiredHorizontalSpace) {
            boolean resizedOneComponent = false;
            while (availableHorizontalSpace > totalRequiredHorizontalSpace) {
                for(Component component: components) {
                    final LinearLayoutData layoutData = (LinearLayoutData)component.getLayoutData();
                    final TerminalSize currentSize = fittingMap.get(component);
                    if (layoutData != null && layoutData.growPolicy == GrowPolicy.CanGrow) {
                        fittingMap.put(component, currentSize.withRelativeColumns(1));
                        availableHorizontalSpace--;
                        resizedOneComponent = true;
                    }
                    if (availableHorizontalSpace <= totalRequiredHorizontalSpace) {
                        break;
                    }
                }
                if (!resizedOneComponent) {
                    break;
                }
            }
        }

        // Assign the sizes and positions
        int leftPosition = 0;
        for(Component component: components) {
            Alignment alignment = Alignment.Beginning;
            LayoutData layoutData = component.getLayoutData();
            if (layoutData instanceof LinearLayoutData) {
                alignment = ((LinearLayoutData)layoutData).alignment;
            }

            TerminalSize decidedSize = fittingMap.get(component);
            TerminalPosition position = component.getPosition();
            position = position.withColumn(leftPosition);
            switch(alignment) {
                case End:
                    position = position.withRow(availableVerticalSpace - decidedSize.getRows());
                    break;
                case Center:
                    position = position.withRow((availableVerticalSpace - decidedSize.getRows()) / 2);
                    break;
                case Beginning:
                default:
                    position = position.withRow(0);
                    break;
            }
            component.setPosition(component.getPosition().with(position));
            component.setSize(component.getSize().with(decidedSize));
            leftPosition += decidedSize.getColumns() + spacing;
        }
    }
}