/*
 * 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.terminal;

import com.googlecode.lanterna.SGR;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.graphics.AbstractTextGraphics;
import com.googlecode.lanterna.TextCharacter;
import com.googlecode.lanterna.graphics.TextGraphics;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * This is the terminal's implementation of TextGraphics. Upon creation it takes a snapshot for the terminal's size, so
 * that it won't require to do an expensive lookup on every call to {@code getSize()}, but this also means that it can
 * go stale quickly if the terminal is resized. You should try to use the object quickly and then let it be GC:ed. It
 * will not pick up on terminal resize! Also, the state of the Terminal after an operation performed by this
 * TextGraphics implementation is undefined and you should probably re-initialize colors and modifiers.
 * <p/>
 * Any write operation that results in an IOException will be wrapped by a RuntimeException since the TextGraphics
 * interface doesn't allow throwing IOException
 */
class TerminalTextGraphics extends AbstractTextGraphics {

    private final Terminal terminal;
    private final TerminalSize terminalSize;

    private final Map<TerminalPosition, TextCharacter> writeHistory;

    private AtomicInteger manageCallStackSize;
    private TextCharacter lastCharacter;
    private TerminalPosition lastPosition;

    TerminalTextGraphics(Terminal terminal) throws IOException {
        this.terminal = terminal;
        this.terminalSize = terminal.getTerminalSize();
        this.manageCallStackSize = new AtomicInteger(0);
        this.writeHistory = new HashMap<>();
        this.lastCharacter = null;
        this.lastPosition = null;
    }

    @Override
    public TextGraphics setCharacter(int columnIndex, int rowIndex, TextCharacter textCharacter) {
        return setCharacter(new TerminalPosition(columnIndex, rowIndex), textCharacter);
    }

    @Override
    public synchronized TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) {
        try {
            if(manageCallStackSize.get() > 0) {
                if(lastCharacter == null || !lastCharacter.equals(textCharacter)) {
                    applyGraphicState(textCharacter);
                    lastCharacter = textCharacter;
                }
                if(lastPosition == null || !lastPosition.equals(position)) {
                    terminal.setCursorPosition(position.getColumn(), position.getRow());
                    lastPosition = position;
                }
            }
            else {
                terminal.setCursorPosition(position.getColumn(), position.getRow());
                applyGraphicState(textCharacter);
            }
            terminal.putCharacter(textCharacter.getCharacter());
            if(manageCallStackSize.get() > 0) {
                lastPosition = position.withRelativeColumn(1);
            }
            writeHistory.put(position, textCharacter);
        }
        catch(IOException e) {
            throw new RuntimeException(e);
        }
        return this;
    }

    @Override
    public TextCharacter getCharacter(int column, int row) {
        return getCharacter(new TerminalPosition(column, row));
    }

    @Override
    public synchronized TextCharacter getCharacter(TerminalPosition position) {
        return writeHistory.get(position);
    }

    private void applyGraphicState(TextCharacter textCharacter) throws IOException {
        terminal.resetColorAndSGR();
        terminal.setForegroundColor(textCharacter.getForegroundColor());
        terminal.setBackgroundColor(textCharacter.getBackgroundColor());
        for(SGR sgr: textCharacter.getModifiers()) {
            terminal.enableSGR(sgr);
        }
    }

    @Override
    public TerminalSize getSize() {
        return terminalSize;
    }

    @Override
    public synchronized TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, char character) {
        try {
            enterAtomic();
            super.drawLine(fromPoint, toPoint, character);
            return this;
        }
        finally {
            leaveAtomic();
        }
    }

    @Override
    public synchronized TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
        try {
            enterAtomic();
            super.drawTriangle(p1, p2, p3, character);
            return this;
        }
        finally {
            leaveAtomic();
        }
    }

    @Override
    public synchronized TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
        try {
            enterAtomic();
            super.fillTriangle(p1, p2, p3, character);
            return this;
        }
        finally {
            leaveAtomic();
        }
    }

    @Override
    public synchronized TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
        try {
            enterAtomic();
            super.fillRectangle(topLeft, size, character);
            return this;
        }
        finally {
            leaveAtomic();
        }
    }

    @Override
    public synchronized TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
        try {
            enterAtomic();
            super.drawRectangle(topLeft, size, character);
            return this;
        }
        finally {
            leaveAtomic();
        }
    }

    @Override
    public synchronized TextGraphics putString(int column, int row, String string) {
        try {
            enterAtomic();
            return super.putString(column, row, string);
        }
        finally {
            leaveAtomic();
        }
    }

    /**
     * It's tricky with this implementation because we can't rely on any state in between two calls to setCharacter
     * since the caller might modify the terminal's state outside of this writer. However, many calls inside
     * TextGraphics will indeed make multiple calls in setCharacter where we know that the state won't change (actually,
     * we can't be 100% sure since the caller might create a separate thread and maliciously write directly to the
     * terminal while call one of the draw/fill/put methods in here). We could just set the state before writing every
     * single character but that would be inefficient. Rather, we keep a counter of if we are inside an 'atomic'
     * (meaning we know multiple calls to setCharacter will have the same state). Some drawing methods call other
     * drawing methods internally for their implementation so that's why this is implemented with an integer value
     * instead of a boolean; when the counter reaches zero we remove the memory of what state the terminal is in.
     */
    private void enterAtomic() {
        manageCallStackSize.incrementAndGet();
    }

    private void leaveAtomic() {
        if(manageCallStackSize.decrementAndGet() == 0) {
            lastPosition = null;
            lastCharacter = null;
        }
    }
}
