/*
 * 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.gui2.menu.MenuBar;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.input.KeyType;

import java.util.*;

/**
 * Abstract Window has most of the code requiring for a window to function, all concrete window implementations extends
 * from this in one way or another. You can define your own window by extending from this, as an alternative to building
 * up the GUI externally by constructing a {@code BasicWindow} and adding components to it.
 * @author Martin
 */
public abstract class AbstractWindow extends AbstractBasePane<Window> implements Window {
    private String title;
    private WindowBasedTextGUI textGUI;
    private boolean visible;
    private TerminalSize lastKnownSize;
    private TerminalSize lastKnownDecoratedSize;
    private TerminalPosition lastKnownPosition;
    private TerminalPosition contentOffset;
    private Set<Hint> hints;
    private WindowPostRenderer windowPostRenderer;
    private boolean closeWindowWithEscape;

    /**
     * Default constructor, this creates a window with no title
     */
    public AbstractWindow() {
        this("");
    }

    /**
     * Creates a window with a specific title that will (probably) be drawn in the window decorations
     * @param title Title of this window
     */
    public AbstractWindow(String title) {
        super();
        this.title = title;
        this.textGUI = null;
        this.visible = true;
        this.contentOffset = TerminalPosition.TOP_LEFT_CORNER;
        this.lastKnownPosition = null;
        this.lastKnownSize = null;
        this.lastKnownDecoratedSize = null;
        this.closeWindowWithEscape = false;

        this.hints = new HashSet<>();
    }

    /**
     * Setting this property to {@code true} will cause pressing the ESC key to close the window. This used to be the
     * default behaviour of lanterna 3 during the development cycle but is not longer the case. You are encouraged to
     * put proper buttons or other kind of components to clearly mark to the user how to close the window instead of
     * magically taking ESC, but sometimes it can be useful (when doing testing, for example) to enable this mode.
     * @param closeWindowWithEscape If {@code true}, this window will self-close if you press ESC key
     */
    public void setCloseWindowWithEscape(boolean closeWindowWithEscape) {
        this.closeWindowWithEscape = closeWindowWithEscape;
    }

    @Override
    public void setTextGUI(WindowBasedTextGUI textGUI) {
        //This is kind of stupid check, but might cause it to blow up on people using the library incorrectly instead of
        //just causing weird behaviour
        if(this.textGUI != null && textGUI != null) {
            throw new UnsupportedOperationException("Are you calling setTextGUI yourself? Please read the documentation"
                    + " in that case (this could also be a bug in Lanterna, please report it if you are sure you are "
                    + "not calling Window.setTextGUI(..) from your code)");
        }
        this.textGUI = textGUI;
    }

    @Override
    public WindowBasedTextGUI getTextGUI() {
        return textGUI;
    }

    /**
     * Alters the title of the window to the supplied string
     * @param title New title of the window
     */
    public void setTitle(String title) {
        this.title = title;
        invalidate();
    }

    @Override
    public String getTitle() {
        return title;
    }

    @Override
    public boolean isVisible() {
        return visible;
    }

    @Override
    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    @Override
    public void draw(TextGUIGraphics graphics) {
        if(!graphics.getSize().equals(lastKnownSize)) {
            getComponent().invalidate();
        }
        setSize(graphics.getSize(), false);
        super.draw(graphics);
    }

    @Override
    public boolean handleInput(KeyStroke key) {
        boolean handled = super.handleInput(key);
        if(!handled && closeWindowWithEscape && key.getKeyType() == KeyType.Escape) {
            close();
            return true;
        }
        return handled;
    }

    @Override
    public TerminalPosition toGlobal(TerminalPosition localPosition) {
        if(localPosition == null) {
            return null;
        }
        return lastKnownPosition.withRelative(contentOffset.withRelative(localPosition));
    }

    @Override
    public TerminalPosition fromGlobal(TerminalPosition globalPosition) {
        if(globalPosition == null || lastKnownPosition == null) {
            return null;
        }
        return globalPosition.withRelative(
                -lastKnownPosition.getColumn() - contentOffset.getColumn(),
                -lastKnownPosition.getRow() - contentOffset.getRow());
    }

    @Override
    public TerminalSize getPreferredSize() {
        TerminalSize preferredSize = contentHolder.getPreferredSize();
        MenuBar menuBar = getMenuBar();
        if (menuBar.getMenuCount() > 0) {
            TerminalSize menuPreferredSize = menuBar.getPreferredSize();
            preferredSize = preferredSize.withRelativeRows(menuPreferredSize.getRows())
                    .withColumns(Math.max(menuPreferredSize.getColumns(), preferredSize.getColumns()));
        }
        return preferredSize;
    }

    @Override
    public void setHints(Collection<Hint> hints) {
        this.hints = new HashSet<>(hints);
        invalidate();
    }

    @Override
    public Set<Hint> getHints() {
        return Collections.unmodifiableSet(hints);
    }

    @Override
    public WindowPostRenderer getPostRenderer() {
        return  windowPostRenderer;
    }

    @Override
    public void addWindowListener(WindowListener windowListener) {
        addBasePaneListener(windowListener);
    }

    @Override
    public void removeWindowListener(WindowListener windowListener) {
        removeBasePaneListener(windowListener);
    }

    /**
     * Sets the post-renderer to use for this window. This will override the default from the GUI system (if there is
     * one set, otherwise from the theme).
     * @param windowPostRenderer Window post-renderer to assign to this window
     */
    public void setWindowPostRenderer(WindowPostRenderer windowPostRenderer) {
        this.windowPostRenderer = windowPostRenderer;
    }

    @Override
    public final TerminalPosition getPosition() {
        return lastKnownPosition;
    }

    @Override
    public final void setPosition(TerminalPosition topLeft) {
        TerminalPosition oldPosition = this.lastKnownPosition;
        this.lastKnownPosition = topLeft;

        // Fire listeners
        for(BasePaneListener<?> listener: getBasePaneListeners()) {
            if(listener instanceof WindowListener) {
                ((WindowListener)listener).onMoved(this, oldPosition, topLeft);
            }
        }
    }

    @Override
    public final TerminalSize getSize() {
        return lastKnownSize;
    }

    @Override
    @Deprecated
    public void setSize(TerminalSize size) {
        setSize(size, true);
    }

    @Override
    public void setFixedSize(TerminalSize size) {
        hints.add(Hint.FIXED_SIZE);
        setSize(size);
    }

    private void setSize(TerminalSize size, boolean invalidate) {
        TerminalSize oldSize = this.lastKnownSize;
        this.lastKnownSize = size;
        if(invalidate) {
            invalidate();
        }

        // Fire listeners
        for(BasePaneListener<?> listener: getBasePaneListeners()) {
            if(listener instanceof WindowListener) {
                ((WindowListener)listener).onResized(this, oldSize, size);
            }
        }
    }

    @Override
    public final TerminalSize getDecoratedSize() {
        return lastKnownDecoratedSize;
    }

    @Override
    public final void setDecoratedSize(TerminalSize decoratedSize) {
        this.lastKnownDecoratedSize = decoratedSize;
    }

    @Override
    public void setContentOffset(TerminalPosition offset) {
        this.contentOffset = offset;
    }

    @Override
    public void close() {
        if(textGUI != null) {
            textGUI.removeWindow(this);
        }
        setComponent(null);
    }

    @Override
    public void waitUntilClosed() {
        WindowBasedTextGUI textGUI = getTextGUI();
        if(textGUI != null) {
            textGUI.waitForWindowToClose(this);
        }
    }

    Window self() { return this; }
}