/*
 * 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.bundle.LanternaThemes;
import com.googlecode.lanterna.graphics.Theme;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.screen.Screen;

import java.io.EOFException;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * This abstract implementation of TextGUI contains some basic management of the underlying Screen and other common code
 * that can be shared between different implementations.
 * @author Martin
 */
public abstract class AbstractTextGUI implements TextGUI {

    private final Screen screen;
    private final List<Listener> listeners;
    private boolean blockingIO;
    private boolean dirty;
    private TextGUIThread textGUIThread;
    private Theme guiTheme;

    /**
     * Constructor for {@code AbstractTextGUI} that requires a {@code Screen} and a factory for creating the GUI thread
     * @param textGUIThreadFactory Factory class to use for creating the {@code TextGUIThread} class
     * @param screen What underlying {@code Screen} to use for this text GUI
     */
    protected AbstractTextGUI(TextGUIThreadFactory textGUIThreadFactory, Screen screen) {
        if(screen == null) {
            throw new IllegalArgumentException("Creating a TextGUI requires an underlying Screen");
        }
        this.screen = screen;
        this.listeners = new CopyOnWriteArrayList<>();
        this.blockingIO = false;
        this.dirty = false;
        this.guiTheme = LanternaThemes.getDefaultTheme();
        this.textGUIThread = textGUIThreadFactory.createTextGUIThread(this);
    }

    /**
     * Reads one key from the input queue, blocking or non-blocking depending on if blocking I/O has been enabled. To
     * enable blocking I/O (disabled by default), use {@code setBlockingIO(true)}.
     * @return One piece of user input as a {@code KeyStroke} or {@code null} if blocking I/O is disabled and there was
     *         no input waiting
     * @throws IOException In case of an I/O error while reading input
     */
    protected KeyStroke readKeyStroke() throws IOException {
        return blockingIO ? screen.readInput() : pollInput();
    }

    /**
     * Polls the underlying input queue for user input, returning either a {@code KeyStroke} or {@code null}
     * @return {@code KeyStroke} representing the user input or {@code null} if there was none
     * @throws IOException In case of an I/O error while reading input
     */
    protected KeyStroke pollInput() throws IOException {
        return screen.pollInput();
    }

    @Override
    public synchronized boolean processInput() throws IOException {
        boolean gotInput = false;
        KeyStroke keyStroke = readKeyStroke();
        if(keyStroke != null) {
            gotInput = true;
            do {
                if (keyStroke.getKeyType() == KeyType.EOF) {
                    throw new EOFException();
                }
                boolean handled = handleInput(keyStroke);
                if(!handled) {
                    handled = fireUnhandledKeyStroke(keyStroke);
                }
                dirty = handled || dirty;
                keyStroke = pollInput();
            } while(keyStroke != null);
        }
        return gotInput;
    }

    @Override
    public void setTheme(Theme theme) {
        if(theme != null) {
            this.guiTheme = theme;
        }
    }

    @Override
    public Theme getTheme() {
        return guiTheme;
    }

    @Override
    public synchronized void updateScreen() throws IOException {
        screen.doResizeIfNecessary();
        drawGUI(new DefaultTextGUIGraphics(this, screen.newTextGraphics()));
        screen.setCursorPosition(getCursorPosition());
        screen.refresh();
        dirty = false;
    }

    @Override
    public Screen getScreen() {
        return screen;
    }

    @Override
    public boolean isPendingUpdate() {
        return screen.doResizeIfNecessary() != null || dirty;
    }

    @Override
    public TextGUIThread getGUIThread() {
        return textGUIThread;
    }

    @Override
    public void addListener(Listener listener) {
        listeners.add(listener);
    }

    @Override
    public void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    /**
     * Enables blocking I/O, causing calls to {@code readKeyStroke()} to block until there is input available. Notice
     * that you can still poll for input using {@code pollInput()}.
     * @param blockingIO Set this to {@code true} if blocking I/O should be enabled, otherwise {@code false}
     */
    public void setBlockingIO(boolean blockingIO) {
        this.blockingIO = blockingIO;
    }

    /**
     * Checks if blocking I/O is enabled or not
     * @return {@code true} if blocking I/O is enabled, otherwise {@code false}
     */
    public boolean isBlockingIO() {
        return blockingIO;
    }

    /**
     * This method should be called when there was user input that wasn't handled by the GUI. It will fire the
     * {@code onUnhandledKeyStroke(..)} method on any registered listener.
     * @param keyStroke The {@code KeyStroke} that wasn't handled by the GUI
     * @return {@code true} if at least one of the listeners handled the key stroke, this will signal to the GUI that it
     * needs to be redrawn again.
     */
    protected final boolean fireUnhandledKeyStroke(KeyStroke keyStroke) {
        boolean handled = false;
        for(Listener listener: listeners) {
            handled = listener.onUnhandledKeyStroke(this, keyStroke) || handled;
        }
        return handled;
    }

    /**
     * Marks the whole text GUI as invalid and that it needs to be redrawn at next opportunity
     */
    protected void invalidate() {
        dirty = true;
    }

    /**
     * Draws the entire GUI using a {@code TextGUIGraphics} object
     * @param graphics Graphics object to draw using
     */
    protected abstract void drawGUI(TextGUIGraphics graphics);

    /**
     * Top-level method for drilling in to the GUI and figuring out, in global coordinates, where to place the text
     * cursor on the screen at this time.
     * @return Where to place the text cursor, or {@code null} if the cursor should be hidden
     */
    protected abstract TerminalPosition getCursorPosition();

    /**
     * This method should take the user input and feed it to the focused component for handling.
     * @param key {@code KeyStroke} representing the user input
     * @return {@code true} if the input was recognized and handled by the GUI, indicating that the GUI should be redrawn
     */
    protected abstract boolean handleInput(KeyStroke key);
}