/*
 * 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.netbeans.modules.search.matcher;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.MalformedInputException;
import java.nio.charset.UnmappableCharacterException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.search.provider.SearchListener;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;

/**
 * The {@code BufferedCharSequence} class provides the {@code CharSequence}
 * interface for a text file.
 * <p>
 * <b>Important notes:</b>
 * <ul>
 *   <li> This implementation doesn't support files with a size more than 2GB,
 *        i.e. {@code Integer.MAX_VALUE} bytes.</li>
 *   <li> This implementation may provide incorrect results if underlying file
 *        will be changed since instantiation of this class until invocation of
 *        the method {@link BufferedCharSequence.close() }.</li>
 *   <li> Any method of this class may throw the run-time exception
 *        {@link BufferedCharSequence.SourceIOException} if the
 *        {@link IOException} will happen during I/O operation.</li>
 * </ul>
 * </p>
 *
 * @author Victor G. Vasilyev mailto:vvg@netbeans.org
 * @version 1.1
 */
public class BufferedCharSequence implements CharSequence {

    // TODO Counting lines. See a cycle in the method
    // BasicSearchCriteria.getTextDetails(BufferedCharSequence,
    //                                    DataObject, SearchPattern)
    
    private static final Logger LOG = Logger.getLogger(
            "org.netbeans.modules.search.BufferedCharSequence"); //NOI18N

    /** K = 2**10 = 1024 */
    public static final int K = 1024;

    // Implementation limits
    /**
     * Max file size that can be processed by this implementation.
     */
    public static final int MAX_FILE_SIZE = Integer.MAX_VALUE;

    // Default values
    /**
     * Max capacity of the source buffer that will be used by default.
     *
     * @see #setMaxBufferSize(int) 
     */
    private static final int MAX_SOURCE_BUFFER_SIZE = 16 * K;
    /**
     * Max subsequence length that will be processed by this implementation by
     * default.
     */
    public static final int MAX_SUBSEQUENCE_LENGTH = Integer.getInteger(
            "org.netbeans.modules.search.matcher.BufferedCharSequence"  //NOI18N
                    + ".MAX_SUBSEQUENCE_LENGTH", 8 * K);                //NOI18N
    /**
     * Min sink buffer size that will be used.
     */
    private static final int MIN_SINK_BUFFER_SIZE = 16;

    /**
     * Definitions of the Unicode Line Terminators.
     */
    interface UnicodeLineTerminator {
        /** Unicode line feed (0x000A) */
        char LF = '\n'; //NOI18N
        /** Unicode carriage return (0x000D) */
        char CR = '\r'; //NOI18N
        /** Unicode line separator (0x2028) */
        char LS = 0x2028;
        /** Unicode paragraph separator (0x2029) */
        char PS = 0x2029;
        /** Unicode next line (0x0085) */
        char NEL = 0x0085;
    }

    // Internal state
    private Source source;
    private Sink sink;
    private final CharsetDecoder decoder;
    private SearchListener listener;
    private long maxReadOffset = 0;
    private CoderResult coderResult;
    private boolean isClosed = false;
    private volatile boolean isTerminated = false;
    private int position = 0; // Invariants: position <= length

    /**
     * Creates {@code BufferedCharSequence} for the specified {@code stream}.
     *
     * @param size size of the file.
     * @deprecated Can cause OutOfMemoryError for big files.
     */
    @Deprecated
    public BufferedCharSequence(final InputStream stream,
                                CharsetDecoder decoder, long size) {
        this.source = new Source(stream, size);
        this.decoder = decoder;
        this.sink = new Sink(this.source);
    }

    /**
     * Creates {@code BufferedCharSequence} for specified FileObject {@code fo}.
     *
     * @param fo FileObject containing character data.
     * @param decoder Charset decoder.
     * @param size file size.
     */
    public BufferedCharSequence(final FileObject fo,
            CharsetDecoder decoder, long size) {
        this.source = new Source(fo, size);
        this.decoder = decoder;
        this.sink = new Sink(this.source);
        LOG.log(Level.FINER, "<init> {0}; decoder = {1}; {2}", // NOI18N
                new Object[]{this.source, this.decoder, this.sink});
    }

    /**
     * The copying constructor.
     * @param bufferedCharSequence the {@code BuBufferedCharSequence} instance
     * that will be copied.
     */
    private BufferedCharSequence(BufferedCharSequence bufferedCharSequence) {
        this.source = bufferedCharSequence.source;
        this.sink = bufferedCharSequence.sink;
        this.decoder = bufferedCharSequence.decoder;
    }

    /**
     * Ensures that the {@link #close()} method is called when there are no more
     * references to the instance of this class.
     *
     * @exception  IOException if an I/O error occurs.
     * @see        BufferedCharSequence#close()
     */
    @Override
    @SuppressWarnings("FinalizeDeclaration")
    protected void finalize() throws Throwable {
        try {
            close();
        } finally {
            super.finalize();
        }
    }
    /**
     * Gets maximal size of the source buffer.
     * @return the size of the source buffer.
     */
    public int getMaxBufferSize() {
        return source.maxBufferSize;
    }

    /**
     * Sets maximal size of the source buffer.
     * @param maxBufferSize the size (must be equal or greater than 1).
     * @throws IllegalArgumentException if the specified size is less than 1.
     */
    public void setMaxBufferSize(int maxBufferSize) {
        if(maxBufferSize < 1) throw new IllegalArgumentException();
        source.maxBufferSize = maxBufferSize;
    }

    public void setSearchListener(SearchListener listener) {
        this.listener = listener;
    }

    /**
     * Resets all resources that support {@link CharSequence} interface.
     * Current position is not changed.
     * 
     * @return the underlying instance of this class.
     * @throws SourceIOException
     */
    public BufferedCharSequence reset() throws SourceIOException {        
        source.reset();
        sink.reset();
        decoder.reset();
        coderResult = CoderResult.UNDERFLOW;
        return this;
    }

    /**
     * Provides to stop use of underlying instance of this class and releases 
     * all involved resources.
     * @return the underlying instance of this class.
     * @throws IOException if an I/O error occurs.
     */
    public synchronized BufferedCharSequence close() throws IOException {
        if (!isClosed) {
            reset();
            source.close();
            source = null;
            sink = null;
            coderResult = null;
            listener = null;
            isClosed = true;
        }
        return this;
    }

    /**
     * Terminate this sequence.
     *
     * Subsequent invocations of charAt will raise TerminatedException.
     */
    void terminate() {
        this.isTerminated = true;
    }

    /**
     * Creates a new {@code BufferedCharSequence} that shares this char
     * sequence's content.
     * 
     * @return The new {@code BufferedCharSequence}
     */
    public BufferedCharSequence duplicate() {
        checkState();
        return new BufferedCharSequence(this);
    }

    // CharSequence interface

    @Override
    public int length() {
        checkState();
        reset();
        int length = 0;
        try {
            while(sink.next()) {  }
        } catch (CharacterCodingException ex) {
            throw new SourceIOException(ex);
        }
        length = sink.buffer.scope.end;        
        return length;
    }

    @Override
    public char charAt(int index) throws IndexOutOfBoundsException {
        if (isTerminated) {
            throw new TerminatedException();
        }
        checkState();
        String errMsg = check(index);
        if(errMsg != null) {
            throw new IndexOutOfBoundsException(errMsg);
        }
        // We should try before to decide to throw IOBE if index > length.
        return getCharAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end)
            throws IndexOutOfBoundsException {
        checkState();
        String errMsg = check(start, end);
        if(errMsg != null) {
            throw new IndexOutOfBoundsException(errMsg);
        }
        // We should try before to decide to throw IOBE if end > length.
        // So, we won't check it here.
        int subSequenceLength = getLength(start, end);
        if(subSequenceLength > MAX_SUBSEQUENCE_LENGTH) {
            throw new IndexOutOfBoundsException(
                    "requested subSequence has a big length (" +
                    subSequenceLength + ") > " +
                    MAX_SUBSEQUENCE_LENGTH); // NOI18N
        }
        CharSequence subSequence = getSubSequence(start, end);
        return subSequence;
    }

    @Override
    public String toString() {
        checkState();
        return subSequence(0, length()).toString();
    }

    // End of the CharSequence interface

    /**
     * Returns the current position.
     * @return the current position.
     */
    public final int position() {
        checkState();
	return position;
    }

    /**
     * Sets the specified position.
     * @param newPosition new position.
     * @return old position.
     */
    public final int changePosition(int newPosition) {
        int oldPosition = position;
        position = newPosition;
	return oldPosition;
    }

    /**
     * Gets a char at the currect position and increments the position.
     * @return a char.
     * @throws IndexOutOfBoundsException if EOF is reached.
     */
    public char nextChar() throws IndexOutOfBoundsException {
        checkState();
        return charAt(position++);
    }

    /**
     * Sets position to 0.
     * @return the underlying instance of this class.
     */
    public final BufferedCharSequence rewind() {
        checkState();
	position = 0;
	return this;
    }

    /**
     * Returns a line started from the current position. Sets the position at
     * the start of the next line.
     *
     * @return text from the current position up to the first occurrence of any
     * <ul>
     *   <li>'\n' - Unicode line feed (0x000A)</li>
     *   <li>'\r' - Unicode carriage return (0x000D)</li>
     *   <li>"\r\n" - A sequence of Unicode line feed (0x000A) and 
     *                Unicode carriage return (0x000D)</li>
     *   <li>Unicode line separator (0x2028)</li>
     *   <li>Unicode paragraph separator (0x2029)</li>
     * </ul>
     */
    public String nextLineText() {
        StringBuilder sb = new StringBuilder();
        try { // try-catch avoids use length() in required expression
              // like as position < length() in the while condition.
            while (true) {
                char c = nextChar();
                switch (c) {
                    case UnicodeLineTerminator.LF:
                    case UnicodeLineTerminator.PS:
                    case UnicodeLineTerminator.LS:
                    case UnicodeLineTerminator.NEL:
                        return sb.toString();
                    case UnicodeLineTerminator.CR:
                        if (charAt(position) == UnicodeLineTerminator.LF) {
                            // process "\r\n".
                            nextChar();
                        }
                        return sb.toString();
                    default:
                        sb.append(c);
                }
            }
        } catch (IndexOutOfBoundsException ioobe) {
            // It is OK. It means that EOF is reached.
        }
        return sb.toString();
    }

    /**
     * Returns a line started from the current position.
     * The current position is not changed. This method is based on the method
     * {@link #nextLineText()}.
     *
     * @param start the start offset of the text line.
     * @return a text of the line.
     */
    public String getLineText(int start) {
        int oldPosition = changePosition(start);
        String text = nextLineText();        
        changePosition(oldPosition);
        return text;
    }

    private char getCharAt(int index) throws IndexOutOfBoundsException {        
        if(sink.buffer.scope.isBefore(index)) {            
            reset();
        }
        while(!sink.buffer.scope.isInside(index)) {
            boolean hasNext;
            try {
                hasNext = sink.next();
            } catch (CharacterCodingException e) {
                throw new SourceIOException(e);
            }
            if (listener != null && sink.buffer.scope.start > maxReadOffset) {
                maxReadOffset = sink.buffer.scope.start;
                listener.fileContentMatchingProgress(source.fo.getPath(),
                        maxReadOffset);
            }
            if(!hasNext) {
                throw new IndexOutOfBoundsException("index is " +
                        index + " > lenght"); // NOI18N
            }
        }        
        return sink.charAt(index);
    }

    private CharSequence getSubSequence(int start, int end) 
                                              throws IndexOutOfBoundsException {
        StringBuilder sb = new StringBuilder(getLength(start, end));
        for(int i = start; i < end; i++) {
            sb.append(charAt(i));
        }
        return sb.toString();
    }

    private int getLength(int start, int end) {
        return end - start;
    }

    private void checkState() {
        if(isClosed) {
            String msg = "BufferedCharSequence is closed";  // NOI18N
            throw new IllegalStateException(msg);
        }
    }

    private String check(int index) {
        if(index < 0) {
            return "index = " + index; // NOI18N
        }
        return null; // OK
    }

    private String check(int start, int end) {
        if(start < 0 || end < 0 || start > end) {
            return "start = " + start + ", end = " + end; // NOI18N
        }
        return null; // OK
    }


    /**
     * The source buffer.
     */
       private class Source {
        private int maxBufferSize = MAX_SOURCE_BUFFER_SIZE;

        private FileObject fo = null;
        private ByteBuffer buffer;
        private InputStream istream;
        private BufferedInputStream bstream;
        private int bufferSize;
        
        @Deprecated
        public Source(InputStream inputStream, long bufferSize) {
            this.bstream = new BufferedInputStream(inputStream);
            this.bstream.mark(Integer.MAX_VALUE);
            this.bufferSize = getBufferSize(bufferSize);
            buffer = newBuffer();
            buffer.position(buffer.limit());
        }

        public Source(FileObject fo, long bufferSize) {
            this.fo = fo;
            this.bufferSize = getBufferSize(bufferSize);
            initStreams();
            buffer = newBuffer();
            buffer.position(buffer.limit());
        }

        @Override
        public String toString() {
            return "source=[stream = " + bstream.toString() + ", buffer = " + buffer + "]";
        }

        private ByteBuffer newBuffer() {
            return ByteBuffer.allocate(bufferSize);
        }

       public void reset() {
            try {
                if (fo == null) {
                    bstream.reset(); // the deprecated way
                } else {
                    closeStreams();
                    initStreams();
                }
            } catch (IOException ex) {
                throw new SourceIOException(ex);
            }
            buffer.clear();
            buffer.position(buffer.limit());
        }

        /**
         * Reads a sequence of bytes from the source stream. Bytes are read
         * starting at the stream's current position, and then the
         * position is updated with the number of bytes actually read.
         *
         * @return The number of bytes read, possibly zero, or -1 if 
         * the end-of-stream is reached
         *
         * @throws ProcessException If some other I/O error occurs
         */
        private int read() {
            try {
                if(buffer.hasArray()) {
                    if (buffer.capacity() == 0) {
                        return -1;
                    }
                    int res = bstream.read(buffer.array(), buffer.position(), buffer.remaining());
                    if(res > 0) {
                        buffer.position(res + buffer.position());
                    }
                    return res;
                }
                throw new IOException("No byte array");
            } catch (IOException ex) {
                throw new SourceIOException(ex);
            }
        }

        public void close() throws IOException {
            closeStreams();
        }

        /**
        * Initialize input stream and buffered input stream.
        */
        private void initStreams() {
            try {
                istream = fo.getInputStream();
                try {
                    bstream = new BufferedInputStream(istream, bufferSize);
                } catch (Throwable t) {
                    if (istream != null) {
                        istream.close();
                    }
                    throw t;
                }
            } catch (Throwable t) {
                throw new SourceIOException(new IOException(t));
            }
        }

        /** Close input streams. */
        private void closeStreams() throws IOException {
            IOException t = null;
            try {
                if (bstream != null) {
                    bstream.close();
                    bstream = null;
                }
            } catch (IOException e) {
                t = e;
            }
            if (istream != null) {
                istream.close();
                istream = null;
            }
            if (t != null) {
                throw t;
            }
        }

        public int getSize(long size) {
            if (size > Integer.MAX_VALUE) {
                LOG.warning("File size is " + size + "bytes. " +
                            "Only first " + MAX_FILE_SIZE +
                            " bytes will be processed.");
                return MAX_FILE_SIZE;
            }
            return (int) size;
        }

        private int getBufferSize(long bufferSize) {
            int size = Math.min(getSize(bufferSize), maxBufferSize);
            return size;
        }

        /**
         *
         * @return {@code true} if EOF, otherwise {@code false}.
         */
        public boolean readNext() {
            buffer.compact();
            int status = read();            
            buffer.flip();            
            return status == -1;
        }

        private int getCapacity() {
            return buffer.capacity();
        }

    } // Source
    /**
     * The sink buffer.
     */

    private class Sink {

        private Buffer buffer;
        private boolean wasEndOfInput;

        public Sink(Source source) {
            int sourceCapacity = source.getCapacity();
            this.buffer = newBuffer(sourceCapacity);
        }

        @Override
        public String toString() {
            return "sink = [" + buffer + "]";
        }

        public void reset() {            
            wasEndOfInput = false;
            buffer.reset();
        }

        public char charAt(int index) throws IndexOutOfBoundsException {
            assert index >= 0;
            return buffer.getCharAt(index);
        }

        /**
         * Obtains a next decoded portion of data. This method implements a
         * <a href=
*"http://java.sun.com/javase/6/docs/api/java/nio/charset/CharsetDecoder.html#steps"
         * >decoding operation</a>.
         * @return {@code true} is successful, otherwise {@code false}.
         */
        private boolean next() throws CharacterCodingException {
            if (wasEndOfInput) {
                return false;
            }
            
            CharBuffer out = buffer.clear();           
            boolean endOfInput = wasEndOfInput;
            
            do {
                if(coderResult == CoderResult.UNDERFLOW) {
                    endOfInput = source.readNext();
                }
                coderResult = decoder.decode(source.buffer, out, endOfInput);
                if (coderResult.isOverflow()) {
                    out = buffer.growBuffer();
                }
                // loop if underflow is reported, EOF is not reached && no chars
                // produced, otherwise our logic could be damaged
                checkError(coderResult);
            } while (out.position() == 0 && coderResult.isUnderflow() && !endOfInput);
            
            if(endOfInput) {
                while((coderResult = decoder.flush(out))
                        == CoderResult.OVERFLOW) {
                    out = buffer.growBuffer();
                }
                checkError(coderResult);
            }
            buffer.adjustScope();
            wasEndOfInput = endOfInput;
            return !buffer.scope.isEmpty();
        }

        private void checkError(CoderResult result)
                throws CharacterCodingException {
            if (result.isError()) {
                result.throwException();
            }
        }

        private Buffer newBuffer(int sourceCapacity) {
            return new Buffer(sourceCapacity);
        }

        /**
         * A character buffer.
         * @see CharBuffer
         * @see Scope
         */
        private class Buffer {

            private CharBuffer charBuffer;
            private Scope scope = new Scope();

            public Buffer(int sourceCapacity) {
                allocate(sourceCapacity);
                reset();
            }

            @Override
            public String toString() {
                return "buffer[capacity = " + charBuffer.capacity() + "]";
            }

            /**
             * Makes the buffer capacity two times more.
             * @return the buffer.
             */
            public CharBuffer growBuffer() {
                int capacity = charBuffer.capacity();
                CharBuffer o = CharBuffer.allocate(capacity << 1);
                charBuffer.flip();
                o.put(charBuffer);
                charBuffer = o;
                LOG.finer("The sink char buffer capacity has been grown: " +
                        capacity + " -> " + charBuffer.capacity());
                return charBuffer;
            }

            private void allocate(int sourceCapacity) {
                if (sourceCapacity == 0) {
                    charBuffer = CharBuffer.allocate(0);
                    return;
                }
                int bufferSize = bufferSize(sourceCapacity);
                charBuffer = CharBuffer.allocate(bufferSize);
            }

            private int bufferSize(int sourceCapacity) {
                 int n = (int) (sourceCapacity * decoder.averageCharsPerByte());
                 return n < MIN_SINK_BUFFER_SIZE ? MIN_SINK_BUFFER_SIZE : n;
            }

            private char getCharAt(int index) {
                int position = charBuffer.position();
                charBuffer.position(0);
                char c = charBuffer.charAt(index - scope.start);
                charBuffer.position(position);
                return c;
            }

            private void reset() {               
                scope.reset();
                charBuffer.clear();
            }

            private void flip() {
                charBuffer.flip();
            }

            private CharBuffer clear() {
                charBuffer.clear();
                return charBuffer;
            }

            private void adjustScope() {             
                scope.start = scope.end == -1 ? 0 : scope.end;              
                flip();                                                 
                scope.end = scope.start + charBuffer.limit();                              
            }

            /**
             * Scope of the {@link Buffer}.
             */
            private class Scope {

                public static final int EOF = -1;

                private int start;
                private int end;

                /**
                 * Determines if the specified {@code index} points inside the
                 * scope.
                 *
                 * @param index the index being tested.
                 *
                 * @return {@code true} if {@code index} points inside the
                 * scope, otherwise {@code false}.
                 */
                public boolean isInside(int index) {
                    return index >= start && index < end;
                }

                /**
                 * Determines if the specified {@code index} points before the
                 * scope.
                 *
                 * @param index the index being tested.
                 *
                 * @return {@code true} if {@code index} &lt; {@code start} or
                 * the scope is not initialized yet, otherwize {@code false}.
                 */
                public boolean isBefore(int index) {
                    return start == EOF || index < start;
                }

                /**
                 * Sets both start and end of the {@code Scope} to the
                 * {@link BufferedCharSequence.​Sink.​Buffer.​Scope#EOF}.
                 */
                public void reset() {
                    start = end = EOF;
                }

                /**
                 * Checks whether the {@code Scope} is empty.
                 * @return {@code true} if empty (i.e {@code start == end}), 
                 * otherwise {@code false}.
                 */
                private boolean isEmpty() {
                    return start == end;
                }

            } // Scope

        } // Buffer

    } // Sink
    /**
     * Signals that an I/O exception of some sort has occurred.
     * Instance of this class wraps a cause {@link IOException} that is the 
     * checked exception to represent it as a unchecked exception.
     */
    public static class SourceIOException extends RuntimeException {

        /**
         * Creates new {@code SourceIOException}.
         * @param cause a cause {@link IOException}.
         */
        public SourceIOException(IOException cause) {
            super(cause);
        }

    } // SourceIOException

    /**
     * Exception thrown when reading characters from an already terminated
     * character sequence.
     */
    static class TerminatedException extends RuntimeException {

        public TerminatedException() {
        }
    }
} // BufferedCharSequence
