package net.sourceforge.phpdt.internal.ui.text;

/*
 * (c) Copyright IBM Corp. 2000, 2001.
 * All Rights Reserved.
 */

import java.io.IOException;

import net.sourceforge.phpdt.internal.corext.phpdoc.SingleCharReader;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;

/**
 * Reads from a document either forwards or backwards. May be configured to skip
 * comments and strings.
 */
public class PHPCodeReader extends SingleCharReader {

	/** The EOF character */
	public static final int EOF = -1;
	private boolean fSkipComments = false;
	private boolean fSkipStrings  = false;
	private boolean fForward      = false;
	
	private IDocument fDocument;
	
	private int fOffset;                           // The current text position within the editor's text
	private int fEnd              = -1;
	private int fCachedLineNumber = -1;            // Holds the last line we checked for single line comments

	public PHPCodeReader() {
	}

	/**
	 * Returns the offset of the last read character. Should only be called
	 * after read has been called.
	 */
	public int getOffset() {
		return fForward ? fOffset - 1 : fOffset;
	}

	public void configureForwardReader(IDocument document, int offset,
			int length, boolean skipComments, boolean skipStrings)
			throws IOException {
		fDocument = document;
		fOffset = offset;
		fSkipComments = skipComments;
		fSkipStrings = skipStrings;

		fForward = true;
		fEnd = Math.min(fDocument.getLength(), fOffset + length);
	}

	public void configureBackwardReader(IDocument document, int offset,
			boolean skipComments, boolean skipStrings) throws IOException {
		fDocument = document;
		fOffset = offset;
		fSkipComments = skipComments;
		fSkipStrings = skipStrings;

		fForward = false;
		try {
			fCachedLineNumber = fDocument.getLineOfOffset(fOffset);
		} catch (BadLocationException x) {
			throw new IOException(x.getMessage());
		}
	}

	/*
	 * @see Reader#close()
	 */
	public void close() throws IOException {
		fDocument = null;
	}

	/*
	 * @see SingleCharReader#read()
	 */
	public int read() throws IOException {
		try {
			return fForward ? readForwards() : readBackwards();
		} catch (BadLocationException x) {
			throw new IOException(x.getMessage());
		}
	}

	private int gotoCommentEnd (int nTextPos) throws BadLocationException {
		while (nTextPos < fEnd) {
			char current = fDocument.getChar (nTextPos++);
			
			if (current == '*') {
				if ((nTextPos < fEnd) && 
				    (fDocument.getChar (nTextPos) == '/')) {
					++nTextPos;
					
					return nTextPos;
				}
			}
		}
		
		return nTextPos;
	}

	/**
	 * 
	 * @param delimiter
	 * @throws BadLocationException
	 */
	private int gotoStringEnd (int nTextPos, char delimiter) throws BadLocationException {
		while (nTextPos < fEnd) {               // If long as we are not at the end of text
			char current = fDocument.getChar (nTextPos++);
			
			if (current == '\\') {				// ignore escaped characters
				++nTextPos;
			} 
			else if (current == delimiter) {
				return nTextPos;
			}
		}
		
		return nTextPos;                        // End position
	}

	/**
	 * 
	 * @param nTextPos               The current text position
	 * 
	 * @return                       The position of the start of next line
	 * 
	 * @throws BadLocationException
	 */
	private int gotoLineEnd (int nTextPos) throws BadLocationException {
		int line = fDocument.getLineOfOffset (nTextPos); // Get the line number of the current text position
		
		return fDocument.getLineOffset (line + 1);
	}

	private int readForwards () throws BadLocationException {
		while (fOffset < fEnd) {
			char current = fDocument.getChar(fOffset++);

			switch (current) {
			    case '"':
			    case '\'':
			        if (fSkipStrings) {
			            fOffset = gotoStringEnd (fOffset, current);
			            continue;
			        }
			        return current;
				
			    case '#':
			        if (fSkipComments && fOffset < fEnd) {
			            fOffset = gotoLineEnd (fOffset);
			            continue;
			        }
			        return current;

			    case '/':
			        if (fSkipComments && fOffset < fEnd) {
			            char next = fDocument.getChar(fOffset);
			            
			            if (next == '*') {			                // A comment starts, advance to the comment end
			                ++fOffset;
			                fOffset = gotoCommentEnd (fOffset);
			                continue;
			            } else if (next == '/') {                   // '//'-comment starts, advance to the line end
			                fOffset = gotoLineEnd (fOffset);
			                continue;
			            }
			        }    
			        return current;
			}

			return current;
		}

		return EOF;
	}

	/**
	 * Check whether the current line contains a single line comment.
	 * If it contains a single line comment (// or #), than set fOffset (the global character index)
	 * to the character just before the single line comment.
	 * 
	 * @throws BadLocationException
	 */
	
	private void handleSingleLineComment () throws BadLocationException {
	    char current;
		int  line = fDocument.getLineOfOffset (fOffset);         // Get the line number which belongs to the current text position fOffset
		
		if (line < fCachedLineNumber) {                          // If we didn't parse this line already
			fCachedLineNumber = line;                            // Remember this line for the next time (so we don't search again!)   
			int nOffset       = fDocument.getLineOffset (line);  // Get the start position of current line      
			
			while (nOffset < fOffset) {                          // As long as the text position is within the current line 
				current = fDocument.getChar (nOffset);           // Get the character from the current text position

				switch (current) {
				    case '/':
				        if (fDocument.getChar (nOffset + 1) == '/') { // If the following character is '/'
				            fOffset = nOffset - 1;
				            
				            return;
				        }
				        break;    
				    
				    case '#':
                        fOffset = nOffset - 1;
                        return;
				    
				    case '"':                                           // It's a string start quote
				    case '\'':
				        nOffset++;                                      // Set to next character
				        
				        while (nOffset < fOffset) {                     // As long as we are within the same line
				            char cChar = fDocument.getChar (nOffset++);
				            
				            if (cChar == '\\') {                        // Ignore escaped characters
				                ++nOffset;
				            } 
				            else if (cChar == current) {                // If end of string found 
				                break;
				            }
				        }
				        break;
				}
				
				nOffset++;                                              // Go for the next character
			}
		}
	}

	/**
	 * We search the for the block comment start sequence "/*"
	 * 
	 * The returned value points to the '/'
	 * 
	 * @throws BadLocationException
	 * 
	 * @return The new text position
	 */
	
	private int gotoCommentStart (int nTextPos) throws BadLocationException {
		while (0 < nTextPos) {                                // As long as we are not at the start of the editor text
		    char current = fDocument.getChar (nTextPos--);    // Get the character from the current text position        
			
			if ((current == '*') &&                           // If current character is a '*' 
			    (0 <= nTextPos) &&                            // and if we are not yet at the start of the editor text 
			    (fDocument.getChar (nTextPos) == '/')) {      // and the previous character is a '/', 
				return nTextPos;                              // We found the block comment start "/*"
			}
		}
		
		return nTextPos;
	}

	/**
	 * The string closing quote has been found, when reading the editor text backwards.
	 * So we have to search for the start of the string
	 * 
	 * The returned value points to '"' or '''
	 * 
	 * @param delimiter The string double quote '"' or single ''' quote we search for
	 * 
	 * @throws BadLocationException
	 */
	private int gotoStringStart (int nTextPos, char delimiter) throws BadLocationException {
		while (0 < nTextPos) {                              // As long as we are not at the start of the editor text
			char current = fDocument.getChar (nTextPos);    // Get the character from the current text position 
			                                                
			if (current == delimiter) {                     // If we found the same character ('"' or ''') again, we have found the string start
				if (!(0 <= nTextPos &&                        
				      fDocument.getChar (nTextPos - 1) == '\\')) // If the character before the string quote is not an '/' 
				return nTextPos;                                 // we found the string start
			}
			                                                    
			--nTextPos;                                          // Go one character back
		}
		
		return nTextPos;
	}

	/**
	 * Read the editor text backwards
	 * 
	 */
	
	private int readBackwards() throws BadLocationException {
	    char current;                                                // The character on position fOffset
	    
		while (0 < fOffset) {                                        // As long as we are not at the beginning of the editor text
			--fOffset;                                               // Step back one character

			handleSingleLineComment ();                              // Search for a single line comment within the current line
			                                                         // If there is a single line comment, fOffset is set to position just before the single line comment

			current = fDocument.getChar (fOffset);                   // Read the current character
			switch (current) {                                       // Process the current character
			    case '/':                                            // Is it a single line comment? 
			        if (fSkipComments &&                             // When comments should be skipped when parsing
			            fOffset > 1) {                               // and if there is indeed an additional character before that '/'
			            char prev = fDocument.getChar (fOffset - 1); // Look at the character in front of the '/'
			            
			            if (prev == '*') {                           // If '*' than a block comment ends here, advance to the comment start
			                fOffset -= 2;                            // Set character pointer to character before "*/"
			                fOffset  = gotoCommentStart (fOffset);   // and search for the starting "/*" of the block comment
			                continue;
			            }
			        }
			        return current;                                  // No block comment end, so return the current character
			        
			    case '"':                                            // Is it a string double quote closing '"'?                                                
			    case '\'':                                           // or a string single quote closing '''?
			        if (fSkipStrings) {                              // If we should skip strings when parsing
			            --fOffset;                                   // Set pointer one before the string closing quote 
			            fOffset = gotoStringStart (fOffset, current);// and search the start of string ('"' or ''')
			            continue;
			        }
			        return current;                                  // No string skip, so return the current character
			}

			return current;                                          // No string and no comment
		}

		return EOF;                                                  // When the start of the text has been found
	}
}