/*
 * Copyright (c) 2010, Rickard Öberg. All Rights Reserved.
 *
 * 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.io;

import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collection;
import java.util.zip.GZIPOutputStream;

/**
 * Utility methods for creating standard Outputs
 */
public class Outputs
{
    // START SNIPPET: method

    /**
     * Write lines to a text file with UTF-8 encoding. Separate each line with a newline ("\n" character). If the writing or sending fails,
     * the file is deleted.
     * <p>
     * If the filename ends with .gz, then the data is automatically GZipped.
     * </p>
     * @param file the file to save the text to
     *
     * @return an Output for storing text in a file
     */
    public static Output<String, IOException> text( final File file )
    // END SNIPPET: method
    {
        return text( file, "UTF-8" );
    }

    // START SNIPPET: method

    /**
     * Write lines to a text file. Separate each line with a newline ("\n" character). If the writing or sending fails,
     * the file is deleted.
     * <p>
     * If the filename ends with .gz, then the data is automatically GZipped.
     * </p>
     * @param file the file to save the text to
     *
     * @return an Output for storing text in a file
     */
    public static Output<String, IOException> text( final File file, final String encoding )
    // END SNIPPET: method
    {
        return new Output<String, IOException>()
        {
            @Override
            @SuppressWarnings( "unchecked" )
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends String, SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                File tmpFile = Files.createTemporayFileOf( file );

                OutputStream stream = new FileOutputStream( tmpFile );

                // If file should be gzipped, do that automatically
                if( file.getName().endsWith( ".gz" ) )
                {
                    stream = new GZIPOutputStream( stream );
                }

                final BufferedWriter writer = new BufferedWriter( new OutputStreamWriter( stream, encoding ) );

                try
                {
                    sender.sendTo( new Receiver<String, IOException>()
                    {
                        @Override
                        public void receive( String item )
                            throws IOException
                        {
                            writer.append( item ).append( '\n' );
                        }
                    } );
                    writer.close();

                    // Replace file with temporary file
                    if( !file.exists() || file.delete() )
                    {
                        if( ! tmpFile.renameTo( file ) )
                        {
                            // TODO: What?? Throw an Exception?
                            System.err.println( "Unable to rename file: " + tmpFile + " to " + file );
                        }
                    }
                }
                catch( IOException e )
                {
                    // We failed writing - close and delete
                    writer.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }
                }
                catch( Throwable senderThrowableType )
                {
                    // We failed writing - close and delete
                    writer.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }
                    throw (SenderThrowableType) senderThrowableType;
                }
            }
        };
    }

    // START SNIPPET: method

    /**
     * Write lines to a Writer. Separate each line with a newline ("\n" character).
     *
     * @param writer the Writer to write the text to
     * @return an Output for storing text in a Writer
     */
    public static Output<String, IOException> text( final Writer writer )
    // END SNIPPET: method
    {
        return new Output<String, IOException>()
        {

            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends String, SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                sender.sendTo( new Receiver<String, IOException>()
                {

                    @Override
                    public void receive( String item )
                        throws IOException
                    {
                        writer.append( item ).append( "\n" );
                    }

                } );
            }

        };
    }

    // START SNIPPET: method

    /**
     * Write lines to a StringBuilder. Separate each line with a newline ("\n" character).
     *
     * @param builder the StringBuilder to append the text to
     * @return an Output for storing text in a StringBuilder
     */
    public static Output<String, IOException> text( final StringBuilder builder )
    // END SNIPPET: method
    {
        return new Output<String, IOException>()
        {

            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends String, SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                sender.sendTo( new Receiver<String, IOException>()
                {

                    @Override
                    public void receive( String item )
                        throws IOException
                    {
                        builder.append( item ).append( "\n" );
                    }

                } );
            }

        };
    }

    // START SNIPPET: method

    /**
     * Write ByteBuffer data to a file. If the writing or sending of data fails the file will be deleted.
     *
     * @param file The destination file.
     *
     * @return The Output ByteBuffer instance backed by a File.
     */
    public static Output<ByteBuffer, IOException> byteBuffer( final File file )
    // END SNIPPET: method
    {
        return new Output<ByteBuffer, IOException>()
        {
            @Override
            @SuppressWarnings( "unchecked" )
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends ByteBuffer, SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                File tmpFile = Files.createTemporayFileOf( file );
                FileOutputStream stream = new FileOutputStream( tmpFile );
                final FileChannel fco = stream.getChannel();

                try
                {
                    sender.sendTo( new Receiver<ByteBuffer, IOException>()
                    {
                        @Override
                        public void receive( ByteBuffer item )
                            throws IOException
                        {
                            fco.write( item );
                        }
                    } );
                    stream.close();

                    // Replace file with temporary file
                    if( !file.exists() || file.delete() )
                    {
                        if( ! tmpFile.renameTo( file ) )
                        {
                            // TODO: What can be done in this case?
                            System.err.println( "Unable to rename file: " + tmpFile + " to " + file );
                        }
                    }
                }
                catch( IOException e )
                {
                    // We failed writing - close and delete
                    stream.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }

                }
                catch( Throwable senderThrowableType )
                {
                    // We failed writing - close and delete
                    stream.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }
                    throw (SenderThrowableType) senderThrowableType;
                }
            }
        };
    }

    // START SNIPPET: method

    /**
     * Write ByteBuffer data to an OutputStream.
     *
     * @param stream Destination OutputStream
     *
     * @return The Output of ByteBuffer that will be backed by the OutputStream.
     */
    public static Output<ByteBuffer, IOException> byteBuffer( final OutputStream stream )
    // END SNIPPET: method
    {
        return new Output<ByteBuffer, IOException>()
        {
            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends ByteBuffer, SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                try
                {
                    sender.sendTo( new Receiver<ByteBuffer, IOException>()
                    {
                        @Override
                        public void receive( ByteBuffer item )
                            throws IOException
                        {
                            if( item.hasArray() )
                            {
                                stream.write( item.array(), item.arrayOffset(), item.limit() );
                            }
                            else
                            {
                                for( int i = 0; i < item.limit(); i++ )
                                {
                                    stream.write( item.get( i ) );
                                }
                            }
                        }
                    } );
                }
                finally
                {
                    stream.close();
                }
            }
        };
    }

    // START SNIPPET: method

    /**
     * Write byte array data to a file. If the writing or sending of data fails the file will be deleted.
     *
     * @param file The File to be written to.
     * @param bufferSize The size of the ByteBuffer.
     *
     * @return An Output instance that will write to the given File.
     */
    public static Output<byte[], IOException> bytes( final File file, final int bufferSize )
    // END SNIPPET: method
    {
        return new Output<byte[], IOException>()
        {
            @Override
            @SuppressWarnings( "unchecked" )
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends byte[], SenderThrowableType> sender )
                throws IOException, SenderThrowableType
            {
                File tmpFile = Files.createTemporayFileOf( file );
                final OutputStream stream = new BufferedOutputStream( new FileOutputStream( tmpFile ), bufferSize );

                try
                {
                    sender.sendTo( new Receiver<byte[], IOException>()
                    {
                        @Override
                        public void receive( byte[] item )
                            throws IOException
                        {
                            stream.write( item );
                        }
                    } );
                    stream.close();

                    // Replace file with temporary file
                    if( !file.exists() || file.delete() )
                    {
                        if( ! tmpFile.renameTo( file ) )
                        {
                            // TODO: WHAT???
                            System.err.println( "Unable to rename " + tmpFile + " to " + file );
                        }
                    }
                }
                catch( IOException e )
                {
                    // We failed writing - close and delete
                    stream.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }
                }
                catch( Throwable senderThrowableType )
                {
                    // We failed writing - close and delete
                    stream.close();
                    if( ! tmpFile.delete() )
                    {
                        System.err.println("Unable to delete temporary file." );
                        tmpFile.deleteOnExit();
                    }
                    throw (SenderThrowableType) senderThrowableType;
                }
            }
        };
    }

    // START SNIPPET: method

    /**
     * Do nothing. Use this if you have all logic in filters and/or specifications
     *
     * @param <T> The item type.
     *
     * @return An Output instance that ignores all data.
     */
    public static <T> Output<T, RuntimeException> noop()
    // END SNIPPET: method
    {
        return withReceiver( new Receiver<T, RuntimeException>()
        {
            @Override
            public void receive( T item )
                throws RuntimeException
            {
                // Do nothing
            }
        } );
    }

    // START SNIPPET: method

    /**
     * Use given receiver as Output. Use this if there is no need to create a "transaction" for each transfer, and no need
     * to do batch writes or similar.
     *
     * @param <T> The item type
     * @param receiver receiver for this Output
     *
     * @return An Output instance backed by a Receiver of items.
     */
    public static <T, ReceiverThrowableType extends Throwable> Output<T, ReceiverThrowableType> withReceiver( final Receiver<T, ReceiverThrowableType> receiver )
    // END SNIPPET: method
    {
        return new Output<T, ReceiverThrowableType>()
        {
            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends T, SenderThrowableType> sender )
                throws ReceiverThrowableType, SenderThrowableType
            {
                sender.sendTo( receiver );
            }
        };
    }

    // START SNIPPET: method

    /**
     * Write objects to System.out.println.
     *
     * @return An Output instance that is backed by System.out
     */
    public static Output<Object, RuntimeException> systemOut()
    // END SNIPPET: method
    {
        return new Output<Object, RuntimeException>()
        {
            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<?, SenderThrowableType> sender )
                throws RuntimeException, SenderThrowableType
            {
                sender.sendTo( new Receiver<Object, RuntimeException>()
                {
                    @Override
                    public void receive( Object item )
                    {
                        System.out.println( item );
                    }
                } );
            }
        };
    }

    // START SNIPPET: method

    /**
     * Write objects to System.err.println.
     *
     * @return An Output instance backed by System.in
     */
    @SuppressWarnings( "UnusedDeclaration" )
    public static Output<Object, RuntimeException> systemErr()
    // END SNIPPET: method
    {
        return new Output<Object, RuntimeException>()
        {
            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<?, SenderThrowableType> sender )
                throws RuntimeException, SenderThrowableType
            {
                sender.sendTo( new Receiver<Object, RuntimeException>()
                {
                    @Override
                    public void receive( Object item )
                    {
                        System.err.println( item );
                    }
                } );
            }
        };
    }

    // START SNIPPET: method

    /**
     * Add items to a collection
     */
    public static <T> Output<T, RuntimeException> collection( final Collection<T> collection )
    // END SNIPPET: method
    {
        return new Output<T, RuntimeException>()
        {
            @Override
            public <SenderThrowableType extends Throwable> void receiveFrom( Sender<? extends T, SenderThrowableType> sender )
                throws RuntimeException, SenderThrowableType
            {
                sender.sendTo( new Receiver<T, RuntimeException>()
                {
                    @Override
                    public void receive( T item )
                        throws RuntimeException
                    {
                        collection.add( item );
                    }
                } );
            }
        };
    }

    private Outputs()
    {
    }
}
