package gov.nasa.gsfc.seadas.ocsswrest.utilities;

import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.Map;

/**
 * A template reader replaces any occurences of <code>${<i>key</i>}</code> or <code>$<i>key</i></code>
 * in the underlying stream with the string representations of any non-null value returned by a given
 * resolver for that key.
 *
 * @author Norman Fomferra
 */
public class TemplateReader extends FilterReader {

    private static final char DEFAULT_KEY_INDICATOR = '$';
    private static final int EOF = -1;
    private Resolver resolver;
    private IntBuffer buffer;
    private char keyIndicator = DEFAULT_KEY_INDICATOR;

    /**
     * Constructs a template reader for the given reader stream and a resolver given by a {@link java.util.Map}.
     *
     * @param in  the underlying reader
     * @param map the map to serve as resolver
     */
    public TemplateReader(Reader in, Map map) {
        this(in, new KeyValueResolver(map));
    }

    /**
     * Constructs a template reader for the given reader stream and the given resolver.
     *
     * @param in       the underlying reader
     * @param resolver the resolver
     */
    public TemplateReader(Reader in, Resolver resolver) {
        super(in);
        if (resolver == null) {
            throw new NullPointerException("resolver");
        }
        this.resolver = resolver;
    }

    /**
     * Gets the key indicator.
     *
     * @return the key indicator, defaults to '$'.
     */
    public char getKeyIndicator() {
        return keyIndicator;
    }

    /**
     * Sets the key indicator.
     *
     * @param keyIndicator the key indicator, must not be a digit, letter or whitespace.
     */
    public void setKeyIndicator(char keyIndicator) {
        if (Character.isWhitespace(keyIndicator) || Character.isLetterOrDigit(keyIndicator)) {
            throw new IllegalArgumentException();
        }
        this.keyIndicator = keyIndicator;
    }

    /**
     * Reads all content.
     * @return the content
     * @throws java.io.IOException if an I/O error occurs
     */
    public String readAll() throws IOException {
        StringBuilder sb = new StringBuilder(16 * 1024);
        while (true) {
            int i = read();
            if (i == EOF) {
                break;
            }
            sb.append((char) i);
        }
        return sb.toString();
    }

    /**
     * Read a single character.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public int read() throws IOException {
        synchronized (lock) {

            if (buffer != null && buffer.ready()) {
                return buffer.next();
            }

            int c = in.read();
            if (c != keyIndicator) {
                return c;
            }

            if (buffer == null) {
                buffer = new IntBuffer();
            }
            buffer.reset();
            buffer.append(keyIndicator);

            c = readAndBuffer();
            if (c != EOF) {
                int keyType = 0;
                if (c == '{') {  // ${key}?
                    do {
                        c = readAndBuffer();
                        if (c == '}') {
                            keyType = 1;
                            break;
                        }
                    } while (c != EOF);
                } else if (Character.isJavaIdentifierStart(c)) { // $key?
                    keyType = 2;
                    do {
                        c = readAndBuffer();
                        if (!(Character.isJavaIdentifierPart(c) || c == '.')) {
                            break;
                        }
                    } while (c != EOF);
                }

                if (keyType != 0) {
                    String key;
                    if (keyType == 1) {  // ${key}
                        key = buffer.substring(2, buffer.length() - 1);
                    } else { // $key
                        key = buffer.substring(1, buffer.length() - 1);
                    }

                    Object value = resolver.resolve(key);
                    if (value != null) {
                        String s = value.toString();
                        int last;
                        if (keyType == 1) { // ${key}
                            last = in.read();
                            buffer.reset();
                            buffer.append(s);
                        } else { // $key
                            last = buffer.charAt(buffer.length() - 1);
                            buffer.reset();
                            buffer.append(s);
                        }
                        buffer.append(last); // last can also be EOF!
                    }
                }
            }
            return buffer.next();
        }
    }

    /**
     * Read characters into an array.  This method will block until some input
     * is available, an I/O error occurs, or the end of the stream is reached.
     *
     * @param cbuf Destination buffer
     * @return The number of characters read, or -1
     *         if the end of the stream
     *         has been reached
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public int read(char cbuf[]) throws IOException {
        return read(cbuf, 0, cbuf.length);
    }

    /**
     * Read characters into a portion of an array.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public int read(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            int i;
            for (i = 0; i < len; i++) {
                int c = read();
                if (c == EOF) {
                    return i == 0 ? EOF : i;
                }
                cbuf[off + i] = (char) c;
            }
            return i;
        }
    }

    /**
     * Attempts to read characters into the specified character buffer.
     * The buffer is used as a repository of characters as-is: the only
     * changes made are the results of a put operation. No flipping or
     * rewinding of the buffer is performed.
     *
     * @param target the buffer to read characters into
     * @return The number of characters added to the buffer, or
     *         -1 if this source of characters is at its end
     * @throws java.io.IOException  if an I/O error occurs
     * @throws NullPointerException if target is null
     * @throws java.nio.ReadOnlyBufferException
     *                              if target is a read only buffer
     */
    @Override
    public int read(CharBuffer target) throws IOException {
        synchronized (lock) {
            int len = target.remaining();
            char[] cbuf = new char[len];
            int n = read(cbuf, 0, len);
            if (n > 0) {
                target.put(cbuf, 0, n);
            }
            return n;
        }
    }

    /**
     * Skip characters.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public long skip(long n) throws IOException {
        synchronized (lock) {
            if (n < 0L) {
                throw new IllegalArgumentException("skip value is negative");
            }
            long i;
            for (i = 0; i < n; i++) {
                int c = read();
                if (c == EOF) {
                    break;
                }
            }
            return i;
        }
    }

    /**
     * Tell whether this stream is ready to be read.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public boolean ready() throws IOException {
        return (buffer != null && buffer.ready()) || in.ready();
    }

    /**
     * Tell whether this stream supports the mark() operation.
     */
    @Override
    public boolean markSupported() {
        return false;
    }

    /**
     * Mark the present position in the stream.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public void mark(int readAheadLimit) throws IOException {
        throw new IOException("mark() not supported");
    }

    /**
     * Reset the stream.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public void reset() throws IOException {
        throw new IOException("reset() not supported");
    }

    /**
     * Close the stream.
     *
     * @throws java.io.IOException If an I/O error occurs
     */
    @Override
    public void close() throws IOException {
        super.close();
        buffer = null;
    }

    /////////////////////////////////////////////////////////////////////////////

    private int readAndBuffer() throws IOException {
        int c = in.read();
        buffer.append(c);
        return c;
    }

    /////////////////////////////////////////////////////////////////////////////

    public interface Resolver {

        Object resolve(String reference);
    }


    private static class KeyValueResolver implements Resolver {

        private Map map;

        public KeyValueResolver(Map map) {
            if (map == null) {
                throw new NullPointerException("map");
            }
            this.map = map;
        }

        public Object resolve(String reference) {
            return map.get(reference);
        }
    }


    /**
     * A buffer to which EOF can also be appended.
     */
    private static class IntBuffer {

        private final static int INC = 8192;
        private int[] buffer;
        private int length;
        private int index;

        public IntBuffer() {
            this.buffer = new int[INC];
        }

        public void reset() {
            length = 0;
            index = 0;
        }

        public int next() {
            if (!ready()) {
                throw new IllegalStateException("!ready()");
            }
            return buffer[index++];
        }

        public int charAt(int pos) {
            if (pos < 0) {
                throw new IndexOutOfBoundsException("pos < 0");
            }
            if (pos >= length) {
                throw new IndexOutOfBoundsException("pos >= length");
            }
            return buffer[pos];
        }

        public void append(int c) {
            if (length >= buffer.length) {
                int[] newBuffer = new int[buffer.length + INC];
                System.arraycopy(buffer, 0, newBuffer, 0, length);
                buffer = newBuffer;
            }
            buffer[length++] = c;
        }

        public void append(String s) {
            for (int i = 0; i < s.length(); i++) {
                append((int) s.charAt(i));
            }
        }

        public boolean ready() {
            return index < length;
        }

        public String substring(int start, int end) {
            int n = end - start;
            char[] cbuf = new char[n];
            for (int i = start; i < end; i++) {
                cbuf[i - start] = (char) buffer[i];
            }
            return new String(cbuf);
        }

        public int length() {
            return length;
        }
    }
}
