/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.io;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;

/**
 * The <code>LineColumnReader</code> is an extension to <code>BufferedReader</code>
 * that keeps track of the line and column information of where the cursor is.
 *
 * @author Guillaume Laforge
 * @since 1.8.0
 */
public class LineColumnReader extends BufferedReader {

    /**
     * The current line position
     */
    private long line = 1;

    /**
     * The current column position
     */
    private long column = 1;

    /**
     * The latest marked line position
     */
    private long lineMark = 1;

    /**
     * The latest marked line position
     */
    private long columnMark = 1;

    private boolean newLineWasRead = false;

    /**
     * Constructor wrapping a <code>Reader</code>
     * (<code>FileReader</code>, <code>FileReader</code>, <code>InputStreamReader</code>, etc.)
     *
     * @param reader the reader to wrap
     */
    public LineColumnReader(Reader reader) {
        super(reader);
    }

    /**
     * Marks the present position in the stream. Subsequent calls to reset() will attempt to reposition the stream to this point.
     *
     * @param readAheadLimit  Limit on the number of characters that may be read while still preserving the mark.
     *      An attempt to reset the stream after reading characters up to this limit or beyond may fail.
     *      A limit value larger than the size of the input buffer will cause a new buffer to be allocated whose size is no smaller than limit.
     *      Therefore large values should be used with care.
     */
    @Override
    public void mark(int readAheadLimit) throws IOException {
        lineMark = line;
        columnMark = column;
        super.mark(readAheadLimit);
    }

    /**
     * Resets the stream to the most recent mark.
     */
    @Override
    public void reset() throws IOException {
        line = lineMark;
        column = columnMark;
        super.reset();
    }

    /**
     * Reads a single character.
     *
     * @return The character read, as an integer in the range 0 to 65535 (0x00-0xffff),
     *      or -1 if the end of the stream has been reached
     */
    @Override
    public int read() throws IOException {
        if (newLineWasRead) {
            line += 1;
            column = 1;
            newLineWasRead = false;
        }

        int charRead = super.read();
        if (charRead > -1) {
            char c = (char)charRead;
            // found a \r or \n, like on Mac or Unix
            // could also be Windows' \r\n
            if (c == '\r' || c == '\n') {
                newLineWasRead = true;
                if (c == '\r') {
                    mark(1);
                    c = (char)super.read();
                    // check if we have \r\n like on Windows
                    // if it's not \r\n we reset, otherwise, the \n is just consummed
                    if (c != '\n') {
                        reset();
                    }
                }
            } else {
                column += 1;
            }
        }

        return charRead;
    }

    /**
     * Reads characters into a portion of an array.
     *
     * @param chars Destination array of char
     * @param startOffset Offset at which to start storing characters
     * @param length Maximum number of characters to read
     * @return an exception if an error occurs
     */
    @Override
    public int read(char[] chars, int startOffset, int length) throws IOException {
        for (int i = startOffset; i <= startOffset + length; i++) {
            int readInt = read();
            if (readInt == -1) return i - startOffset;
            chars[i] = (char)readInt;
        }
        return length;
    }

    /**
     * Reads a line of text. A line is considered to be terminated by any one of a line feed ('\n'),
     * a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
     *
     * @return A String containing the contents of the line, not including any line-termination characters,
     *      or null if the end of the stream has been reached
     */
    @Override
    public String readLine() throws IOException {
        StringBuilder result = new StringBuilder();
        for (;;) {
            int intRead = read();
            if (intRead == -1) {
                return result.length() == 0 ? null : result.toString();
            }

            char c = (char)intRead;
            if (c == '\n' || c == '\r') break;
            result.append(c);
        }
        return result.toString();
    }

    /**
     * Skips characters.
     *
     * @param toSkip the number of characters to skip
     * @return The number of characters actually skipped
     */
    @Override
    public long skip(long toSkip) throws IOException {
        for (long i = 0; i < toSkip; i++) {
            int intRead = read();
            if (intRead == -1) return i;
        }
        return toSkip;
    }

    /**
     * Reads characters into an array.
     * This method will block until some input is available, an I/O error occurs,
     *  or the end of the stream is reached.
     *
     * @param chars Destination buffer
     * @return The number of characters read, or -1 if the end of the stream has been reached
     */
    @Override
    public int read(char[] chars) throws IOException {
        return read(chars, 0, chars.length - 1);
    }

    /**
     * Not implemented.
     *
     * @param buffer Destination buffer
     * @return The number of characters read, or -1 if the end of the stream has been reached
     * @throws UnsupportedOperationException as the method is not implemented
     */
    @Override
    public int read(CharBuffer buffer) {
        throw new UnsupportedOperationException("read(CharBuffer) not yet implemented");
    }

    /**
     * Closes the stream and releases any system resources associated with it.
     * Once the stream has been closed, further read(), ready(), mark(), reset(), or skip() invocations
     * will throw an IOException. Closing a previously closed stream has no effect.
     */
    @Override
    public void close() throws IOException {
        super.close();
    }

    public long getColumn() {
        return column;
    }

    public void setColumn(long column) {
        this.column = column;
    }

    public long getColumnMark() {
        return columnMark;
    }

    public void setColumnMark(long columnMark) {
        this.columnMark = columnMark;
    }

    public long getLine() {
        return line;
    }

    public void setLine(long line) {
        this.line = line;
    }

    public long getLineMark() {
        return lineMark;
    }

    public void setLineMark(long lineMark) {
        this.lineMark = lineMark;
    }
}
