/*
 * 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 java.io.EOFException;
import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * Abstract implementation of {@link TextGUIThread} with common logic for both available concrete implementations.
 */
public abstract class AbstractTextGUIThread implements TextGUIThread {

    protected final TextGUI textGUI;
    protected final Queue<Runnable> customTasks;
    protected ExceptionHandler exceptionHandler;

    /**
     * Sets up this {@link AbstractTextGUIThread} for operations on the supplies {@link TextGUI}
     * @param textGUI Text GUI this {@link TextGUIThread} implementations will be operating on
     */
    public AbstractTextGUIThread(TextGUI textGUI) {
        this.exceptionHandler = new ExceptionHandler() {
            @Override
            public boolean onIOException(IOException e) {
                e.printStackTrace();
                return true;
            }

            @Override
            public boolean onRuntimeException(RuntimeException e) {
                e.printStackTrace();
                return true;
            }
        };
        this.textGUI = textGUI;
        this.customTasks = new LinkedBlockingQueue<>();
    }

    @Override
    public void invokeLater(Runnable runnable) throws IllegalStateException {
        customTasks.add(runnable);
    }

    @Override
    public void setExceptionHandler(ExceptionHandler exceptionHandler) {
        if(exceptionHandler == null) {
            throw new IllegalArgumentException("Cannot call setExceptionHandler(null)");
        }
        this.exceptionHandler = exceptionHandler;
    }

    @Override
    public synchronized boolean processEventsAndUpdate() throws IOException {
        if(getThread() != Thread.currentThread()) {
            throw new IllegalStateException("Calling processEventAndUpdate outside of GUI thread");
        }
        try {
            textGUI.processInput();
            while (!customTasks.isEmpty()) {
                Runnable r = customTasks.poll();
                if (r != null) {
                    r.run();
                }
            }
            if (textGUI.isPendingUpdate()) {
                textGUI.updateScreen();
                return true;
            }
            return false;
        }
        catch (EOFException e) {
            // Always re-throw EOFExceptions so the UI system knows we've closed the terminal
            throw e;
        }
        catch (IOException e) {
            if (exceptionHandler != null) {
                exceptionHandler.onIOException(e);
            }
            else {
                throw e;
            }
        }
        catch (RuntimeException e) {
            if (exceptionHandler != null) {
                exceptionHandler.onRuntimeException(e);
            }
            else {
                throw e;
            }
        }
        return true;
    }

    @Override
    public void invokeAndWait(final Runnable runnable) throws IllegalStateException, InterruptedException {
        Thread guiThread = getThread();
        if(guiThread == null || Thread.currentThread() == guiThread) {
            runnable.run();
        }
        else {
            final CountDownLatch countDownLatch = new CountDownLatch(1);
            invokeLater(() -> {
                try {
                    runnable.run();
                }
                finally {
                    countDownLatch.countDown();
                }
            });
            countDownLatch.await();
        }
    }
}
