/*******************************************************************************
 * Copyright (c) 2008 ARM Limited and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 * ARM Limited - Initial API and implementation
 *******************************************************************************/

package org.eclipse.cdt.debug.internal.ui.disassembly.viewer;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Properties;

import org.eclipse.cdt.debug.ui.disassembly.IDocumentPresentation;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DefaultPositionUpdater;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModel;

/**
 * Converts the model elements into the text content
 */
public class VirtualDocument extends Document {
    
    public class LinePosition extends Position {

        private int fDistance = 0;

        LinePosition( int offset, int distance ) {
            super( offset );
            fDistance = distance;
        }

        LinePosition( int offset, int length, int distance ) {
            super( offset, length );
            fDistance = distance;
        }

        int getDistance() {
            return fDistance;
        }

        /* (non-Javadoc)
         * @see org.eclipse.jface.text.Position#equals(java.lang.Object)
         */
        @Override
        public boolean equals( Object other ) {
            if ( !(other instanceof LinePosition) )
                return false;
            return ( getDistance() == ((LinePosition)other).getDistance() );
        }
    }

    public final static String CATEGORY_LINE = "category_line"; // "category_line"; //$NON-NLS-1$
    private static final String PENDING_LINE = ".............................."; //$NON-NLS-1$

    private Object fRoot;
    private int fCurrentOffset = 0;

    private IDocumentPresentation fPresentationContext;
    private AnnotationModel fAnnotationModel;
    private DocumentContentProvider fContentProvider;
    private DocumentLabelProvider fLabelProvider;
    private DocumentAnnotationProvider fAnnotationProvider;
    
    public VirtualDocument( AnnotationModel annotationModel, IDocumentPresentation presentationContext, Object root ) {
        super();
        fRoot = root;
        fPresentationContext = presentationContext;
        fAnnotationModel = annotationModel;
        fContentProvider = new DocumentContentProvider( this );
        fLabelProvider = new DocumentLabelProvider( this );
        fAnnotationProvider = new DocumentAnnotationProvider( this );
        getContentProvider().init( fRoot );
    }

    public void dispose() {
        getContentProvider().dispose();
        getLabelProvider().dispose();
        getAnnotationProvider().dispose();
        fRoot = null;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.text.AbstractDocument#completeInitialization()
     */
    @Override
    protected void completeInitialization() {
        super.completeInitialization();
        addPositionCategory( CATEGORY_LINE );
        addPositionUpdater( new DefaultPositionUpdater( CATEGORY_LINE ) );
    }

    public IDocumentPresentation getPresentationContext() {
        return fPresentationContext;
    }

    public AnnotationModel getAnnotationModel() {
        return fAnnotationModel;
    }

    public DocumentContentProvider getContentProvider() {
        return fContentProvider;
    }

    protected DocumentLabelProvider getLabelProvider() {
        return fLabelProvider;
    }

    protected DocumentAnnotationProvider getAnnotationProvider() {
        return fAnnotationProvider;
    }

    private String createPendingContent( int lineCount, int oldOffset, int offset ) {
        int oldLineCount = getNumberOfLines() - 1;
        int intersectStart = Math.max( oldOffset, offset );
        int intersectEnd = Math.min( oldOffset + oldLineCount, offset + lineCount );
        int intersectCount = intersectEnd - intersectStart;
        StringBuilder sb = new StringBuilder();
        int line = 0;
        if ( oldOffset > offset ) { // scrolling up
            for ( int i = 0; i < oldOffset - offset; ++i ) {
                try {
                    addPosition( CATEGORY_LINE, new LinePosition( sb.length(), offset - i ) );
                    sb.append( PENDING_LINE ).append( '\n' );
                    ++line;
                }
                catch( BadLocationException e ) {
                    // shouldn't happen
                }
                catch( BadPositionCategoryException e ) {
                    // shouldn't happen
                }
            }
        }
        else { // scrolling down
            line += offset - oldOffset;
        }
        for ( int i = 0; i < intersectCount; ++i ) {
            try {
                IRegion region = getLineInformation( line++ );
                sb.append( get( region.getOffset(), region.getLength() ) ).append( '\n' );
            }
            catch( BadLocationException e ) {
                // shouldn't happen
            }
        }
        // Assuming the offset isn't changed when resizing
        int pendingLines = 0;
        if ( oldLineCount < lineCount ) { // resizing
            pendingLines = lineCount - oldLineCount;
        }
        else if ( offset > oldOffset ) { // scrolling down
            pendingLines = offset - oldOffset;
        }
        for ( int i = 0; i < pendingLines; ++i ) {
            sb.append( PENDING_LINE ).append( '\n' );
            ++line;
        }
        return sb.toString();
    }

    public int getCurrentOffset() {
        return fCurrentOffset;
    }

    public void setCurrentOffset( int offset ) {
        fCurrentOffset = offset;
    }

    public Object getElementAtLine( int line ) {
        return getContentProvider().getElementAtLine( line );
    }

    public void updateContent( int lineCount, int offset, boolean revealInput ) {
        int oldOffset = fCurrentOffset;
        fCurrentOffset = offset;
        removePositions();
        getAnnotationModel().removeAllAnnotations();
        set( createPendingContent( lineCount, oldOffset, offset ) );
        setPositions();
        getContentProvider().update( getPresentationContext(), lineCount, offset, revealInput );
    }

    protected void updateElement( Object input, int index, Object element ) {
        getLabelProvider().update( input, element, index, getPresentationContext() );
        getAnnotationProvider().update( getContentProvider().getInput(), element, index, getPresentationContext() );
    }

    @SuppressWarnings("unchecked")
    protected void updateAnnotations( int lineNumber, Annotation[] annotations ) {
        IAnnotationModel annotationModel = getAnnotationModel();
        try {
            Position[] positions = getPositions( CATEGORY_LINE );
            if ( lineNumber < positions.length ) {
                Iterator it = annotationModel.getAnnotationIterator();
                ArrayList<Annotation> oldAnnotations = new ArrayList<Annotation>( 3 );
                while( it.hasNext() ) {
                    Annotation ann = (Annotation)it.next();
                    if ( positions[lineNumber].equals( annotationModel.getPosition( ann ) ) ) {
                        oldAnnotations.add( ann );
                    }
                }
                for ( Annotation ann : oldAnnotations ) {
                    annotationModel.removeAnnotation( ann );
                }
                for ( Annotation ann : annotations ) {
                    annotationModel.addAnnotation( ann, positions[lineNumber] );
                }
            }
        }
        catch( BadPositionCategoryException e ) {
        }
    }

    final void labelDone( Object element, int lineNumber, Properties labels ) {
        try {
            String line = labels.getProperty( IDocumentPresentation.ATTR_LINE_LABEL );
            IRegion region = getLineInformation( lineNumber );
            if ( get( region.getOffset(), region.getLength() ).compareTo( line ) != 0 )
                replace( region.getOffset(), region.getLength(), line );
        }
        catch( BadLocationException e ) {
        }
    }

    protected void removeLine( int lineNumber ) {
        try {
            IRegion region = getLineInformation( lineNumber );
            replace( region.getOffset(), region.getLength(), "" ); //$NON-NLS-1$
        }
        catch( BadLocationException e ) {
        }
    }

    private void removePositions() {
        try {
            Position[] oldPositions = getPositions( CATEGORY_LINE );
            for ( Position p : oldPositions ) {
                removePosition( CATEGORY_LINE, p );
            }
        }
        catch( BadPositionCategoryException e ) {
        }
    }

    private void setPositions() {
        try {
            Position[] oldPositions = getPositions( CATEGORY_LINE );
            int offset = getCurrentOffset();
            int lines = getNumberOfLines();
            for ( Position p : oldPositions ) {
                removePosition( CATEGORY_LINE, p );
            }
            for ( int i = 0; i < lines; ++i ) {
                IRegion info = getLineInformation( i );
                addPosition( CATEGORY_LINE, new LinePosition( info.getOffset(), info.getLength(), offset + i ) );
            }
        }
        catch( BadPositionCategoryException e ) {
            // shouldn't happen
        }
        catch( BadLocationException e ) {
            // shouldn't happen
        }
    }
}
