/*
 * 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.nifi.processors.hadoop.util;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * This class allows the user to define byte-array filters or single-byte filters that will modify the content that is written to the underlying stream. Each filter can be given a maximum number of
 * replacements that it should perform.
 */
public class ByteFilteringOutputStream extends FilterOutputStream {

    private final List<Filter> multiByteFilters = new ArrayList<>();
    private final List<Filter> singleByteFilters = new ArrayList<>();
    private final OutputStream wrapped;

    public ByteFilteringOutputStream(final OutputStream toWrap)
            throws IOException {
        super(toWrap);
        this.wrapped = toWrap;
    }

    @Override
    public synchronized void write(byte[] buffer, int offset, int length)
            throws IOException {
        for (final Filter filter : multiByteFilters) {
            if (filter.matches(buffer, offset, length)) {
                wrapped.write(filter.replaceWith);
            } else {
                wrapped.write(buffer, offset, length);
            }
        }
    }

    @Override
    public synchronized void write(int data) throws IOException {
        for (final Filter filter : singleByteFilters) {
            if (filter.matches((byte) data)) {
                wrapped.write(filter.replaceWith);
            } else {
                wrapped.write(data);
            }
        }
    }

    /**
     * Causes this stream to write <tt>replaceWith</tt> in place of
     * <tt>toReplace</tt> if {@link #write(byte[], int, int)} is called where the value to write is equal to
     * <tt>toReplace</tt>.
     * <p/>
     * @param toReplace the byte array to replace
     * @param replaceWith the byte array to be substituted
     */
    public void addFilter(final byte[] toReplace, final byte[] replaceWith) {
        addFilter(toReplace, replaceWith, -1);
    }

    /**
     * Causes this stream to write <tt>replaceWith</tt> in place of
     * <tt>toReplace</tt> if {@link #write(byte[], int, int)} is called where the value to write is equal to
     * <tt>toReplace</tt>.
     * <p/>
     * @param toReplace the byte array to replace
     * @param replaceWith the byte array to be substituted
     * @param maxReplacements the maximum number of replacements that should be made
     */
    public void addFilter(final byte[] toReplace, final byte[] replaceWith, final int maxReplacements) {
        multiByteFilters.add(new Filter(toReplace, replaceWith, maxReplacements));
    }

    /**
     * Causes this stream to write <tt>replaceWith</tt> in place of
     * <tt>toReplace</tt> if {@link #write(int)} is called where the value to write is equal to
     * <tt>toReplace</tt>.
     * <p/>
     * @param toReplace the byte to replace
     * @param replaceWith the byte to be substituted
     */
    public void addFilter(final byte toReplace, final byte replaceWith) {
        addFilter(toReplace, replaceWith, -1);
    }

    /**
     * Causes this stream to write <tt>replaceWith</tt> in place of
     * <tt>toReplace</tt> if {@link #write(int)} is called where the value to write is equal to
     * <tt>toReplace</tt>.
     * <p/>
     * @param toReplace the byte to replace
     * @param replaceWith the byte to be substituted
     * @param maxReplacements the maximum number of replacements that should be made
     */
    public void addFilter(final byte toReplace, final byte replaceWith, final int maxReplacements) {
        singleByteFilters.add(new Filter(new byte[]{toReplace}, new byte[]{replaceWith}, maxReplacements));
    }

    static class Filter {

        final byte[] toReplace;
        final byte[] replaceWith;
        final int maxMatches;
        int numMatches = 0;

        public Filter(final byte[] toReplace, final byte[] replaceWith, final int maxReplacements) {
            this.toReplace = toReplace;
            this.replaceWith = replaceWith;
            this.maxMatches = maxReplacements;
        }

        public boolean matches(final byte candidate) {
            return matches(new byte[]{candidate}, 0, 1);
        }

        public boolean matches(final byte[] candidate, final int offset, final int length) {
            final boolean finishedReplacing = (numMatches >= maxMatches && maxMatches > -1);

            if (finishedReplacing || (length != toReplace.length)) {
                return false;
            }

            final byte[] compare;
            if (length == candidate.length) {
                compare = candidate;
            } else {
                compare = new byte[length];
                System.arraycopy(candidate, offset, compare, 0, length);
            }

            final boolean match = Arrays.equals(compare, toReplace);
            if (match) {
                numMatches++;
            }

            return match;
        }
    }
}
