/*
 * This file is part of lanterna (http://code.google.com/p/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-2014 Martin
 */
package com.googlecode.lanterna.graphics;

import com.googlecode.lanterna.*;
import com.googlecode.lanterna.screen.TabBehaviour;

import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;

/**
 * This class hold the default logic for drawing the basic text graphic as exposed by TextGraphic. All implementations
 * reply on a setCharacter method being implemented in subclasses.
 * @author Martin
 */
public abstract class AbstractTextGraphics implements TextGraphics {
    protected TextColor foregroundColor;
    protected TextColor backgroundColor;
    protected TabBehaviour tabBehaviour;
    protected final EnumSet<SGR> activeModifiers;
    private final ShapeRenderer shapeRenderer;

    protected AbstractTextGraphics() {
        this.activeModifiers = EnumSet.noneOf(SGR.class);
        this.tabBehaviour = TabBehaviour.ALIGN_TO_COLUMN_4;
        this.foregroundColor = TextColor.ANSI.DEFAULT;
        this.backgroundColor = TextColor.ANSI.DEFAULT;
        this.shapeRenderer = new DefaultShapeRenderer(new DefaultShapeRenderer.Callback() {
            @Override
            public void onPoint(int column, int row, TextCharacter character) {
                setCharacter(column, row, character);
            }
        });
    }

    @Override
    public TextColor getBackgroundColor() {
        return backgroundColor;
    }

    @Override
    public TextGraphics setBackgroundColor(final TextColor backgroundColor) {
        this.backgroundColor = backgroundColor;
        return this;
    }

    @Override
    public TextColor getForegroundColor() {
        return foregroundColor;
    }

    @Override
    public TextGraphics setForegroundColor(final TextColor foregroundColor) {
        this.foregroundColor = foregroundColor;
        return this;
    }

    @Override
    public TextGraphics enableModifiers(SGR... modifiers) {
        enableModifiers(Arrays.asList(modifiers));
        return this;
    }

    private void enableModifiers(Collection<SGR> modifiers) {
        this.activeModifiers.addAll(modifiers);
    }

    @Override
    public TextGraphics disableModifiers(SGR... modifiers) {
        disableModifiers(Arrays.asList(modifiers));
        return this;
    }

    private void disableModifiers(Collection<SGR> modifiers) {
        this.activeModifiers.removeAll(modifiers);
    }

    @Override
    public synchronized TextGraphics setModifiers(EnumSet<SGR> modifiers) {
        activeModifiers.clear();
        activeModifiers.addAll(modifiers);
        return this;
    }

    @Override
    public TextGraphics clearModifiers() {
        this.activeModifiers.clear();
        return this;
    }

    @Override
    public EnumSet<SGR> getActiveModifiers() {
        return activeModifiers;
    }

    @Override
    public TabBehaviour getTabBehaviour() {
        return tabBehaviour;
    }

    @Override
    public TextGraphics setTabBehaviour(TabBehaviour tabBehaviour) {
        if(tabBehaviour != null) {
            this.tabBehaviour = tabBehaviour;
        }
        return this;
    }

    @Override
    public TextGraphics fill(char c) {
        fillRectangle(TerminalPosition.TOP_LEFT_CORNER, getSize(), c);
        return this;
    }

    @Override
    public TextGraphics setCharacter(int column, int row, char character) {
        return setCharacter(column, row, newTextCharacter(character));
    }

    @Override
    public TextGraphics setCharacter(TerminalPosition position, TextCharacter textCharacter) {
        setCharacter(position.getColumn(), position.getRow(), textCharacter);
        return this;
    }

    @Override
    public TextGraphics setCharacter(TerminalPosition position, char character) {
        return setCharacter(position.getColumn(), position.getRow(), character);
    }

    @Override
    public TextGraphics drawLine(TerminalPosition fromPosition, TerminalPosition toPoint, char character) {
        return drawLine(fromPosition, toPoint, newTextCharacter(character));
    }

    @Override
    public TextGraphics drawLine(TerminalPosition fromPoint, TerminalPosition toPoint, TextCharacter character) {
        shapeRenderer.drawLine(fromPoint, toPoint, character);
        return this;
    }

    @Override
    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, char character) {
        return drawLine(fromX, fromY, toX, toY, newTextCharacter(character));
    }

    @Override
    public TextGraphics drawLine(int fromX, int fromY, int toX, int toY, TextCharacter character) {
        return drawLine(new TerminalPosition(fromX, fromY), new TerminalPosition(toX, toY), character);
    }

    @Override
    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
        return drawTriangle(p1, p2, p3, newTextCharacter(character));
    }

    @Override
    public TextGraphics drawTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
        shapeRenderer.drawTriangle(p1, p2, p3, character);
        return this;
    }

    @Override
    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, char character) {
        return fillTriangle(p1, p2, p3, newTextCharacter(character));
    }

    @Override
    public TextGraphics fillTriangle(TerminalPosition p1, TerminalPosition p2, TerminalPosition p3, TextCharacter character) {
        shapeRenderer.fillTriangle(p1, p2, p3, character);
        return this;
    }

    @Override
    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
        return drawRectangle(topLeft, size, newTextCharacter(character));
    }

    @Override
    public TextGraphics drawRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
        shapeRenderer.drawRectangle(topLeft, size, character);
        return this;
    }

    @Override
    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, char character) {
        return fillRectangle(topLeft, size, newTextCharacter(character));
    }

    @Override
    public TextGraphics fillRectangle(TerminalPosition topLeft, TerminalSize size, TextCharacter character) {
        shapeRenderer.fillRectangle(topLeft, size, character);
        return this;
    }

    @Override
    public TextGraphics drawImage(TerminalPosition topLeft, TextImage image) {
        return drawImage(topLeft, image, TerminalPosition.TOP_LEFT_CORNER, image.getSize());
    }

    @Override
    public TextGraphics drawImage(
            TerminalPosition topLeft,
            TextImage image,
            TerminalPosition sourceImageTopLeft,
            TerminalSize sourceImageSize) {

        // If the source image position is negative, offset the whole image
        if(sourceImageTopLeft.getColumn() < 0) {
            topLeft = topLeft.withRelativeColumn(-sourceImageTopLeft.getColumn());
            sourceImageSize = sourceImageSize.withRelativeColumns(sourceImageTopLeft.getColumn());
            sourceImageTopLeft = sourceImageTopLeft.withColumn(0);
        }
        if(sourceImageTopLeft.getRow() < 0) {
            topLeft = topLeft.withRelativeRow(-sourceImageTopLeft.getRow());
            sourceImageSize = sourceImageSize.withRelativeRows(sourceImageTopLeft.getRow());
            sourceImageTopLeft = sourceImageTopLeft.withRow(0);
        }

        // cropping specified image-subrectangle to the image itself:
        int fromRow = Math.max(sourceImageTopLeft.getRow(), 0);
        int untilRow = Math.min(sourceImageTopLeft.getRow() + sourceImageSize.getRows(), image.getSize().getRows());
        int fromColumn = Math.max(sourceImageTopLeft.getColumn(), 0);
        int untilColumn = Math.min(sourceImageTopLeft.getColumn() + sourceImageSize.getColumns(), image.getSize().getColumns());

        // difference between position in image and position on target:
        int diffRow = topLeft.getRow() - sourceImageTopLeft.getRow();
        int diffColumn = topLeft.getColumn() - sourceImageTopLeft.getColumn();

        // top/left-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate)
        fromRow = Math.max(fromRow, -diffRow);
        fromColumn = Math.max(fromColumn, -diffColumn);

        // bot/right-crop at target(TextGraphics) rectangle: (only matters, if topLeft has a negative coordinate)
        untilRow = Math.min(untilRow, getSize().getRows() - diffRow);
        untilColumn = Math.min(untilColumn, getSize().getColumns() - diffColumn);

        if (fromRow >= untilRow || fromColumn >= untilColumn) {
            return this;
        }
        for (int row = fromRow; row < untilRow; row++) {
            for (int column = fromColumn; column < untilColumn; column++) {
                setCharacter(column + diffColumn, row + diffRow, image.getCharacterAt(column, row));
            }
        }
        return this;
    }

    @Override
    public TextGraphics putString(int column, int row, String string) {
        if(string.contains("\n")) {
            string = string.substring(0, string.indexOf("\n"));
        }
        if(string.contains("\r")) {
            string = string.substring(0, string.indexOf("\r"));
        }
        string = tabBehaviour.replaceTabs(string, column);
        int offset = 0;
        for(int i = 0; i < string.length(); i++) {
            char character = string.charAt(i);
            setCharacter(
                    column + offset,
                    row,
                    new TextCharacter(
                            character,
                            foregroundColor,
                            backgroundColor,
                            activeModifiers.clone()));
            
            if(CJKUtils.isCharCJK(character)) {
                //CJK characters are twice the normal characters in width, so next character position is two columns forward
                offset += 2;
            }
            else {
                //For "normal" characters we advance to the next column
                offset += 1;
            }
        }
        return this;
    }

    @Override
    public TextGraphics putString(TerminalPosition position, String string) {
        putString(position.getColumn(), position.getRow(), string);
        return this;
    }

    @Override
    public TextGraphics putString(int column, int row, String string, SGR extraModifier, SGR... optionalExtraModifiers) {clearModifiers();
        EnumSet<SGR> extraModifiers = EnumSet.of(extraModifier, optionalExtraModifiers);
        extraModifiers.removeAll(activeModifiers);
        enableModifiers(extraModifiers);
        putString(column, row, string);
        disableModifiers(extraModifiers);
        return this;
    }

    @Override
    public TextGraphics putString(TerminalPosition position, String string, SGR extraModifier, SGR... optionalExtraModifiers) {
        putString(position.getColumn(), position.getRow(), string, extraModifier, optionalExtraModifiers);
        return this;
    }

    @Override
    public TextGraphics newTextGraphics(TerminalPosition topLeftCorner, TerminalSize size) throws IllegalArgumentException {
        TerminalSize writableArea = getSize();
        if(topLeftCorner.getColumn() + size.getColumns() <= 0 ||
                topLeftCorner.getColumn() >= writableArea.getColumns() ||
                topLeftCorner.getRow() + size.getRows() <= 0 ||
                topLeftCorner.getRow() >= writableArea.getRows()) {
            //The area selected is completely outside of this TextGraphics, so we can return a "null" object that doesn't
            //do anything because it is impossible to change anything anyway
            return new NullTextGraphics(size);
        }
        return new SubTextGraphics(this, topLeftCorner, size);
    }

    private TextCharacter newTextCharacter(char character) {
        return new TextCharacter(character, foregroundColor, backgroundColor, activeModifiers);
    }
}
