/*
 * 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.jasper.tagplugins.jstl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Objects;

import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import jakarta.servlet.jsp.JspException;
import jakarta.servlet.jsp.JspTagException;
import jakarta.servlet.jsp.PageContext;

import org.apache.jasper.compiler.Localizer;

/**
 * Util contains some often used consts, static methods and embedded class to support the JSTL tag plugin.
 */

public class Util {

    private static final String VALID_SCHEME_CHAR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+.-";

    public static final String DEFAULT_ENCODING = "ISO-8859-1";

    private static final int HIGHEST_SPECIAL = '>';

    private static final char[][] specialCharactersRepresentation = new char[HIGHEST_SPECIAL + 1][];

    static {
        specialCharactersRepresentation['&'] = "&amp;".toCharArray();
        specialCharactersRepresentation['<'] = "&lt;".toCharArray();
        specialCharactersRepresentation['>'] = "&gt;".toCharArray();
        specialCharactersRepresentation['"'] = "&#034;".toCharArray();
        specialCharactersRepresentation['\''] = "&#039;".toCharArray();
    }

    /**
     * Converts the given string description of a scope to the corresponding PageContext constant. The validity of the
     * given scope has already been checked by the appropriate TLV.
     *
     * @param scope String description of scope
     *
     * @return PageContext constant corresponding to given scope description taken from
     *             org.apache.taglibs.standard.tag.common.core.Util
     */
    public static int getScope(String scope) {
        int ret = PageContext.PAGE_SCOPE;

        if ("request".equalsIgnoreCase(scope)) {
            ret = PageContext.REQUEST_SCOPE;
        } else if ("session".equalsIgnoreCase(scope)) {
            ret = PageContext.SESSION_SCOPE;
        } else if ("application".equalsIgnoreCase(scope)) {
            ret = PageContext.APPLICATION_SCOPE;
        }

        return ret;
    }

    /**
     * Returns <code>true</code> if our current URL is absolute, <code>false</code> otherwise. taken from
     * org.apache.taglibs.standard.tag.common.core.ImportSupport
     *
     * @param url The URL
     *
     * @return <code>true</code> if the URL is absolute
     */
    public static boolean isAbsoluteUrl(String url) {
        if (url == null) {
            return false;
        }

        int colonPos = url.indexOf(':');
        if (colonPos == -1) {
            return false;
        }

        for (int i = 0; i < colonPos; i++) {
            if (VALID_SCHEME_CHAR.indexOf(url.charAt(i)) == -1) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get the value associated with a content-type attribute. Syntax defined in RFC 2045, section 5.1. taken from
     * org.apache.taglibs.standard.tag.common.core.Util
     *
     * @param input The attribute string
     * @param name  The attribute name
     *
     * @return the attribute value
     */
    public static String getContentTypeAttribute(String input, String name) {
        int begin;
        int end;
        int index = input.toUpperCase(Locale.ENGLISH).indexOf(name.toUpperCase(Locale.ENGLISH));
        if (index == -1) {
            return null;
        }
        index = index + name.length(); // positioned after the attribute name
        index = input.indexOf('=', index); // positioned at the '='
        if (index == -1) {
            return null;
        }
        index += 1; // positioned after the '='
        input = input.substring(index).trim();

        if (input.charAt(0) == '"') {
            // attribute value is a quoted string
            begin = 1;
            end = input.indexOf('"', begin);
            if (end == -1) {
                return null;
            }
        } else {
            begin = 0;
            end = input.indexOf(';');
            if (end == -1) {
                end = input.indexOf(' ');
            }
            if (end == -1) {
                end = input.length();
            }
        }
        return input.substring(begin, end).trim();
    }

    /**
     * Performs the following substring replacements (to facilitate output to XML/HTML pages):
     * <ul>
     * <li>{@code &} -&gt; {@code &amp}</li>
     * <li>{@code <} -&gt; {@code &lt;}</li>
     * <li>{@code >} -&gt; {@code &gt;}</li>
     * <li>{@code "} -&gt; {@code &#034;}</li>
     * <li>{@code '} -&gt; {@code &#039;}</li>
     * </ul>
     * See also OutSupport.writeEscapedXml(). taken from org.apache.taglibs.standard.tag.common.core.Util
     *
     * @param buffer Data to escape
     *
     * @return escaped data
     */
    public static String escapeXml(String buffer) {
        String result = escapeXml(buffer.toCharArray(), buffer.length());
        return Objects.requireNonNullElse(result, buffer);
    }

    @SuppressWarnings("null") // escapedBuffer cannot be null
    public static String escapeXml(char[] arrayBuffer, int length) {
        int start = 0;
        StringBuilder escapedBuffer = null;

        for (int i = 0; i < length; i++) {
            char c = arrayBuffer[i];
            if (c <= HIGHEST_SPECIAL) {
                char[] escaped = specialCharactersRepresentation[c];
                if (escaped != null) {
                    // create StringBuilder to hold escaped xml string
                    if (start == 0) {
                        escapedBuffer = new StringBuilder(length + 5);
                    }
                    // add unescaped portion
                    if (start < i) {
                        escapedBuffer.append(arrayBuffer, start, i - start);
                    }
                    start = i + 1;
                    // add escaped xml
                    escapedBuffer.append(escaped);
                }
            }
        }
        // no xml escaping was necessary
        if (start == 0) {
            return null;
        }
        // add rest of unescaped portion
        if (start < length) {
            escapedBuffer.append(arrayBuffer, start, length - start);
        }
        return escapedBuffer.toString();
    }

    /**
     * Utility methods taken from org.apache.taglibs.standard.tag.common.core.UrlSupport
     *
     * @param url         The URL
     * @param context     The context
     * @param pageContext The page context
     *
     * @return the absolute URL
     *
     * @throws JspException If the URL doesn't start with '/'
     */
    public static String resolveUrl(String url, String context, PageContext pageContext) throws JspException {
        // don't touch absolute URLs
        if (isAbsoluteUrl(url)) {
            return url;
        }

        // normalize relative URLs against a context root
        HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
        if (context == null) {
            if (url.startsWith("/")) {
                return request.getContextPath() + url;
            } else {
                return url;
            }
        } else {
            if (!context.startsWith("/") || !url.startsWith("/")) {
                throw new JspTagException(Localizer.getMessage("jstl.urlMustStartWithSlash"));
            }
            if (context.equals("/")) {
                // Don't produce string starting with '//', many
                // browsers interpret this as host name, not as
                // path on same host.
                return url;
            } else {
                return context + url;
            }
        }
    }

    /**
     * Wraps responses to allow us to retrieve results as Strings. Mainly taken from
     * org.apache.taglibs.standard.tag.common.core.importSupport.
     */
    public static class ImportResponseWrapper extends HttpServletResponseWrapper {

        private final StringWriter sw = new StringWriter();
        private final ByteArrayOutputStream bos = new ByteArrayOutputStream();
        private final ServletOutputStream sos = new ServletOutputStream() {
            @Override
            public void write(int b) throws IOException {
                bos.write(b);
            }

            @Override
            public boolean isReady() {
                // Non-blocking IO not supported
                return false;
            }

            @Override
            public void setWriteListener(WriteListener listener) {
                // Non-blocking IO not supported
                throw new UnsupportedOperationException();
            }


        };
        private boolean isWriterUsed;
        private boolean isStreamUsed;
        private int status = 200;
        private String charEncoding;

        public ImportResponseWrapper(HttpServletResponse arg0) {
            super(arg0);
        }

        @Override
        public PrintWriter getWriter() {
            if (isStreamUsed) {
                throw new IllegalStateException(Localizer.getMessage("jstl.writerAfterOS"));
            }
            isWriterUsed = true;
            return new PrintWriter(sw);
        }

        @Override
        public ServletOutputStream getOutputStream() {
            if (isWriterUsed) {
                throw new IllegalStateException(Localizer.getMessage("jstl.OSAfterWriter"));
            }
            isStreamUsed = true;
            return sos;
        }

        @Override
        public void setContentType(String x) {
            // ignore
        }

        @Override
        public void setLocale(Locale x) {
            // ignore
        }

        @Override
        public void setStatus(int status) {
            this.status = status;
        }

        @Override
        public int getStatus() {
            return status;
        }

        public String getCharEncoding() {
            return this.charEncoding;
        }

        public void setCharEncoding(String ce) {
            this.charEncoding = ce;
        }

        public String getString() throws UnsupportedEncodingException {
            if (isWriterUsed) {
                return sw.toString();
            } else if (isStreamUsed) {
                if (this.charEncoding != null && !this.charEncoding.isEmpty()) {
                    return bos.toString(charEncoding);
                } else {
                    return bos.toString(StandardCharsets.ISO_8859_1);
                }
            } else {
                return ""; // target didn't write anything
            }
        }
    }

}
