/*
 * Copyright 2011 Paul Merlin.
 *
 * Licensed  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.qi4j.library.uowfile.internal;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
import org.qi4j.io.Inputs;
import org.qi4j.io.Outputs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UoWFile
{
    /* package */ static final Logger LOGGER = LoggerFactory.getLogger( "org.qi4j.library.uowfile" );
    private static final int FILE_BUFFER_SIZE = 4096;
    private static final AtomicLong COUNT = new AtomicLong( 0L );
    private final long originalIdentity;
    private final File original;
    private final File current;
    private final File backup;

    UoWFile( File original, File workDir )
    {
        this.originalIdentity = original.length() + original.lastModified();
        this.original = original;
        long count = COUNT.incrementAndGet();
        this.current = new File( workDir, original.getName() + ".current." + count );
        this.backup = new File( workDir, original.getName() + ".backup." + count );
    }

    public File asFile()
    {
        return current;
    }

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder().append( UoWFile.class.getSimpleName() );
        // UoWFile{parent/( original(oid->id) | current(id) | backup(id) )}
        sb.append( "{" ).append( original.getParentFile().getName() ).append( "/( " ).
            append( original.getName() ).append( "(" ).append( originalIdentity ).append( "->" ).append( fileTag( original ) ).append( ") | " ).
            append( current.getName() ).append( "(" ).append( fileTag( current ) ).append( ") | " ).
            append( backup.getName() ).append( "(" ).append( fileTag( backup ) ).
            append( ") )}" );
        return sb.toString();
    }

    void copyOriginalToCurrent()
    {
        if( original.exists() )
        {
            copy( original, current );
        }
    }

    void apply()
        throws ConcurrentUoWFileStateModificationException
    {
        LOGGER.trace( "Will apply changes to {}", this );
        if( fileTag( current ) != originalIdentity )
        {
            if( fileTag( original ) != originalIdentity )
            {
                LOGGER.info( "Concurrent modification, original creation identity is {} and original apply identity is {}", originalIdentity, fileTag( original ) );
                throw new ConcurrentUoWFileStateModificationException( this );
            }
            if( original.exists() )
            {
                move( original, backup );
            }
            if( current.exists() )
            {
                move( current, original );
            }
            LOGGER.debug( "Applied changes to {}", original );
        }
    }

    void rollback()
    {
        if( backup.exists() )
        {
            if( fileTag( original ) != originalIdentity )
            {
                delete( original );
                move( backup, original );
            }
            LOGGER.debug( "Restored backup to {}", original );
        }
    }

    void cleanup()
    {
        if( current.exists() )
        {
            delete( current );
        }
        if( backup.exists() )
        {
            delete( backup );
        }
    }

    /**
     * @return OL if the file does not exist
     */
    private long fileTag( File file )
    {
        return file.length() + file.lastModified();
    }

    private void copy( File source, File dest )
    {
        try
        {
            Inputs.byteBuffer( source, FILE_BUFFER_SIZE ).transferTo( Outputs.byteBuffer( dest ) );
        }
        catch( IOException ex )
        {
            throw new UoWFileException( ex );
        }
    }

    private void delete( File file )
    {
        if( !file.delete() )
        {
            throw new UoWFileException( new IOException( "Unable to delete file " + file ) );
        }
    }

    private void move( File source, File dest )
    {
        // Atomic move attempt
        if( !source.renameTo( dest ) )
        {
            // source and dest are probably on different filesystem, fallback to a non atomic copy/move operation
            copy( source, dest );
            if( !source.delete() )
            {
                throw new UoWFileException( new IOException( "Unable to delete source file " + source
                                                             + " after copy(move) to " + dest
                                                             + " (rename failed before that)." ) );
            }
            LOGGER.warn( "Moved {} to {} using a copy/delete operation instead of an atomic move. "
                         + "Are they on different filesystems?", source, dest );
        }
    }

}
