package com.atlassian.bitbucket.util.web;

import org.apache.commons.io.IOUtils;

import javax.annotation.Nonnull;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.GZIPInputStream;

/**
 * Wrapper around HttpServletRequest that turns the {@link javax.servlet.ServletInputStream} into a {@link java.io.BufferedInputStream}. This
 * allows us to read from the {@link javax.servlet.ServletInputStream} in filters and reset the inputstream if we decide that we simply
 * want to forward the request down the filter-chain.
 * <p>
 * More specifically, BufferedHttpServletRequest allows you to use {@link java.io.InputStream#mark(int)} and
 * {@link java.io.InputStream#reset()} on the {@link javax.servlet.ServletInputStream} that is returned from
 * {@link javax.servlet.http.HttpServletRequest#getInputStream()}.
 */
public class PeekableHttpServletRequest extends HttpServletRequestWrapper {

    private final int bufferSize;
    private String content;
    private BufferedServletInputStream inputStream;

    /**
     * Constructs a request object wrapping the given request.
     *
     * @throws IllegalArgumentException if the request is null
     */
    public PeekableHttpServletRequest(HttpServletRequest request, int bufferSize) {
        super(request);
        this.bufferSize = bufferSize;
    }

    public int getBufferSize() {
        return bufferSize;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (inputStream == null) {
            inputStream = new BufferedServletInputStream(super.getInputStream(), bufferSize);
        }
        return inputStream;
    }

    public String getContent() throws IOException {
        if (content == null) {
            InputStream inputStream = getInputStream();
            inputStream.mark(getBufferSize());

            try {
                if ("gzip".equals(getHeader("content-encoding"))) {
                    inputStream = new GZIPInputStream(inputStream);
                }
                content = IOUtils.toString(inputStream, "UTF-8");
            } catch (IOException e) {
                // error reading from the http request. Most likely, the http request is larger than maxRequestSize. In this
                // case return null
                return null;
            } finally {
                // reset the request inputstream so it can be passed down the filter chain and used as if we hadn't touched it
                getInputStream().reset();
            }
        }

        return content;
    }
    
    private static class BufferedServletInputStream extends ServletInputStream {

        private final BufferedInputStream inputStream;
        private final int bufferSize;
        private int curPos = -1;
        private int markPos = -1;
        private int maxPos = -1;

        private BufferedServletInputStream(ServletInputStream inputStream, int bufferSize) {
            this.inputStream = new BufferedInputStream(inputStream, bufferSize);
            this.bufferSize = bufferSize;
        }

        @Override
        public int read() throws IOException {
            ensureBufferNotAtLimit();
            int result = inputStream.read();
            if (result != -1) {
                ++curPos;
            }
            return result;
        }

        @Override
        public int read(@Nonnull byte[] bytes) throws IOException {
            return read(bytes, 0, bytes.length);
        }

        @Override
        public int read(@Nonnull byte[] bytes, int i, int i1) throws IOException {
            ensureBufferNotAtLimit();
            if (maxPos != -1) {
                int bytesAvailable = maxPos - curPos;
                i1 = Math.min(i1, bytesAvailable);
            }
            int result = inputStream.read(bytes, i, i1);
            if (result > 0) {
                curPos += result;
            }

            return result;
        }

        @Override
        public long skip(long l) throws IOException {
            int limit = maxPos != -1 ? maxPos : bufferSize - 1;
            long maxSkippable = Math.max(0, limit - curPos);
            if (l > maxSkippable) {
                return maxSkippable;
            }
            return super.skip(l);
        }

        @Override
        public int available() throws IOException {
            return inputStream.available();
        }

        @Override
        public void close() throws IOException {
            inputStream.close();
        }

        @Override
        public void mark(int i) {
            inputStream.mark(i);
            markPos = curPos;
            maxPos = curPos + i;
        }

        @Override
        public void reset() throws IOException {
            inputStream.reset();
            curPos = markPos;
            maxPos = -1;
        }

        @Override
        public boolean markSupported() {
            return inputStream.markSupported();
        }

        private void ensureBufferNotAtLimit() throws IOException {
            if (maxPos >= 0 && curPos >= maxPos) {
                throw new IOException("Buffer is already at the limit, reading more would mean that we can't reset the inputstream");
            }
        }
    }
}
