/*
 * 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 org.apache.commons.csv;

import java.io.IOException;

/**
 * Abstract lexer class; contains common utility routines shared by lexers
 */
abstract class Lexer {

    private final boolean isEncapsulating;
    private final boolean isEscaping;
    private final boolean isCommentEnabled;

    private final char delimiter;
    private final char escape;
    private final char encapsulator;
    private final char commmentStart;

    final boolean surroundingSpacesIgnored;
    final boolean emptyLinesIgnored;

    final CSVFormat format;

    /** The input stream */
    final ExtendedBufferedReader in;

    Lexer(final CSVFormat format, final ExtendedBufferedReader in) {
        this.format = format;
        this.in = in;
        this.isEncapsulating = format.isEncapsulating();
        this.isEscaping = format.isEscaping();
        this.isCommentEnabled = format.isCommentingEnabled();
        this.delimiter = format.getDelimiter();
        this.escape = format.getEscape();
        this.encapsulator = format.getEncapsulator();
        this.commmentStart = format.getCommentStart();
        this.surroundingSpacesIgnored = format.getIgnoreSurroundingSpaces();
        this.emptyLinesIgnored = format.getIgnoreEmptyLines();
    }

    int getLineNumber() {
        return in.getLineNumber();
    }

    // TODO escape handling needs more work
    int readEscape() throws IOException {
        // assume c is the escape char (normally a backslash)
        final int c = in.read();
        switch (c) {
        case 'r':
            return '\r';
        case 'n':
            return '\n';
        case 't':
            return '\t';
        case 'b':
            return '\b';
        case 'f':
            return '\f';
        case ExtendedBufferedReader.END_OF_STREAM:
            throw new IOException("EOF whilst processing escape sequence");
        default:
            return c;
        }
    }

    void trimTrailingSpaces(final StringBuilder buffer) {
        int length = buffer.length();
        while (length > 0 && Character.isWhitespace(buffer.charAt(length - 1))) {
            length = length - 1;
        }
        if (length != buffer.length()) {
            buffer.setLength(length);
        }
    }

    /**
     * @return true if the given char is a whitespace character
     */
    boolean isWhitespace(final int c) {
        return c != format.getDelimiter() && Character.isWhitespace((char) c);
    }

    /**
     * Greedy - accepts \n, \r and \r\n This checker consumes silently the second control-character...
     *
     * @return true if the given character is a line-terminator
     */
    boolean isEndOfLine(int c) throws IOException {
        // check if we have \r\n...
        if (c == '\r' && in.lookAhead() == '\n') {
            // note: does not change c outside of this method !!
            c = in.read();
        }
        return c == '\n' || c == '\r';
    }

    /**
     * Checks if the current character represents the start of a line: a CR, LF or is at the start of the file.
     *
     * @param c
     * @return true if the character is at the start of a line.
     */
    boolean isStartOfLine(final int c) {
        return c == '\n' || c == '\r' || c == ExtendedBufferedReader.UNDEFINED;
    }

    /**
     * @return true if the given character indicates end of file
     */
    boolean isEndOfFile(final int c) {
        return c == ExtendedBufferedReader.END_OF_STREAM;
    }

    abstract Token nextToken(Token reusableToken) throws IOException;

    boolean isDelimiter(final int c) {
        return c == delimiter;
    }

    boolean isEscape(final int c) {
        return isEscaping && c == escape;
    }

    boolean isEncapsulator(final int c) {
        return isEncapsulating && c == encapsulator;
    }

    boolean isCommentStart(final int c) {
        return isCommentEnabled && c == commmentStart;
    }
}

@@ -966,16 +966,229 @@
 exer %7B%0A%0A
+    private static final char FF = '%5Cf';%0A    private static final char BELL = '%5Cb';%0A    private static final char TAB = '%5Ct';%0A    private static final char LF = '%5Cn';%0A    private static final char CR = '%5Cr';%0A    %0A
     priv
@@ -2563,20 +2563,18 @@
  return 
-'%5Cr'
+CR
 ;%0A      
@@ -2604,20 +2604,18 @@
  return 
-'%5Cn'
+LF
 ;%0A      
@@ -2645,20 +2645,19 @@
  return 
-'%5Ct'
+TAB
 ;%0A      
@@ -2687,20 +2687,20 @@
  return 
-'%5Cb'
+BELL
 ;%0A      
@@ -2730,20 +2730,18 @@
  return 
-'%5Cf'
+FF
 ;%0A      
@@ -3735,20 +3735,18 @@
 f (c == 
-'%5Cr'
+CR
  && in.l
@@ -3759,20 +3759,18 @@
 ad() == 
-'%5Cn'
+LF
 ) %7B%0A    
@@ -3879,36 +3879,34 @@
     return c == 
-'%5Cn'
+LF
  %7C%7C c == '%5Cr';%0A 
@@ -3898,20 +3898,18 @@
 %7C%7C c == 
-'%5Cr'
+CR
 ;%0A    %7D%0A
@@ -4185,20 +4185,18 @@
 rn c == 
-'%5Cn'
+LF
  %7C%7C c ==
@@ -4196,20 +4196,18 @@
 %7C%7C c == 
-'%5Cr'
+CR
  %7C%7C c ==

Refactor magic strings into constants.