package terminal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static terminal.LispTerminal.END_OF_SEGMENT;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.input.KeyStroke;
import com.googlecode.lanterna.input.KeyType;
import com.googlecode.lanterna.terminal.virtual.DefaultVirtualTerminal;
import com.googlecode.lanterna.terminal.virtual.VirtualTerminal;

import stream.UncheckedIOException;

public class VirtualTerminalInteractor {

    private PipedOutputStream inputWriter;
    private PipedInputStream inputReader;
    private PipedOutputStream outputWriter;
    private PipedInputStream outputReader;
    private VirtualTerminal virtualTerminal;
    private FlushListener flushListener;
    private LispTerminal lispTerminal;
    private TerminalConfiguration configuration;

    public VirtualTerminalInteractor() {
        this.inputWriter = new PipedOutputStream();
        this.inputReader = new PipedInputStream();
        this.outputWriter = new PipedOutputStream();
        this.outputReader = new PipedInputStream();
        this.virtualTerminal = new DefaultVirtualTerminal();
        this.flushListener = new FlushListener();
        this.virtualTerminal.addVirtualTerminalListener(flushListener);
        this.lispTerminal = new LispTerminal(createTerminalConfiguration());
    }

    private TerminalConfiguration createTerminalConfiguration() {
        configuration = new TerminalConfiguration();
        configuration.setInputPair(inputWriter, inputReader);
        configuration.setOutputPair(outputWriter, outputReader);
        configuration.setTerminal(virtualTerminal);

        return configuration;
    }

    public TerminalConfiguration getConfiguration() {
        return configuration;
    }

    public void start() {
        lispTerminal.start();
    }

    public void stop() {
        try {
            lispTerminal.stop();
            outputWriter.close();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void pressKey(KeyType keyType) {
        virtualTerminal.addInput(new KeyStroke(keyType));
        waitForFlushes(1);
    }

    public void pressKeyTimes(KeyType keyType, int times) {
        for (int i = times; i > 0; i--)
            virtualTerminal.addInput(new KeyStroke(keyType));

        waitForFlushes(times);
    }

    public void pressControlKey(KeyType keyType) {
        virtualTerminal.addInput(new KeyStroke(keyType, true, false));
        waitForFlushes(1);
    }

    public void enterCharacter(char character) {
        virtualTerminal.addInput(new KeyStroke(character, false, false));
        waitForFlushes(1);
    }

    public void enterControlCharacter(char character) {
        virtualTerminal.addInput(new KeyStroke(character, true, false));
        waitForFlushes(1);
    }

    public void enterCharacters(String characters) {
        for (char c : characters.toCharArray())
            virtualTerminal.addInput(new KeyStroke(c, false, false));

        waitForFlushes(characters.length());
    }

    public void produceOutput(String output) {
        try {
            for (char c : output.toCharArray())
                outputWriter.write(c);

            outputWriter.write(END_OF_SEGMENT);
            outputWriter.flush();
            waitForFlushes(1);
        } catch (IOException ignored) {}
    }

    public void waitForFlushes(int flushCount) {
        try {
            synchronized (flushListener) {
                while (flushListener.getFlushCount() < flushCount)
                    flushListener.wait();

                flushListener.reduceFlushCount(flushCount);
            }
        } catch (InterruptedException ignored) {}
    }

    public void waitForPrompt() {
        waitForFlushes(1);
    }

    public void setColumns(int columns) {
        int rows = virtualTerminal.getTerminalSize().getRows();
        virtualTerminal.setTerminalSize(new TerminalSize(columns, rows));
    }

    public void setRows(int rows) {
        int columns = virtualTerminal.getTerminalSize().getColumns();
        virtualTerminal.setTerminalSize(new TerminalSize(columns, rows));
    }

    public void assertCursorPosition(int column, int row) {
        assertEquals(column, virtualTerminal.getCursorPosition().getColumn());
        assertEquals(row, virtualTerminal.getCursorPosition().getRow());
    }

    public void assertScreenText(String... rows) {
        for (int row = 0; row < rows.length; row++)
            for (int column = 0; column < rows[row].length(); column++)
                assertCharacterAtPosition(rows[row].charAt(column), column, row);
    }

    public void assertCharacterAtPosition(char character, int column, int row) {
        TerminalPosition position = new TerminalPosition(column, row);
        String expected = String.valueOf(character);
        String actual = String.valueOf(virtualTerminal.getCharacter(position).getCharacter());
        assertEquals(expected, actual);
    }

    public void assertInputWritten(String expected) {
        String actual = "";

        try {
            inputWriter.close();

            for (int c = inputReader.read(); c != -1; c = inputReader.read())
                actual += (char) c;
        } catch (IOException ignored) {}

        assertEquals(expected, actual);
    }

    public void assertInputStreamClosed() {
        try {
            inputWriter.write(0);
            fail("input stream not closed");
        } catch (IOException ignored) {}
    }

}
