/*
* Copyright (c) 2005 Nokia Corporation and/or its subsidiary(-ies).
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of the License "Eclipse Public License v1.0"
* which accompanies this distribution, and is available
* at the URL "http://www.eclipse.org/legal/epl-v10.html".
*
* Initial Contributors:
* Nokia Corporation - initial contribution.
*
* Contributors:
*
* Description: 
*
*/

package com.nokia.sdt.sourcegen;

import com.nokia.cpp.internal.api.utils.core.Check;


/**
 * Base functionality for a source formatter, which allows the adaptive
 * formatting based on an externally managed source buffer.
 * <p>
 * This class provides support for quickly priming the formatter's at-newline and 
 * at-space flags based on the current position.
 * <p>
 * For cases requiring indentation -- such as, when the at-newline flag is true,
 * or when an open or close block character is emitted, #findIndentLevel() does
 * more work to read the current or previous line to find what the current
 * indentation level is.
 * <p>
 * These routines use a ISourceFormatting object to determine how to format
 * text, but existing formatting overrules this object. The line ending style is
 * derived from the source, and the un- indentation removes characters from the
 * existing indentation sequence (although when that sequence is spaces but tabs
 * are configured, we cannot predict how much to remove; only one character at a
 * time is removed in such a case).
 * 
 * 
 * 
 */
public abstract class SourceFormatterBase implements ISourceFormatter {

	private ISourceFormatting formatting;

	/** unit of indentation */
	protected String indentUnit;
	/** using tabs? */
	private boolean indentTabs;
	/** spaces to indent, if tabs are not used */
	private int indentSpaces;
	/** cached indent spaces, even if tabs are used */
	private String indentSpacesUnit;
	
	// generated by findCurrentPositionInfo()
	/** currently sitting after a newline? */
	protected boolean atNewLine;
	/** if atNewLine, the newline used */
	protected String newLine;
	/** currently sitting after a space? */
	protected boolean atSpace;

	// generated by findIndentLevel()
    /** current indentation */
	protected StringBuilder indent;
	/** brace delta (>1 means more opens than closes) */
	protected int braceDelta;
    
	/** buffer, free to use per invocation */
	protected StringBuilder formatBuffer;

	protected boolean atIndent;


	
    /**
     * 
     */
    public SourceFormatterBase(ISourceFormatting formatting) {
    	this.formatting = formatting;
    	this.indentSpaces = formatting.getIndentSpaces();
    	this.indentTabs = formatting.isUsingTabs();
        if (indentSpaces <= 8)
        	this.indentSpacesUnit = "        ".substring(8 - indentSpaces); //$NON-NLS-1$
        else {
            StringBuilder buf = new StringBuilder();
            for (int i = 0; i < indentSpaces; i++)
                buf.append(' ');
            this.indentSpacesUnit = buf.toString();
        }

    	if (indentTabs)
            this.indentUnit = "\t"; //$NON-NLS-1$
        else
        	this.indentUnit = indentSpacesUnit;
    	
        indent = new StringBuilder();
        formatBuffer = new StringBuilder();
        
        this.newLine = formatting.getEOL();
    }

    /* (non-Javadoc)
     * @see com.nokia.sdt.sourcegen.ISourceFormatter#getSourceFormatting()
     */
    public ISourceFormatting getSourceFormatting() {
    	return formatting;
    }
    
    /* (non-Javadoc)
     * @see com.nokia.sdt.sourcegen.ISourceFormatter#setSourceFormatting(com.nokia.sdt.sourcegen.ISourceFormatting)
     */
    public void setSourceFormatting(ISourceFormatting formatting) {
    	Check.checkArg(formatting);
    	this.formatting = formatting;
    }

    /**
     * Find current end-of-line and at-space flags.
     * @param seq
     */
    protected void findCurrentPositionInfo(CharSequence seq) {
    	int offs = seq.length();
    	atSpace = false;
    	atNewLine = false;
    	atIndent = false;
    	newLine = null;
    	if (offs >= 0) {
    		offs--;
    		char ch = offs >= 0 ? seq.charAt(offs) : '\n';
    		if (ch == ' ' || ch == '\t') {
    			atSpace = true;
    			atIndent = true; // so far
    		}
    		else if (ch == '\r' || ch == '\n') {
    			atNewLine = true;
    			atIndent = true;
    			if (ch == '\n') {
    				if (offs > 0 && seq.charAt(offs - 1) == '\r') {
    					newLine = "\r\n"; //$NON-NLS-1$
    					offs--;
    				} else if (offs >= 0)
    					newLine = "\n"; //$NON-NLS-1$
    			}
    			else
    				newLine = "\r"; //$NON-NLS-1$
    		}
    		
    		// if not found, keep looking, so we can keep the
    		// existing style where possible
    		if (newLine == null) {
    			while (offs > 0) {
    				offs--;
    	    		ch = seq.charAt(offs);
    	    		if (ch == '\r' || ch == '\n') {
    	    			if (ch == '\n') {
    	    				if (offs > 0 && seq.charAt(offs - 1) == '\r') {
    	    					newLine = "\r\n"; //$NON-NLS-1$
    	    					offs--;
    	    				} else
    	    					newLine = "\n"; //$NON-NLS-1$
    	    			}
    	    			else
    	    				newLine = "\r"; //$NON-NLS-1$
    	    			break;
    	    		}
    	    		else if (ch != '\t' && ch != ' ') 
    	    			atIndent = false;
    			}
    		} else {
    			// finalize atIndent
    			while (offs >= 0) {
    	    		ch = seq.charAt(offs);
    	    		if (ch == '\r' || ch == '\n')
    	    			break;
    	    		else if (ch != '\t' && ch != ' ') { 
    	    			atIndent = false;
    	    			break;
    	    		}
    	    		offs--;
    			}
    			
    		}
    	}
    	
    	if (newLine == null)
    		newLine = formatting.getEOL();
    	
    }

    /**
     * Find current indentation level for the given line,
     * where 'seq' is positioned at a new line.  This is only
     * needed upon a resync, and is not accurate.
     * We'd hope, but it's not guaranteed, that the indentation
     * matches the expected format.
     * @param seq
     */
    protected void findIndentLevel(CharSequence seq) {
    	int offs = seq.length();

    	braceDelta = 0;
    	
    	if (atNewLine) {
	    	// skip the EOL (which may not exist in an empty buffer)
    		offs -= newLine.length();
    	}
    	
    	// find the beginning of the previous line, and 
    	// find the span of indentation text
    	int wsOffs = -1;
    	while (offs >= 0) {
    		offs--;
    		char ch = offs >= 0 ? seq.charAt(offs) : '\n';
    		if (ch == ' ' || ch == '\t') {
    			// remember where whitespace ends on line
    			if (wsOffs == -1)
    				wsOffs = offs + 1;
    		}
    		else if (ch == '\r' || ch == '\n') {
    			break;
    		}
    		else {
    			if (ch == '{')
    				braceDelta++;
    			else if (ch == '}')
    				braceDelta--;
    			
    			wsOffs = -1;
    		}
    	}

    	// got to the beginning
		indent.setLength(0);
		if (wsOffs != -1)
			indent.append(seq, offs + 1, wsOffs);
    }
    
    /**
     * Update indentation
     */
    protected void updateIndent(int levels) {
    	while (levels > 0) {
    		indent.append(indentUnit);
    		levels--;
    	}
    	
    	while (levels < 0) {
			dedent();
			levels++;
    	}
    }

    /**
     * Dedent (unindent) by removing the last indentation
     * from the scanned indentation text.
     */
    protected void dedent() {
    	int indentLength = indent.length();
    	if (indentLength == 0)
    		return;
    	
    	// expect matching format
    	int idx = indent.lastIndexOf(indentUnit);
    	// N.B.: if idx==-1, no way this can match
    	if (idx + indentUnit.length() == indentLength) {
    		indent.delete(idx, indentLength);
    	} else {
    		// assume a mismatched indent uses the expected
    		// number of spaces or tabs
    		if (indentTabs && indentSpaces <= indentLength &&
    				((idx = indent.lastIndexOf(indentSpacesUnit))) + indentSpaces == indentLength)
    			indent.delete(indentLength - indentSpaces, indentLength);
    		else
    			// else, just bail and take off one character
    			indent.delete(indentLength - 1, indentLength);
    	}
    }
    
}