/*
 *  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.directory.studio.ldifeditor.editor;


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import org.apache.directory.studio.ldapbrowser.core.BrowserCoreConstants;
import org.apache.directory.studio.ldifeditor.LdifEditorConstants;
import org.apache.directory.studio.ldifeditor.editor.text.LdifExternalAnnotationModel;
import org.apache.directory.studio.ldifparser.model.LdifFile;
import org.apache.directory.studio.ldifparser.model.container.LdifContainer;
import org.apache.directory.studio.ldifparser.model.container.LdifRecord;
import org.apache.directory.studio.ldifparser.parser.LdifParser;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.operation.IRunnableContext;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.AbstractDocumentProvider;


/**
 * This class implements the LDIF Document Provider.
 * This class is used to share a LDIF Document and listen on document modifications.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class LdifDocumentProvider extends AbstractDocumentProvider implements IDocumentListener
{

    private final LdifParser ldifParser;

    private final LdifDocumentSetupParticipant ldifDocumentSetupParticipant;

    private LdifFile ldifModel;


    /**
     * Creates a new instance of LdifDocumentProvider.
     */
    public LdifDocumentProvider()
    {
        super();
        this.ldifParser = new LdifParser();
        this.ldifDocumentSetupParticipant = new LdifDocumentSetupParticipant();
    }


    /**
     * {@inheritDoc}
     */
    public IDocument getDocument( Object element )
    {
        IDocument document = super.getDocument( element );
        return document;
    }


    /**
     * Gets the LDIF Model
     *
     * @return
     *      the LDIF Model
     */
    public LdifFile getLdifModel()
    {
        return ldifModel;
    }


    /**
     * {@inheritDoc}
     */
    public void documentAboutToBeChanged( DocumentEvent event )
    {
    }


    /**
     * Update the LDIF Model.
     */
    public void documentChanged( DocumentEvent event )
    {
        try
        {
            int changeOffset = event.getOffset();
            int replacedTextLength = event.getLength();
            int insertedTextLength = event.getText() != null ? event.getText().length() : 0;
            IDocument document = event.getDocument();
            // Region changeRegion = new Region(changeOffset,
            // replacedTextLength);
            Region changeRegion = new Region( changeOffset - BrowserCoreConstants.LINE_SEPARATOR.length(),
                replacedTextLength + ( 2 * BrowserCoreConstants.LINE_SEPARATOR.length() ) );

            // get containers to replace (from changeOffset till
            // changeOffset+replacedTextLength, check end of record)
            List<LdifContainer> oldContainerList = new ArrayList<LdifContainer>();
            List<LdifContainer> containers = ldifModel.getContainers();
            
            for ( int i = 0; i < containers.size(); i++ )
            {
                LdifContainer ldifContainer = containers.get( i );

                Region containerRegion = new Region( containers.get( i ).getOffset(), containers.get( i ).getLength() );

                boolean changeOffsetAtEOF = i == containers.size() - 1
                    && changeOffset >= containerRegion.getOffset() + containerRegion.getLength();

                if ( TextUtilities.overlaps( containerRegion, changeRegion ) || changeOffsetAtEOF )
                {
                    // remember index
                    int index = i;

                    // add invalid containers and non-records before overlap
                    i--;
                    for ( ; i >= 0; i-- )
                    {
                        ldifContainer = containers.get( i );
                        
                        if ( !ldifContainer.isValid() || !( ldifContainer instanceof LdifRecord ) )
                        {
                            oldContainerList.add( 0, ldifContainer );
                        }
                        else
                        {
                            break;
                        }
                    }

                    // add all overlapping containers
                    i = index;
                    for ( ; i < containers.size(); i++ )
                    {
                        ldifContainer = containers.get( i );
                        containerRegion = new Region( ldifContainer.getOffset(), ldifContainer.getLength() );
                        
                        if ( TextUtilities.overlaps( containerRegion, changeRegion ) || changeOffsetAtEOF )
                        {
                            oldContainerList.add( ldifContainer );
                        }
                        else
                        {
                            break;
                        }
                    }

                    // add invalid containers and non-records after overlap
                    for ( ; i < containers.size(); i++ )
                    {
                        ldifContainer = containers.get( i );

                        if ( !ldifContainer.isValid() || !( ldifContainer instanceof LdifRecord )
                            || !( oldContainerList.get( oldContainerList.size() - 1 ) instanceof LdifRecord ) )
                        {
                            oldContainerList.add( ldifContainer );
                        }
                        else
                        {
                            break;
                        }
                    }
                }
            }
            
            LdifContainer[] oldContainers = ( LdifContainer[] ) oldContainerList
                .toArray( new LdifContainer[oldContainerList.size()] );
            int oldCount = oldContainers.length;
            int oldOffset = oldCount > 0 ? oldContainers[0].getOffset() : 0;
            int oldLength = oldCount > 0 ? ( oldContainers[oldContainers.length - 1].getOffset()
                + oldContainers[oldContainers.length - 1].getLength() - oldContainers[0].getOffset() ) : 0;

            // get new content
            int newOffset = oldOffset;
            int newLength = oldLength - replacedTextLength + insertedTextLength;
            String textToParse = document.get( newOffset, newLength );

            // parse partion content to containers (offset=0)
            LdifFile newModel = this.ldifParser.parse( textToParse );
            List<LdifContainer> newContainers = newModel.getContainers();

            // replace old containers with new containers
            // must adjust offsets of all following containers in model
            ldifModel.replace( oldContainers, newContainers );

        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }

    }


    /**
     * Creates an LDIF annotation model.
     */
    protected IAnnotationModel createAnnotationModel( Object element ) throws CoreException
    {
        return new LdifExternalAnnotationModel();
    }


    /**
     * Tries to read the file pointed at by <code>input</code> if it is an
     * <code>IPathEditorInput</code>. If the file does not exist, <code>true</code>
     * is returned.
     *  
     * @param document the document to fill with the contents of <code>input</code>
     * @param input the editor input
     * @return <code>true</code> if setting the content was successful or no file exists, <code>false</code> otherwise
     * @throws CoreException if reading the file fails
     */
    private boolean setDocumentContent( IDocument document, IEditorInput input ) throws CoreException
    {
        // TODO: handle encoding
        Reader reader;
        try
        {
            String inputClassName = input.getClass().getName();
            if ( input instanceof IPathEditorInput )
            {
                reader = new FileReader( ( ( IPathEditorInput ) input ).getPath().toFile() );
            }
            else if ( inputClassName.equals( "org.eclipse.ui.internal.editors.text.JavaFileEditorInput" ) //$NON-NLS-1$
                || inputClassName.equals( "org.eclipse.ui.ide.FileStoreEditorInput" ) ) //$NON-NLS-1$
            // The class 'org.eclipse.ui.internal.editors.text.JavaFileEditorInput'
            // is used when opening a file from the menu File > Open... in Eclipse 3.2.x
            // The class 'org.eclipse.ui.ide.FileStoreEditorInput' is used when
            // opening a file from the menu File > Open... in Eclipse 3.3.x
            {
                reader = new FileReader( new File( input.getToolTipText() ) );
            }
            else
            {
                return false;
            }
        }
        catch ( FileNotFoundException e )
        {
            // return empty document and save later
            return true;
        }

        try
        {
            setDocumentContent( document, reader );
            return true;
        }
        catch ( IOException e )
        {
            throw new CoreException( new Status( IStatus.ERROR, LdifEditorConstants.PLUGIN_ID, IStatus.OK,
                "error reading file", e ) ); //$NON-NLS-1$
        }
    }


    /**
     * Reads in document content from a reader and fills <code>document</code>
     * 
     * @param document the document to fill
     * @param reader the source
     * @throws IOException if reading fails
     */
    private void setDocumentContent( IDocument document, Reader reader ) throws IOException
    {
        Reader in = new BufferedReader( reader );
        try
        {
            StringBuffer buffer = new StringBuffer( 512 );
            char[] readBuffer = new char[512];
            int n = in.read( readBuffer );
            while ( n > 0 )
            {
                buffer.append( readBuffer, 0, n );
                n = in.read( readBuffer );
            }

            document.set( buffer.toString() );

        }
        finally
        {
            in.close();
        }
    }


    /**
     * Set up the document: partitioning and incremental parser
     * 
     * @param document the new document
     */
    protected void setupDocument( IDocument document )
    {

        // setup document partitioning
        ldifDocumentSetupParticipant.setup( document );

        // initial parsing of whole document
        this.ldifModel = this.ldifParser.parse( document.get() );

        // add listener for incremental parsing
        document.addDocumentListener( this );

    }


    /**
     * Remove document listener.
     */
    protected void disposeElementInfo( Object element, ElementInfo info )
    {
        IDocument document = info.fDocument;
        document.removeDocumentListener( this );

        super.disposeElementInfo( element, info );
    }


    /**
     * {@inheritDoc}
     */
    protected IDocument createDocument( Object element ) throws CoreException
    {
        if ( element instanceof IEditorInput )
        {
            IDocument document = new Document();
            if ( setDocumentContent( document, ( IEditorInput ) element ) )
            {
                setupDocument( document );
            }
            return document;
        }

        return null;
    }


    /**
     * {@inheritDoc}
     */
    protected void doSaveDocument( IProgressMonitor monitor, Object element, IDocument document, boolean overwrite )
        throws CoreException
    {
        File file = null;
        String elementClassName = element.getClass().getName();
        if ( element instanceof FileEditorInput )
        // FileEditorInput class is used when the file is opened
        // from a project in the workspace.
        {
            writeDocumentContent( document, ( ( FileEditorInput ) element ).getFile(), monitor );
            return;
        }
        else if ( element instanceof IPathEditorInput )
        {
            IPathEditorInput pei = ( IPathEditorInput ) element;
            IPath path = pei.getPath();
            file = path.toFile();
        }
        else if ( elementClassName.equals( "org.eclipse.ui.internal.editors.text.JavaFileEditorInput" ) //$NON-NLS-1$
            || elementClassName.equals( "org.eclipse.ui.ide.FileStoreEditorInput" ) ) //$NON-NLS-1$
        // The class 'org.eclipse.ui.internal.editors.text.JavaFileEditorInput'
        // is used when opening a file from the menu File > Open... in Eclipse 3.2.x
        // The class 'org.eclipse.ui.ide.FileStoreEditorInput' is used when
        // opening a file from the menu File > Open... in Eclipse 3.3.x
        {
            file = new File( ( ( IEditorInput ) element ).getToolTipText() );
        }

        if ( file != null )
        {
            try
            {
                file.createNewFile();

                if ( file.exists() )
                {
                    if ( file.canWrite() )
                    {
                        Writer writer = new FileWriter( file );
                        writeDocumentContent( document, writer, monitor );
                    }
                    else
                    {
                        throw new CoreException( new Status( IStatus.ERROR,
                            "org.eclipse.ui.examples.rcp.texteditor", IStatus.OK, "file is read-only", null ) ); //$NON-NLS-1$ //$NON-NLS-2$
                    }
                }
                else
                {
                    throw new CoreException( new Status( IStatus.ERROR,
                        "org.eclipse.ui.examples.rcp.texteditor", IStatus.OK, "error creating file", null ) ); //$NON-NLS-1$ //$NON-NLS-2$
                }
            }
            catch ( IOException e )
            {
                throw new CoreException( new Status( IStatus.ERROR,
                    "org.eclipse.ui.examples.rcp.texteditor", IStatus.OK, "error when saving file", e ) ); //$NON-NLS-1$ //$NON-NLS-2$
            }

        }
    }


    /**
     * Saves the document contents to a stream.
     * 
     * @param document the document to save
     * @param file the file to save it to
     * @param monitor a progress monitor to report progress
     * @throws CoreException 
     * @throws IOException if writing fails
     */
    private void writeDocumentContent( IDocument document, IFile file, IProgressMonitor monitor ) throws CoreException
    {
        if ( file != null )
        {
            file.setContents( new ByteArrayInputStream( document.get().getBytes() ), true, true, monitor );
        }
    }


    /**
     * Saves the document contents to a stream.
     * 
     * @param document the document to save
     * @param writer the stream to save it to
     * @param monitor a progress monitor to report progress
     * @throws IOException if writing fails
     */
    private void writeDocumentContent( IDocument document, Writer writer, IProgressMonitor monitor ) throws IOException
    {
        Writer out = new BufferedWriter( writer );
        try
        {
            out.write( document.get() );
        }
        finally
        {
            out.close();
        }
    }


    /**
     * {@inheritDoc}
     */
    protected IRunnableContext getOperationRunner( IProgressMonitor monitor )
    {
        return null;
    }


    /**
     * {@inheritDoc}
     */
    public boolean isModifiable( Object element )
    {
        String elementClassName = element.getClass().getName();
        if ( element instanceof IPathEditorInput )
        {
            IPathEditorInput pei = ( IPathEditorInput ) element;
            File file = pei.getPath().toFile();
            return file.canWrite() || !file.exists(); // Allow to edit new files
        }
        else if ( elementClassName.equals( "org.eclipse.ui.internal.editors.text.JavaFileEditorInput" ) //$NON-NLS-1$
            || elementClassName.equals( "org.eclipse.ui.ide.FileStoreEditorInput" ) ) //$NON-NLS-1$
        // The class 'org.eclipse.ui.internal.editors.text.JavaFileEditorInput'
        // is used when opening a file from the menu File > Open... in Eclipse 3.2.x
        // The class 'org.eclipse.ui.ide.FileStoreEditorInput' is used when
        // opening a file from the menu File > Open... in Eclipse 3.3.x
        {
            File file = new File( ( ( IEditorInput ) element ).getToolTipText() );
            return file.canWrite() || !file.exists(); // Allow to edit new files
        }

        return false;
    }


    /**
     * {@inheritDoc}
     */
    public boolean isReadOnly( Object element )
    {
        return !isModifiable( element );
    }


    /**
     * {@inheritDoc}
     */
    public boolean isStateValidated( Object element )
    {
        return true;
    }
}