//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http.pathmap;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;

/**
 * PathSpec for URI Template based declarations
 *
 * @see <a href="https://tools.ietf.org/html/rfc6570">URI Templates (Level 1)</a>
 */
public class UriTemplatePathSpec extends RegexPathSpec
{
    private static final Logger LOG = Log.getLogger(UriTemplatePathSpec.class);

    private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\{(.*)\\}");
    /**
     * Reserved Symbols in URI Template variable
     */
    private static final String VARIABLE_RESERVED = ":/?#[]@" + // gen-delims
        "!$&'()*+,;="; // sub-delims
    /**
     * Allowed Symbols in a URI Template variable
     */
    private static final String VARIABLE_SYMBOLS = "-._";
    private static final Set<String> FORBIDDEN_SEGMENTS;

    static
    {
        FORBIDDEN_SEGMENTS = new HashSet<>();
        FORBIDDEN_SEGMENTS.add("/./");
        FORBIDDEN_SEGMENTS.add("/../");
        FORBIDDEN_SEGMENTS.add("//");
    }

    private String[] variables;

    public UriTemplatePathSpec(String rawSpec)
    {
        super();
        Objects.requireNonNull(rawSpec, "Path Param Spec cannot be null");

        if ("".equals(rawSpec) || "/".equals(rawSpec))
        {
            super.pathSpec = "/";
            super.pattern = Pattern.compile("^/$");
            super.pathDepth = 1;
            this.specLength = 1;
            this.variables = new String[0];
            this.group = PathSpecGroup.EXACT;
            return;
        }

        if (rawSpec.charAt(0) != '/')
        {
            // path specs must start with '/'
            StringBuilder err = new StringBuilder();
            err.append("Syntax Error: path spec \"");
            err.append(rawSpec);
            err.append("\" must start with '/'");
            throw new IllegalArgumentException(err.toString());
        }

        for (String forbidden : FORBIDDEN_SEGMENTS)
        {
            if (rawSpec.contains(forbidden))
            {
                StringBuilder err = new StringBuilder();
                err.append("Syntax Error: segment ");
                err.append(forbidden);
                err.append(" is forbidden in path spec: ");
                err.append(rawSpec);
                throw new IllegalArgumentException(err.toString());
            }
        }

        this.pathSpec = rawSpec;

        StringBuilder regex = new StringBuilder();
        regex.append('^');

        List<String> varNames = new ArrayList<>();
        // split up into path segments (ignoring the first slash that will always be empty)
        String[] segments = rawSpec.substring(1).split("/");
        char[] segmentSignature = new char[segments.length];
        this.pathDepth = segments.length;
        for (int i = 0; i < segments.length; i++)
        {
            String segment = segments[i];
            Matcher mat = VARIABLE_PATTERN.matcher(segment);

            if (mat.matches())
            {
                // entire path segment is a variable.
                String variable = mat.group(1);
                if (varNames.contains(variable))
                {
                    // duplicate variable names
                    StringBuilder err = new StringBuilder();
                    err.append("Syntax Error: variable ");
                    err.append(variable);
                    err.append(" is duplicated in path spec: ");
                    err.append(rawSpec);
                    throw new IllegalArgumentException(err.toString());
                }

                assertIsValidVariableLiteral(variable);

                segmentSignature[i] = 'v'; // variable
                // valid variable name
                varNames.add(variable);
                // build regex
                regex.append("/([^/]+)");
            }
            else if (mat.find(0))
            {
                // variable exists as partial segment
                StringBuilder err = new StringBuilder();
                err.append("Syntax Error: variable ");
                err.append(mat.group());
                err.append(" must exist as entire path segment: ");
                err.append(rawSpec);
                throw new IllegalArgumentException(err.toString());
            }
            else if ((segment.indexOf('{') >= 0) || (segment.indexOf('}') >= 0))
            {
                // variable is split with a path separator
                StringBuilder err = new StringBuilder();
                err.append("Syntax Error: invalid path segment /");
                err.append(segment);
                err.append("/ variable declaration incomplete: ");
                err.append(rawSpec);
                throw new IllegalArgumentException(err.toString());
            }
            else if (segment.indexOf('*') >= 0)
            {
                // glob segment
                StringBuilder err = new StringBuilder();
                err.append("Syntax Error: path segment /");
                err.append(segment);
                err.append("/ contains a wildcard symbol (not supported by this uri-template implementation): ");
                err.append(rawSpec);
                throw new IllegalArgumentException(err.toString());
            }
            else
            {
                // valid path segment
                segmentSignature[i] = 'e'; // exact
                // build regex
                regex.append('/');
                // escape regex special characters
                for (char c : segment.toCharArray())
                {
                    if ((c == '.') || (c == '[') || (c == ']') || (c == '\\'))
                    {
                        regex.append('\\');
                    }
                    regex.append(c);
                }
            }
        }

        // Handle trailing slash (which is not picked up during split)
        if (rawSpec.charAt(rawSpec.length() - 1) == '/')
        {
            regex.append('/');
        }

        regex.append('$');

        this.pattern = Pattern.compile(regex.toString());

        int varcount = varNames.size();
        this.variables = varNames.toArray(new String[varcount]);

        // Convert signature to group
        String sig = String.valueOf(segmentSignature);

        if (Pattern.matches("^e*$", sig))
        {
            this.group = PathSpecGroup.EXACT;
        }
        else if (Pattern.matches("^e*v+", sig))
        {
            this.group = PathSpecGroup.PREFIX_GLOB;
        }
        else if (Pattern.matches("^v+e+", sig))
        {
            this.group = PathSpecGroup.SUFFIX_GLOB;
        }
        else
        {
            this.group = PathSpecGroup.MIDDLE_GLOB;
        }
    }

    /**
     * Validate variable literal name, per RFC6570, Section 2.1 Literals
     */
    private void assertIsValidVariableLiteral(String variable)
    {
        int len = variable.length();

        int i = 0;
        int codepoint;
        boolean valid = (len > 0); // must not be zero length

        while (valid && i < len)
        {
            codepoint = variable.codePointAt(i);
            i += Character.charCount(codepoint);

            // basic letters, digits, or symbols
            if (isValidBasicLiteralCodepoint(codepoint))
            {
                continue;
            }

            // The ucschar and iprivate pieces
            if (Character.isSupplementaryCodePoint(codepoint))
            {
                continue;
            }

            // pct-encoded
            if (codepoint == '%')
            {
                if (i + 2 > len)
                {
                    // invalid percent encoding, missing extra 2 chars
                    valid = false;
                    continue;
                }
                codepoint = TypeUtil.convertHexDigit(variable.codePointAt(i++)) << 4;
                codepoint |= TypeUtil.convertHexDigit(variable.codePointAt(i++));

                // validate basic literal
                if (isValidBasicLiteralCodepoint(codepoint))
                {
                    continue;
                }
            }

            valid = false;
        }

        if (!valid)
        {
            // invalid variable name
            StringBuilder err = new StringBuilder();
            err.append("Syntax Error: variable {");
            err.append(variable);
            err.append("} an invalid variable name: ");
            err.append(pathSpec);
            throw new IllegalArgumentException(err.toString());
        }
    }

    private boolean isValidBasicLiteralCodepoint(int codepoint)
    {
        // basic letters or digits
        if ((codepoint >= 'a' && codepoint <= 'z') ||
            (codepoint >= 'A' && codepoint <= 'Z') ||
            (codepoint >= '0' && codepoint <= '9'))
        {
            return true;
        }

        // basic allowed symbols
        if (VARIABLE_SYMBOLS.indexOf(codepoint) >= 0)
        {
            return true; // valid simple value
        }

        // basic reserved symbols
        if (VARIABLE_RESERVED.indexOf(codepoint) >= 0)
        {
            LOG.warn("Detected URI Template reserved symbol [{}] in path spec \"{}\"", (char)codepoint, pathSpec);
            return false; // valid simple value
        }

        return false;
    }

    public Map<String, String> getPathParams(String path)
    {
        Matcher matcher = getMatcher(path);
        if (matcher.matches())
        {
            if (group == PathSpecGroup.EXACT)
            {
                return Collections.emptyMap();
            }
            Map<String, String> ret = new HashMap<>();
            int groupCount = matcher.groupCount();
            for (int i = 1; i <= groupCount; i++)
            {
                ret.put(this.variables[i - 1], matcher.group(i));
            }
            return ret;
        }
        return null;
    }

    public int getVariableCount()
    {
        return variables.length;
    }

    public String[] getVariables()
    {
        return this.variables;
    }
}
