/*
 * 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.jackrabbit.spi.commons.conversion;

import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;

import javax.jcr.NamespaceException;

/**
 * <code>PathParser</code> formats a {@link Path} using a
 * {@link NameResolver} and a {@link PathFactory}.
 */
public class PathParser {

    // constants for parser
    private static final int STATE_PREFIX_START = 0;
    private static final int STATE_PREFIX = 1;
    private static final int STATE_NAME_START = 2;
    private static final int STATE_NAME = 3;
    private static final int STATE_INDEX = 4;
    private static final int STATE_INDEX_END = 5;
    private static final int STATE_DOT = 6;
    private static final int STATE_DOTDOT = 7;
    private static final int STATE_IDENTIFIER = 8;
    private static final int STATE_URI = 9;
    private static final int STATE_URI_END = 10;

    private static final char EOF = (char) -1;

    /**
     * Parses <code>jcrPath</code> into a <code>Path</code> object using
     * <code>resolver</code> to convert prefixes into namespace URIs. If
     * resolver is <code>null</code> this method only checks the format of the
     * passed String and returns <code>null</code>.
     *
     * @param jcrPath the jcr path.
     * @param resolver the namespace resolver.
     * @param factory <code>PathFactory</code> to be used.
     * @return A path object.
     * @throws MalformedPathException If the <code>jcrPath</code> is malformed.
     * @throws IllegalNameException if any of the jcrNames is malformed.
     * @throws NamespaceException If an unresolvable prefix is encountered.
     */
    public static Path parse(String jcrPath, NameResolver resolver, PathFactory factory)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        return parse(null, jcrPath, resolver, factory);
    }

    /**
     * Parses <code>jcrPath</code> into a <code>Path</code> object using
     * <code>resolver</code> to convert prefixes into namespace URIs. If the
     * specified <code>jcrPath</code> is an identifier based absolute path
     * beginning with an identifier segment the specified
     * <code>IdentifierResolver</code> will be used to resolve it to an
     * absolute path.<p/>
     * If <code>namResolver</code> is <code>null</code> or if <code>identifierResolver</code>
     * is <code>null</code> and the path starts with an identifier segment, this
     * method only checks the format of the string and returns <code>null</code>.
     *
     * @param jcrPath the jcr path.
     * @param nameResolver the namespace resolver.
     * @param identifierResolver the resolver to validate any trailing identifier
     * segment and resolve to an absolute path.
     * @param factory
     * @return A path object.
     * @throws MalformedPathException If the <code>jcrPath</code> is malformed.
     * @throws IllegalNameException if any of the jcrNames is malformed.
     * @throws NamespaceException If an unresolvable prefix is encountered.
     * @since JCR 2.0
     */
    public static Path parse(String jcrPath, NameResolver nameResolver,
                             IdentifierResolver identifierResolver, PathFactory factory)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        return parse(null, jcrPath, nameResolver, identifierResolver, factory);
    }

    /**
     * Parses <code>jcrPath</code> into a <code>Path</code> object using
     * <code>resolver</code> to convert prefixes into namespace URIs. If the
     * specified <code>jcrPath</code> is an identifier based absolute path
     * beginning with an identifier segment the specified
     * <code>IdentifierResolver</code> will be used to resolve it to an
     * absolute path.<p/>
     * If <code>namResolver</code> is <code>null</code> or if <code>identifierResolver</code>
     * is <code>null</code> and the path starts with an identifier segment, this
     * method only checks the format of the string and returns <code>null</code>.
     *
     * @param jcrPath the jcr path.
     * @param nameResolver the namespace resolver.
     * @param identifierResolver the resolver to validate any trailing identifier
     * segment and resolve to an absolute path.
     * @param factory
     * @param normalizeIdentifier
     * @return A path object.
     * @throws MalformedPathException If the <code>jcrPath</code> is malformed.
     * @throws IllegalNameException if any of the jcrNames is malformed.
     * @throws NamespaceException If an unresolvable prefix is encountered.
     * @since JCR 2.0
     */
    public static Path parse(String jcrPath, NameResolver nameResolver,
                             IdentifierResolver identifierResolver,
                             PathFactory factory, boolean normalizeIdentifier)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        return parse(null, jcrPath, nameResolver, identifierResolver, factory, normalizeIdentifier);
    }

    /**
     * Parses the given <code>jcrPath</code> and returns a <code>Path</code>. If
     * <code>parent</code> is not <code>null</code>, it is prepended to the
     * built path before it is returned. If <code>resolver</code> is
     * <code>null</code>, this method only checks the format of the string and
     * returns <code>null</code>.
     *
     * @param parent   the parent path
     * @param jcrPath  the JCR path
     * @param resolver the namespace resolver to get prefixes for namespace
     *                 URIs.
     * @param factory
     * @return the <code>Path</code> object.
     * @throws MalformedPathException If the <code>jcrPath</code> is malformed.
     * @throws IllegalNameException if any of the jcrNames is malformed.
     * @throws NamespaceException If an unresolvable prefix is encountered.
     */
    public static Path parse(Path parent, String jcrPath,
                             NameResolver resolver,
                             PathFactory factory) throws MalformedPathException, IllegalNameException, NamespaceException {
        return parse(parent, jcrPath, resolver, null, factory);
    }

    /**
     * Parses the given <code>jcrPath</code> and returns a <code>Path</code>. If
     * <code>parent</code> is not <code>null</code>, it is prepended to the
     * built path before it is returned. If the specified <code>jcrPath</code>
     * is an identifier based absolute path beginning with an identifier segment
     * the given <code>identifierResolver</code> will be used to resolve it to an
     * absolute path.<p/>
     * If <code>nameResolver</code> is <code>null</code> or if <code>identifierResolver</code>
     * is <code>null</code> and the path starts with an identifier segment, this
     * method only checks the format of the string and returns <code>null</code>.
     *
     * @param parent the parent path.
     * @param jcrPath the jcr path.
     * @param nameResolver the namespace resolver.
     * @param identifierResolver the resolver to validate any trailing identifier
     * segment and resolve it to an absolute path.
     * @param factory The path factory.
     * @return the <code>Path</code> object.
     * @throws MalformedPathException
     * @throws IllegalNameException
     * @throws NamespaceException
     */
    public static Path parse(Path parent, String jcrPath, NameResolver nameResolver,
                             IdentifierResolver identifierResolver, PathFactory factory)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        return parse(parent, jcrPath, nameResolver, identifierResolver, factory, true);
    }

    /**
     * Parses the given <code>jcrPath</code> and returns a <code>Path</code>. If
     * <code>parent</code> is not <code>null</code>, it is prepended to the
     * built path before it is returned. If the specified <code>jcrPath</code>
     * is an identifier based absolute path beginning with an identifier segment
     * the given <code>identifierResolver</code> will be used to resolve it to an
     * absolute path.<p/>
     * If <code>nameResolver</code> is <code>null</code> or if <code>identifierResolver</code>
     * is <code>null</code> and the path starts with an identifier segment, this
     * method only checks the format of the string and returns <code>null</code>.
     *
     * @param parent the parent path.
     * @param jcrPath the jcr path.
     * @param nameResolver the namespace resolver.
     * @param identifierResolver the resolver to validate any trailing identifier
     * segment and resolve it to an absolute path.
     * @param factory The path factory.
     * @param normalizeIdentifier
     * @return the <code>Path</code> object.
     * @throws MalformedPathException
     * @throws IllegalNameException
     * @throws NamespaceException
     */
    private static Path parse(Path parent, String jcrPath, NameResolver nameResolver,
                             IdentifierResolver identifierResolver, PathFactory factory,
                             boolean normalizeIdentifier)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        // check for length
        int len = jcrPath == null ? 0 : jcrPath.length();

        // shortcut
        if (len == 1 && jcrPath.charAt(0) == '/') {
            return factory.getRootPath();
        }

        if (len == 0) {
            throw new MalformedPathException("empty path");
        }

        // check if absolute path
        PathBuilder builder = new PathBuilder(factory);
        int pos = 0;
        if (jcrPath.charAt(0) == '/') {
            if (parent != null) {
                throw new MalformedPathException("'" + jcrPath + "' is not a relative path.");
            }
            builder.addRoot();
            pos++;
        }

        // add master if present
        if (parent != null) {
            builder.addAll(parent.getElements());
        }

        // parse the path
        int state;
        if (jcrPath.charAt(0) == '[') {
            if (parent != null) {
                throw new MalformedPathException("'" + jcrPath + "' is not a relative path.");
            }
            state = STATE_IDENTIFIER;
            pos++;
        } else {
            state = STATE_PREFIX_START;
        }

        int lastPos = pos;

        String name = null;

        int index = Path.INDEX_UNDEFINED;
        boolean wasSlash = false;

        boolean checkFormat = (nameResolver == null);

        while (pos <= len) {
            char c = pos == len ? EOF : jcrPath.charAt(pos);
            char rawCharacter = c;
            pos++;
            // special check for whitespace
            if (c != ' ' && Character.isWhitespace(c)) {
                c = '\t';
            }
            switch (c) {
                case '/':
                case EOF:
                    if (state == STATE_PREFIX_START && c != EOF) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. double slash '//' not allowed.");
                    }
                    if (state == STATE_URI && c == EOF) {
                        // this handles the case where URI state was entered but the end of the segment was reached (JCR-3562)
                        state = STATE_URI_END;
                    }
                    if (state == STATE_PREFIX
                            || state == STATE_NAME
                            || state == STATE_INDEX_END
                            || state == STATE_URI_END) {

                        // eof pathelement
                        if (name == null) {
                            if (wasSlash) {
                                throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names.");
                            }
                            name = jcrPath.substring(lastPos, pos - 1);
                        }

                        // only add element if resolver not null. otherwise this
                        // is just a check for valid format.
                        if (checkFormat) {
                            NameParser.checkFormat(name);
                        } else {
                            Name qName = nameResolver.getQName(name);
                            builder.addLast(qName, index);
                        }
                        state = STATE_PREFIX_START;
                        lastPos = pos;
                        name = null;
                        index = Path.INDEX_UNDEFINED;
                    } else if (state == STATE_IDENTIFIER) {
                        if (c == EOF) {
                            // eof identifier reached
                            if (jcrPath.charAt(pos - 2) != ']') {
                                throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Unterminated identifier segment.");
                            }
                            String identifier = jcrPath.substring(lastPos, pos - 2);
                            if (checkFormat) {
                                if (identifierResolver != null) {
                                    identifierResolver.checkFormat(identifier);
                                } // else ignore. TODO: rather throw?
                            } else if (identifierResolver == null) {
                                throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Identifier segments are not supported.");
                            } else if (normalizeIdentifier) {
                                builder.addAll(identifierResolver.getPath(identifier).getElements());
                            } else {
                                identifierResolver.checkFormat(identifier);
                                builder.addLast(factory.createElement(identifier));
                            }
                            state = STATE_PREFIX_START;
                            lastPos = pos;
                        }
                    } else if (state == STATE_DOT) {
                        builder.addLast(factory.getCurrentElement());
                        lastPos = pos;
                        state = STATE_PREFIX_START;
                    } else if (state == STATE_DOTDOT) {
                        builder.addLast(factory.getParentElement());
                        lastPos = pos;
                        state = STATE_PREFIX_START;
                    } else if (state != STATE_URI
                            && !(state == STATE_PREFIX_START && c == EOF)) { // ignore trailing slash
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character.");
                    }
                    break;

                case '.':
                    if (state == STATE_PREFIX_START) {
                        state = STATE_DOT;
                    } else if (state == STATE_DOT) {
                        state = STATE_DOTDOT;
                    } else if (state == STATE_DOTDOT) {
                        state = STATE_PREFIX;
                    } else if (state == STATE_INDEX_END) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected.");
                    }
                    break;

                case ':':
                    if (state == STATE_PREFIX_START) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. Prefix must not be empty");
                    } else if (state == STATE_PREFIX) {
                        if (wasSlash) {
                            throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names.");
                        }
                        state = STATE_NAME_START;
                        // don't reset the lastPos/pos since prefix+name are passed together to the NameResolver
                    } else if (state != STATE_IDENTIFIER && state != STATE_URI) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid name character");
                    }
                    break;

                case '[':
                    if (state == STATE_PREFIX || state == STATE_NAME) {
                        if (wasSlash) {
                            throw new MalformedPathException("'" + jcrPath + "' is not a valid path: Trailing slashes not allowed in prefixes and names.");
                        }
                        state = STATE_INDEX;
                        name = jcrPath.substring(lastPos, pos - 1);
                        lastPos = pos;
                    } else if (state != STATE_IDENTIFIER) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character.");
                    }
                    break;

                case ']':
                    if (state == STATE_INDEX) {
                        try {
                            index = Integer.parseInt(jcrPath.substring(lastPos, pos - 1));
                        } catch (NumberFormatException e) {
                            throw new MalformedPathException("'" + jcrPath + "' is not a valid path. NumberFormatException in index: " + jcrPath.substring(lastPos, pos - 1));
                        }
                        if (index < Path.INDEX_DEFAULT) {
                            throw new MalformedPathException("'" + jcrPath + "' is not a valid path. Index number invalid: " + index);
                        }
                        state = STATE_INDEX_END;
                    } else if (state != STATE_IDENTIFIER) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character.");
                    }
                    break;

                case ' ':
                    if (state == STATE_PREFIX_START || state == STATE_NAME_START) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid name start");
                    } else if (state == STATE_INDEX_END) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected.");
                    } else if (state == STATE_DOT || state == STATE_DOTDOT) {
                        state = STATE_PREFIX;
                    }
                    break;

                case '\t':
                    if (state != STATE_IDENTIFIER) {
                        String message = String.format("'%s' is not a valid path. Whitespace other than SP (U+0020) not a allowed in a name, but U+%04x was found at position %d.",
                                            jcrPath, (long) rawCharacter, pos - 1);
                        throw new MalformedPathException(message);
                    }
                case '*':
                case '|':
                    if (state != STATE_IDENTIFIER) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not a valid name character.");
                    }
                case '{':
                    if (state == STATE_PREFIX_START && lastPos == pos-1) {
                        // '{' marks the start of a uri enclosed in an expanded name
                        // instead of the usual namespace prefix, if it is
                        // located at the beginning of a new segment.
                        state = STATE_URI;
                    } else if (state == STATE_NAME_START || state == STATE_DOT || state == STATE_DOTDOT) {
                        // otherwise it's part of the local name
                        state = STATE_NAME;
                    }
                    break;

                case '}':
                    if (state == STATE_URI) {
                        state = STATE_URI_END;
                    }
                    break;

                default:
                    if (state == STATE_PREFIX_START || state == STATE_DOT || state == STATE_DOTDOT) {
                        state = STATE_PREFIX;
                    } else if (state == STATE_NAME_START) {
                        state = STATE_NAME;
                    } else if (state == STATE_INDEX_END) {
                        throw new MalformedPathException("'" + jcrPath + "' is not a valid path. '" + c + "' not valid after index. '/' expected.");
                    }
            }
            wasSlash = c == ' ';
        }

        if (checkFormat) {
            // this was only for checking the format
            return null;
        } else {
            return builder.getPath();
        }
    }

    /**
     * Check the format of the given jcr path. Note, the neither name nor
     * namespace validation (resolution of prefix to URI) is performed and
     * therefore will not be detected.
     *
     * @param jcrPath
     * @throws MalformedPathException If the <code>jcrPath</code> is malformed.
     */
    public static void checkFormat(String jcrPath) throws MalformedPathException {
        try {
            // since no path is created -> use default factory
            parse(jcrPath, null, null, PathFactoryImpl.getInstance());
        } catch (NamespaceException e) {
            // will never occur
        } catch (IllegalNameException e) {
            // will never occur
        }
    }
}
