/**
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.atlas.query;

import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.type.AtlasBusinessMetadataType;
import org.apache.atlas.type.AtlasStructType.AtlasAttribute.AtlasRelationshipEdgeDirection;
import org.apache.atlas.type.AtlasType;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.apache.atlas.repository.Constants.CLASSIFICATION_LABEL;

public class IdentifierHelper {
    private static final Pattern        SINGLE_QUOTED_IDENTIFIER   = Pattern.compile("'(\\w[\\w\\d\\.\\s]*)'");
    private static final Pattern        DOUBLE_QUOTED_IDENTIFIER   = Pattern.compile("\"(\\w[\\w\\d\\.\\s]*)\"");
    private static final Pattern        BACKTICK_QUOTED_IDENTIFIER = Pattern.compile("`(\\w[\\w\\d\\.\\s]*)`");
    private static final Character[]    ESCAPE_CHARS               = new Character[] {'@', '#', '&', '~', '/'};
    private static final Set<Character> ESCAPE_CHARACTERS_SET      = new HashSet<>(Arrays.asList(ESCAPE_CHARS));

    private IdentifierHelper() {
        // to block instantiation
    }

    public static String get(String quotedIdentifier) {
        String ret;

        if (StringUtils.isEmpty(quotedIdentifier)) {
            return null;
        }

        if (quotedIdentifier.charAt(0) == '`') {
            ret = extract(BACKTICK_QUOTED_IDENTIFIER, quotedIdentifier);
        } else if (quotedIdentifier.charAt(0) == '\'') {
            ret = extract(SINGLE_QUOTED_IDENTIFIER, quotedIdentifier);
        } else if (quotedIdentifier.charAt(0) == '"') {
            ret = extract(DOUBLE_QUOTED_IDENTIFIER, quotedIdentifier);
        } else {
            ret = quotedIdentifier;
        }

        return ret;
    }

    public static Info create(GremlinQueryComposer.Context context, org.apache.atlas.query.Lookup lookup, String identifier) {
        Info idInfo = new Info(identifier);

        idInfo.update(lookup, context);

        return idInfo;
    }

    public static String getQualifiedName(org.apache.atlas.query.Lookup lookup, GremlinQueryComposer.Context context, String name) {
        try {
            return lookup.getQualifiedName(context, name);
        } catch (AtlasBaseException e) {
            context.error(e, AtlasErrorCode.INVALID_DSL_QUALIFIED_NAME, context.getActiveTypeName(), name);
        }

        return "";
    }

    public static boolean isQuoted(String val) {
        boolean ret = false;

        if (StringUtils.isEmpty(val)) {
            return ret;
        }

        char first = val.charAt(0);
        char last  = val.charAt(val.length() - 1);

        if (first == last && (first == '\'' || first == '"' || first == '`')) {
            ret = true;
        }

        return ret;
    }

    public static String removeQuotes(String rhs) {
        if (StringUtils.isEmpty(rhs)) {
            return rhs;
        }

        return rhs.replace("\"", "")
                .replace("'", "")
                .replace("`", "");
    }

    public static String getQuoted(String s) {
        return String.format("'%s'", s);
    }

    public static boolean isTrueOrFalse(String rhs) {
        return rhs.equalsIgnoreCase("true") || rhs.equalsIgnoreCase("false");
    }

    public static String getFixedRegEx(String s) {
        return s.replace("*", ".*").replace('?', '.');
    }

    public static String escapeCharacters(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < value.length(); i++) {
            char c = value.charAt(i);

            if (c != '*' && ESCAPE_CHARACTERS_SET.contains(c)) {
                sb.append('\\');
            }

            sb.append(c);
        }

        return sb.toString();
    }

    public static String removeWildcards(String s) {
        return removeQuotes(s).replace("*", "").replace("?", "");
    }

    public static boolean isCompleteValue(String s) {
        return (StringUtils.isEmpty(s) || StringUtils.equals(s, "0") || StringUtils.equalsIgnoreCase(s, "false"));
    }

    public static boolean isInCompleteValue(String s) {
        return StringUtils.isNotEmpty(s) && (StringUtils.equals(s, "1") || StringUtils.equalsIgnoreCase(s, "true"));
    }

    private static String extract(Pattern p, String s) {
        Matcher m = p.matcher(s);

        return m.find() ? m.group(1) : s;
    }

    public static class Info {
        private       String                         raw;
        private final String                         actual;
        private       String[]                       parts;
        private       String                         typeName;
        private       String                         attributeName;
        private       boolean                        isPrimitive;
        private       AtlasRelationshipEdgeDirection edgeDirection;
        private       String                         edgeLabel;
        private       boolean                        introduceType;
        private       boolean                        hasSubtypes;
        private       String                         subTypes;
        private       boolean                        isTrait;
        private       boolean                        newContext;
        private       boolean                        isAttribute;
        private       String                         qualifiedName;
        private       boolean                        isDate;
        private       boolean                        isNumeric;

        public Info(String s) {
            this.raw    = removeQuotes(s);
            this.actual = IdentifierHelper.get(raw);
        }

        public String getQualifiedName() {
            return qualifiedName;
        }

        public boolean isPrimitive() {
            return isPrimitive;
        }

        public boolean isAttribute() {
            return isAttribute;
        }

        public String getAttributeName() {
            return attributeName;
        }

        public AtlasRelationshipEdgeDirection getEdgeDirection() {
            return edgeDirection;
        }

        public String getEdgeLabel() {
            return edgeLabel;
        }

        public String getTypeName() {
            return typeName;
        }

        public void setTypeName(String typeName) {
            this.typeName = typeName;
        }

        public boolean isReferredType() {
            return introduceType;
        }

        public boolean isTrait() {
            return isTrait;
        }

        public boolean hasSubtypes() {
            return hasSubtypes;
        }

        public String getSubTypes() {
            return subTypes;
        }

        public String get() {
            return actual;
        }

        public boolean isDate() {
            return isDate;
        }

        public boolean hasParts() {
            return parts.length > 1;
        }

        public String getRaw() {
            return raw;
        }

        public boolean isNumeric() {
            return isNumeric;
        }

        private void update(org.apache.atlas.query.Lookup lookup, GremlinQueryComposer.Context context) {
            try {
                newContext = context.isEmpty();

                if (!newContext) {
                    if (context.hasAlias(this.raw)) {
                        raw = context.getTypeNameFromAlias(this.raw);
                    }

                    updateTypeInfo(lookup, context);
                    setIsTrait(context, lookup, attributeName);
                    updateEdgeInfo(lookup, context);

                    introduceType = (!isPrimitive() && !context.hasAlias(parts[0])) || isTrait;

                    updateSubTypes(lookup, context);
                }
            } catch (NullPointerException ex) {
                String exception = (StringUtils.isNotEmpty(ex.getMessage()) ? ex.getMessage() : "Exception");

                context.getErrorList().add(exception);
            }
        }

        private void setIsTrait(GremlinQueryComposer.Context ctx, Lookup lookup, String s) {
            isTrait = lookup.isTraitType(s);
        }

        private void updateSubTypes(org.apache.atlas.query.Lookup lookup, GremlinQueryComposer.Context context) {
            if (isTrait) {
                return;
            }

            hasSubtypes = lookup.doesTypeHaveSubTypes(context);

            if (hasSubtypes) {
                subTypes = lookup.getTypeAndSubTypes(context);
            }
        }

        private void updateEdgeInfo(org.apache.atlas.query.Lookup lookup, GremlinQueryComposer.Context context) {
            if (!isPrimitive && !isTrait && typeName != attributeName) {
                edgeDirection = lookup.getRelationshipEdgeDirection(context, attributeName);
                edgeLabel     = lookup.getRelationshipEdgeLabel(context, attributeName);
                typeName      = lookup.getTypeFromEdge(context, attributeName);
            } else if (isTrait) {
                edgeDirection = AtlasRelationshipEdgeDirection.OUT;
                edgeLabel     = CLASSIFICATION_LABEL;
            }
        }

        private void updateTypeInfo(org.apache.atlas.query.Lookup lookup, GremlinQueryComposer.Context context) {
            parts = StringUtils.split(raw, ".");

            // check if this is a business attribute
            if (parts.length == 2) {
                try {
                    AtlasType type = lookup.getType(parts[0]);

                    if (type instanceof AtlasBusinessMetadataType) {
                        parts    = new String[1];
                        parts[0] = raw;
                    }
                } catch (AtlasBaseException excp) {
                    // ignore
                }
            }

            if (parts.length == 1) {
                typeName      = context.hasAlias(parts[0]) ? context.getTypeNameFromAlias(parts[0]) : context.getActiveTypeName();
                qualifiedName = getDefaultQualifiedNameForSinglePartName(context, parts[0]);
                attributeName = parts[0];
            } else if (parts.length == 2) {
                boolean isAttrOfActiveType = lookup.hasAttribute(context, parts[0]);

                if (isAttrOfActiveType) {
                    attributeName = parts[0];
                } else {
                    typeName = context.hasAlias(parts[0]) ? context.getTypeNameFromAlias(parts[0]) : parts[0];

                    if (typeName != null && lookup.isTraitType(typeName) && !typeName.equals(context.getActiveTypeName())) {
                        attributeName = typeName;
                    } else {
                        attributeName = parts[1];
                    }
                }
            }

            isAttribute = lookup.hasAttribute(context, attributeName);
            isPrimitive = lookup.isPrimitive(context, attributeName);

            setQualifiedName(lookup, context, isAttribute, attributeName);
            setIsDate(lookup, context, isPrimitive, attributeName);
            setIsNumeric(lookup, context, isPrimitive, attributeName);
        }

        private String getDefaultQualifiedNameForSinglePartName(GremlinQueryComposer.Context context, String s) {
            String qn = context.getTypeNameFromAlias(s);

            if (StringUtils.isEmpty(qn) && SelectClauseComposer.isKeyword(s)) {
                return s;
            }

            return qn;
        }

        private void setQualifiedName(Lookup lookup, GremlinQueryComposer.Context context, boolean isAttribute, String attrName) {
            if (isAttribute) {
                qualifiedName = getQualifiedName(lookup, context, attrName);
            }
        }

        private String getQualifiedName(Lookup lookup, GremlinQueryComposer.Context context, String name) {
            return IdentifierHelper.getQualifiedName(lookup, context, name);
        }

        private void setIsDate(Lookup lookup, GremlinQueryComposer.Context context, boolean isPrimitive, String attrName) {
            if (isPrimitive) {
                isDate = lookup.isDate(context, attrName);
            }
        }

        private void setIsNumeric(Lookup lookup, GremlinQueryComposer.Context context, boolean isPrimitive, String attrName) {
            if (isPrimitive) {
                isNumeric = lookup.isNumeric(context, attrName);
            }
        }
    }
}
