/*
 * 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.openjpa.persistence.jest;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.util.Arrays;

/**
 * Reads from an input stream and writes to an output stream after replacing matched tokens
 * by their counterpart.
 *
 *
 * @author Pinaki Poddar
 *
 */
public class TokenReplacedStream {
    /**
     * Read the given input stream and replaces the tokens as it reads. The replaced stream is written to the
     * given output stream.
     *
     * @param in a non-null input stream
     * @param out a character oriented writer
     * @param replacements an even number of Strings. Any occurrence of the even-indexed i-th String in the
     * input stream will be replaced by the (i+1)-th String in the output writer.
     */
    public void replace(InputStream in, Writer out, String... prs) throws IOException {
        BufferedReader inRdr = new BufferedReader(new InputStreamReader(in));
        replace(inRdr, out, prs);
    }

    public void replace(Reader in, Writer out, String... prs) throws IOException {
        if (prs.length%2 != 0)
            throw new IllegalArgumentException("Even number of pattern/string pairs: " + Arrays.toString(prs)
                + ". Must be even number of arguments.");
        Pattern[] patterns = new Pattern[prs.length/2];
        for (int i = 0; i < prs.length; i += 2) {
            patterns[i/2] = new Pattern(prs[i], prs[i+1]);
        }

        StringBuilder tmp = new StringBuilder();
        for (int c = 0; (c = in.read()) != -1;) {
            int cursor = match((char)c, patterns);
            if (cursor < 0) { // no pattern recognized at all
                if (tmp.length() > 0) { // append  partial match then discard partial memory
                    for (int j = 0; j < tmp.length(); j++) {
                        out.write(tmp.charAt(j));
                    }
                    tmp.delete(0, tmp.length());
                }
                out.write((char)c); // directly output
            } else {
                Pattern p = matched(patterns); // has any pattern matched completely
                if (p != null) { // a pattern matched completely
                    char[] replace = p.replace().toCharArray();
                    for (char value : replace) {
                        out.write(value);
                    }
                    reset(patterns);
                    tmp.delete(0, tmp.length());
                } else {
                    tmp.append((char)c); // remember partial match
                }
            }
        }
    }

    /**
     * Match the given character to all patterns and return the index of highest match.
     * @param c a character to match
     * @param patterns an array of patterns
     * @return -1 if character matched no pattern
     */
    int match(char c, Pattern...patterns) {
        if (patterns == null)
            return -1;
        int result = -1;
        for (Pattern p : patterns) {
            result = Math.max(result, p.match(c));
        }
        return result;
    }

    /**
     * Gets the pattern if any in matched state
     * @param patterns
     */
    Pattern matched(Pattern...patterns) {
        if (patterns == null)
            return null;
        for (Pattern p : patterns) {
            if (p.isMatched()) return p;
        }
        return null;
    }

    /**
     * Resets all the patterns.
     * @param patterns
     */
    void reset(Pattern...patterns) {
        if (patterns == null)
            return;
        for (Pattern p : patterns) {
            p.reset();
        }
    }

    public static class Pattern {
        private final char[] chars;
        private final String _replace;
        private int _cursor;

        /**
         * Construct a pattern and its replacement.
         */
        public Pattern(String s, String replace) {
            if (s == null || s.length() == 0)
                throw new IllegalArgumentException("Pattern [" + s + "] can not be empty or null ");
            if (replace == null)
                throw new IllegalArgumentException("Replacement [" + replace + "] is null for pattern [" + s + "]");
            chars = s.toCharArray();
            _cursor = -1;
            _replace = replace;
        }

        /**
         * Match the given character with the current cursor and advance the matching length.
         * @param c
         * @return the matching length. -1 denotes the pattern did not match the character.
         */
        public int match(char c) {
            if (c != chars[++_cursor]) {
                reset();
            }
            return _cursor;
        }

        /**
         * Reset the cursor. Subsequent matching will begin at start.
         */
        public void reset() {
            _cursor = -1;
        }

        /**
         * Is this pattern matched fully?
         * A pattern is fully matched when the matching length is equal to the length of the pattern string.
         */
        public boolean isMatched() {
            return _cursor == chars.length-1;
        }

        /**
         * Gets the string to be replaced.
         */
        public String replace() {
            return _replace;
        }

        @Override
        public String toString() {
            return new String(chars) + ":" + _cursor;
        }
    }

}
