package com.termux.terminal;

import junit.framework.TestCase;
import java.util.Arrays;
import java.util.Random;

public class TerminalRowTest extends TestCase {

    /**
     * The properties of these code points are validated in {@link #testStaticConstants()}.
     */
    private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 = 0x679C;

    private static final int ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2 = 0x679D;

    private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1 = 0x2070E;

    private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2 = 0x20731;

    /**
     * Unicode Character 'MUSICAL SYMBOL G CLEF' (U+1D11E). Two java chars required for this.
     */
    static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1 = 0x1D11E;

    /**
     * Unicode Character 'MUSICAL SYMBOL G CLEF OTTAVA ALTA' (U+1D11F). Two java chars required for this.
     */
    private static final int TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2 = 0x1D11F;

    private final int COLUMNS = 80;

    /**
     * A combining character.
     */
    private static final int DIARESIS_CODEPOINT = 0x0308;

    private TerminalRow row;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        row = new TerminalRow(COLUMNS, TextStyle.NORMAL);
    }

    private void assertLineStartsWith(int... codePoints) {
        char[] chars = row.mText;
        int charIndex = 0;
        for (int i = 0; i < codePoints.length; i++) {
            int lineCodePoint = chars[charIndex++];
            if (Character.isHighSurrogate((char) lineCodePoint)) {
                lineCodePoint = Character.toCodePoint((char) lineCodePoint, chars[charIndex++]);
            }
            assertEquals("Differing a code point index=" + i, codePoints[i], lineCodePoint);
        }
    }

    private void assertColumnCharIndicesStartsWith(int... indices) {
        for (int i = 0; i < indices.length; i++) {
            int expected = indices[i];
            int actual = row.findStartOfColumn(i);
            assertEquals("At index=" + i, expected, actual);
        }
    }

    public void testSimpleDiaresis() {
        row.setChar(0, DIARESIS_CODEPOINT, 0);
        assertEquals(81, row.getSpaceUsed());
        row.setChar(0, DIARESIS_CODEPOINT, 0);
        assertEquals(82, row.getSpaceUsed());
        assertLineStartsWith(' ', DIARESIS_CODEPOINT, DIARESIS_CODEPOINT, ' ');
    }

    public void testStaticConstants() {
        assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
        assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
        assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
        assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
        assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
        assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
        assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1));
        assertEquals(1, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2));
        assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
        assertEquals(2, Character.charCount(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
        assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1));
        assertEquals(2, WcWidth.width(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2));
        assertEquals(1, Character.charCount(DIARESIS_CODEPOINT));
        assertEquals(0, WcWidth.width(DIARESIS_CODEPOINT));
    }

    public void testOneColumn() {
        assertEquals(0, row.findStartOfColumn(0));
        row.setChar(0, 'a', 0);
        assertEquals(0, row.findStartOfColumn(0));
    }

    public void testAscii() {
        assertEquals(0, row.findStartOfColumn(0));
        row.setChar(0, 'a', 0);
        assertLineStartsWith('a', ' ', ' ');
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(80, row.getSpaceUsed());
        row.setChar(0, 'b', 0);
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(2, row.findStartOfColumn(2));
        assertEquals(80, row.getSpaceUsed());
        assertColumnCharIndicesStartsWith(0, 1, 2, 3);
        char[] someChars = new char[] { 'a', 'c', 'e', '4', '5', '6', '7', '8' };
        char[] rawLine = new char[80];
        Arrays.fill(rawLine, ' ');
        Random random = new Random();
        for (int i = 0; i < 1000; i++) {
            int lineIndex = random.nextInt(rawLine.length);
            int charIndex = random.nextInt(someChars.length);
            rawLine[lineIndex] = someChars[charIndex];
            row.setChar(lineIndex, someChars[charIndex], 0);
        }
        char[] lineChars = row.mText;
        for (int i = 0; i < rawLine.length; i++) {
            assertEquals(rawLine[i], lineChars[i]);
        }
    }

    public void testUnicode() {
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(80, row.getSpaceUsed());
        row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
        assertEquals(81, row.getSpaceUsed());
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(2, row.findStartOfColumn(1));
        assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, ' ', ' ');
        assertColumnCharIndicesStartsWith(0, 2, 3, 4);
        row.setChar(0, 'a', 0);
        assertEquals(80, row.getSpaceUsed());
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertLineStartsWith('a', ' ', ' ');
        assertColumnCharIndicesStartsWith(0, 1, 2, 3);
        row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
        row.setChar(1, 'a', 0);
        assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 'a', ' ');
        row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, 0);
        row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, 0);
        assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_1, TWO_JAVA_CHARS_DISPLAY_WIDTH_ONE_2, ' ');
        assertColumnCharIndicesStartsWith(0, 2, 4, 5);
        assertEquals(82, row.getSpaceUsed());
    }

    public void testDoubleWidth() {
        row.setChar(0, 'a', 0);
        row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
        assertLineStartsWith('a', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
        assertColumnCharIndicesStartsWith(0, 1, 1, 2);
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ' ', ' ');
        assertColumnCharIndicesStartsWith(0, 0, 1, 2);
        row.setChar(0, ' ', 0);
        assertLineStartsWith(' ', ' ', ' ', ' ');
        assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
        assertLineStartsWith(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
        assertColumnCharIndicesStartsWith(0, 0, 1, 1, 2);
        row.setChar(0, 'a', 0);
        assertLineStartsWith('a', ' ', ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, ' ');
    }

    /**
     * Just as {@link #testDoubleWidth()} but requires a surrogate pair.
     */
    public void testDoubleWidthSurrogage() {
        row.setChar(0, 'a', 0);
        assertColumnCharIndicesStartsWith(0, 1, 2, 3, 4);
        row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
        assertColumnCharIndicesStartsWith(0, 1, 1, 3, 4);
        assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
        row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
        assertColumnCharIndicesStartsWith(0, 0, 2, 3, 4);
        assertLineStartsWith(TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, ' ', ' ', ' ');
        row.setChar(0, ' ', 0);
        assertLineStartsWith(' ', ' ', ' ', ' ');
        row.setChar(0, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_1, 0);
        row.setChar(1, TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, 0);
        assertLineStartsWith(' ', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
        row.setChar(0, 'a', 0);
        assertLineStartsWith('a', TWO_JAVA_CHARS_DISPLAY_WIDTH_TWO_2, ' ');
    }

    public void testReplacementChar() {
        row.setChar(0, TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 0);
        row.setChar(1, 'Y', 0);
        assertLineStartsWith(TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'Y', ' ', ' ');
    }

    public void testSurrogateCharsWithNormalDisplayWidth() {
        // These requires a UTF-16 surrogate pair, and has a display width of one.
        int first = 0x1D306;
        int second = 0x1D307;
        // Assert the above statement:
        assertEquals(2, Character.toChars(first).length);
        assertEquals(2, Character.toChars(second).length);
        row.setChar(0, second, 0);
        assertEquals(second, Character.toCodePoint(row.mText[0], row.mText[1]));
        assertEquals(' ', row.mText[2]);
        assertEquals(2, row.findStartOfColumn(1));
        row.setChar(0, first, 0);
        assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
        assertEquals(' ', row.mText[2]);
        assertEquals(2, row.findStartOfColumn(1));
        row.setChar(1, second, 0);
        row.setChar(2, 'a', 0);
        assertEquals(first, Character.toCodePoint(row.mText[0], row.mText[1]));
        assertEquals(second, Character.toCodePoint(row.mText[2], row.mText[3]));
        assertEquals('a', row.mText[4]);
        assertEquals(' ', row.mText[5]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(2, row.findStartOfColumn(1));
        assertEquals(4, row.findStartOfColumn(2));
        assertEquals(5, row.findStartOfColumn(3));
        assertEquals(6, row.findStartOfColumn(4));
        row.setChar(0, ' ', 0);
        assertEquals(' ', row.mText[0]);
        assertEquals(second, Character.toCodePoint(row.mText[1], row.mText[2]));
        assertEquals('a', row.mText[3]);
        assertEquals(' ', row.mText[4]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(3, row.findStartOfColumn(2));
        assertEquals(4, row.findStartOfColumn(3));
        assertEquals(5, row.findStartOfColumn(4));
        for (int i = 0; i < 80; i++) {
            row.setChar(i, i % 2 == 0 ? first : second, 0);
        }
        for (int i = 0; i < 80; i++) {
            int idx = row.findStartOfColumn(i);
            assertEquals(i % 2 == 0 ? first : second, Character.toCodePoint(row.mText[idx], row.mText[idx + 1]));
        }
        for (int i = 0; i < 80; i++) {
            row.setChar(i, i % 2 == 0 ? 'a' : 'b', 0);
        }
        for (int i = 0; i < 80; i++) {
            int idx = row.findStartOfColumn(i);
            assertEquals(i, idx);
            assertEquals(i % 2 == 0 ? 'a' : 'b', row.mText[i]);
        }
    }

    public void testOverwritingDoubleDisplayWidthWithNormalDisplayWidth() {
        // Initial "OO "
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(0, row.findStartOfColumn(1));
        assertEquals(1, row.findStartOfColumn(2));
        // Setting first column to a clears second: "a  "
        row.setChar(0, 'a', 0);
        assertEquals('a', row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(2, row.findStartOfColumn(2));
        // Back to initial "OO "
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(0, row.findStartOfColumn(1));
        assertEquals(1, row.findStartOfColumn(2));
        // Setting first column to a clears first: " a "
        row.setChar(1, 'a', 0);
        assertEquals(' ', row.mText[0]);
        assertEquals('a', row.mText[1]);
        assertEquals(' ', row.mText[2]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(2, row.findStartOfColumn(2));
    }

    public void testOverwritingDoubleDisplayWidthWithSelf() {
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(0, row.findStartOfColumn(1));
        assertEquals(1, row.findStartOfColumn(2));
    }

    public void testNormalCharsWithDoubleDisplayWidth() {
        // These fit in one java char, and has a display width of two.
        assertTrue(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1 != ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2);
        assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
        assertEquals(1, Character.charCount(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
        assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1));
        assertEquals(2, WcWidth.width(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2));
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals(0, row.findStartOfColumn(1));
        assertEquals(' ', row.mText[1]);
        row.setChar(0, 'a', 0);
        assertEquals('a', row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals(1, row.findStartOfColumn(1));
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        // The first character fills both first columns.
        assertEquals(0, row.findStartOfColumn(1));
        row.setChar(2, 'a', 0);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals('a', row.mText[1]);
        assertEquals(1, row.findStartOfColumn(2));
        row.setChar(0, 'c', 0);
        assertEquals('c', row.mText[0]);
        assertEquals(' ', row.mText[1]);
        assertEquals('a', row.mText[2]);
        assertEquals(' ', row.mText[3]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(2, row.findStartOfColumn(2));
    }

    public void testNormalCharsWithDoubleDisplayWidthOverlapping() {
        // These fit in one java char, and has a display width of two.
        row.setChar(0, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, 0);
        row.setChar(2, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
        row.setChar(4, 'a', 0);
        // O = ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO
        // A = ANOTHER_JAVA_CHAR_DISPLAY_WIDTH_TWO
        // "OOAAa    "
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(0, row.findStartOfColumn(1));
        assertEquals(1, row.findStartOfColumn(2));
        assertEquals(1, row.findStartOfColumn(3));
        assertEquals(2, row.findStartOfColumn(4));
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1, row.mText[0]);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
        assertEquals('a', row.mText[2]);
        assertEquals(' ', row.mText[3]);
        row.setChar(1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, 0);
        // " AA a    "
        assertEquals(' ', row.mText[0]);
        assertEquals(ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_2, row.mText[1]);
        assertEquals(' ', row.mText[2]);
        assertEquals('a', row.mText[3]);
        assertEquals(' ', row.mText[4]);
        assertEquals(0, row.findStartOfColumn(0));
        assertEquals(1, row.findStartOfColumn(1));
        assertEquals(1, row.findStartOfColumn(2));
        assertEquals(2, row.findStartOfColumn(3));
        assertEquals(3, row.findStartOfColumn(4));
    }

    // https://github.com/jackpal/Android-Terminal-Emulator/issues/145
    public void testCrashATE145() {
        // 0xC2541 is unassigned, use display width 1 for UNICODE_REPLACEMENT_CHAR.
        // assertEquals(1, WcWidth.width(0xC2541));
        assertEquals(2, Character.charCount(0xC2541));
        assertEquals(2, WcWidth.width(0x73EE));
        assertEquals(1, Character.charCount(0x73EE));
        assertEquals(0, WcWidth.width(0x009F));
        assertEquals(1, Character.charCount(0x009F));
        int[] points = new int[] { 0xC2541, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD, 'B', 0x009B, 0x61C9, 'Z' };
        // int[] expected = new int[] { TerminalEmulator.UNICODE_REPLACEMENT_CHAR, 'a', '8', 0x73EE, 0x009F, 0x881F, 0x8324, 0xD4C9, 0xFFFD,
        // 'B', 0x009B, 0x61C9, 'Z' };
        int currentColumn = 0;
        for (int point : points) {
            row.setChar(currentColumn, point, 0);
            currentColumn += WcWidth.width(point);
        }
        // assertLineStartsWith(points);
        // assertEquals(Character.highSurrogate(0xC2541), line.mText[0]);
        // assertEquals(Character.lowSurrogate(0xC2541), line.mText[1]);
        // assertEquals('a', line.mText[2]);
        // assertEquals('8', line.mText[3]);
        // assertEquals(Character.highSurrogate(0x73EE), line.mText[4]);
        // assertEquals(Character.lowSurrogate(0x73EE), line.mText[5]);
        //
        // char[] chars = line.mText;
        // int charIndex = 0;
        // for (int i = 0; i < points.length; i++) {
        // char c = chars[charIndex];
        // charIndex++;
        // int thisPoint = (int) c;
        // if (Character.isHighSurrogate(c)) {
        // thisPoint = Character.toCodePoint(c, chars[charIndex]);
        // charIndex++;
        // }
        // assertEquals("At index=" + i + ", charIndex=" + charIndex + ", char=" + (char) thisPoint, points[i], thisPoint);
        // }
    }

    public void testNormalization() {
        // int lowerCaseN = 0x006E;
        // int combiningTilde = 0x0303;
        // int combined = 0x00F1;
        row.setChar(0, 0x006E, 0);
        assertEquals(80, row.getSpaceUsed());
        row.setChar(0, 0x0303, 0);
        assertEquals(81, row.getSpaceUsed());
        // assertEquals("\u00F1  ", new String(term.getScreen().getLine(0)));
        assertLineStartsWith(0x006E, 0x0303, ' ');
    }

    public void testInsertWideAtLastColumn() {
        row.setChar(COLUMNS - 2, 'Z', 0);
        row.setChar(COLUMNS - 1, 'a', 0);
        assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
        assertEquals('a', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
        row.setChar(COLUMNS - 1, 'ö', 0);
        assertEquals('Z', row.mText[row.findStartOfColumn(COLUMNS - 2)]);
        assertEquals('ö', row.mText[row.findStartOfColumn(COLUMNS - 1)]);
        // line.setChar(COLUMNS - 1, ONE_JAVA_CHAR_DISPLAY_WIDTH_TWO_1);
        // assertEquals('Z', line.mText[line.findStartOfColumn(COLUMNS - 2)]);
        // assertEquals(' ', line.mText[line.findStartOfColumn(COLUMNS - 1)]);
    }
}
