/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed 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 com.android.manifmerger;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Describes an attribute characteristics like if it supports smart package name replacement, has
 * a default value and a validator for its values.
 */
class AttributeModel {

    @NonNull private final XmlNode.NodeName mName;
    private final boolean mIsPackageDependent;
    @Nullable private final String mDefaultValue;
    @Nullable private final Validator mOnReadValidator;
    @Nullable private final Validator mOnWriteValidator;
    @NonNull private final MergingPolicy mMergingPolicy;

    /**
     * Define a new attribute with specific characteristics.
     *
     * @param name name of the attribute, so far assumed to be in the
     *             {@link com.android.SdkConstants#ANDROID_URI} namespace.
     * @param isPackageDependent true if the attribute support smart substitution of package name.
     * @param defaultValue an optional default value.
     * @param onReadValidator an optional validator to validate values against.
     */
    private AttributeModel(@NonNull XmlNode.NodeName name,
            boolean isPackageDependent,
            @Nullable String defaultValue,
            @Nullable Validator onReadValidator,
            @Nullable Validator onWriteValidator,
            @NonNull MergingPolicy mergingPolicy) {
        mName = name;
        mIsPackageDependent = isPackageDependent;
        mDefaultValue = defaultValue;
        mOnReadValidator = onReadValidator;
        mOnWriteValidator = onWriteValidator;
        mMergingPolicy = mergingPolicy;
    }

    @NonNull
    XmlNode.NodeName getName() {
        return mName;
    }

    /**
     * Return true if the attribute support smart substitution of partially fully qualified
     * class names with package settings as provided by the manifest node's package attribute
     * {@link <a href=http://developer.android.com/guide/topics/manifest/manifest-element.html>}
     *
     * @return true if this attribute supports smart substitution or false if not.
     */
    boolean isPackageDependent() {
        return mIsPackageDependent;
    }

    /**
     * Returns the attribute's default value or null if none.
     */
    @Nullable
    String getDefaultValue() {
        return mDefaultValue;
    }

    /**
     * Returns the attribute's {@link com.android.manifmerger.AttributeModel.Validator} to
     * validate its value when read from xml files or null if no validation is necessary.
     */
    @Nullable
    public Validator getOnReadValidator() {
        return mOnReadValidator;
    }

    /**
     * Returns the attribute's {@link com.android.manifmerger.AttributeModel.Validator} to
     * validate its value when the merged file is about to be persisted.
     */
    @Nullable
    public Validator getOnWriteValidator() {
        return mOnWriteValidator;
    }

    /**
     * Returns the {@link com.android.manifmerger.AttributeModel.MergingPolicy} for this
     * attribute.
     */
    @NonNull
    public MergingPolicy getMergingPolicy() {
        return mMergingPolicy;
    }

    /**
     * Creates a new {@link Builder} to describe an attribute.
     * @param attributeName the to be described attribute name
     */
    @NonNull
    static Builder newModel(String attributeName) {
        return new Builder(attributeName);
    }


    static class Builder {

        private final String mName;
        private boolean mIsPackageDependent = false;
        private String mDefaultValue;
        private Validator mOnReadValidator;
        private Validator mOnWriteValidator;
        @NonNull
        private MergingPolicy mMergingPolicy = STRICT_MERGING_POLICY;

        Builder(String name) {
            this.mName = name;
        }

        /**
         * Sets the attribute support for smart substitution of partially fully qualified
         * class names with package settings as provided by the manifest node's package attribute
         * {@link <a href=http://developer.android.com/guide/topics/manifest/manifest-element.html>}
         */
        @NonNull
        Builder setIsPackageDependent() {
            mIsPackageDependent = true;
            return this;
        }

        /**
         * Sets the attribute default value.
         */
        @NonNull
        Builder setDefaultValue(String value) {
            mDefaultValue =  value;
            return this;
        }

        /**
         * Sets a {@link com.android.manifmerger.AttributeModel.Validator} to validate the
         * attribute's values coming from xml files.
         */
        @NonNull
        Builder setOnReadValidator(Validator validator) {
            mOnReadValidator = validator;
            return this;
        }

        /**
         * Sets a {@link com.android.manifmerger.AttributeModel.Validator} to validate values
         * before they are written to the final merged document.
         */
        @NonNull
        Builder setOnWriteValidator(Validator validator) {
            mOnWriteValidator = validator;
            return this;
        }

        @NonNull
        Builder setMergingPolicy(@NonNull MergingPolicy mergingPolicy) {
            mMergingPolicy = mergingPolicy;
            return this;
        }

        /**
         * Build an immutable {@link com.android.manifmerger.AttributeModel}
         */
        @NonNull
        AttributeModel build() {
            return new AttributeModel(
                    XmlNode.fromXmlName("android:" + mName),
                    mIsPackageDependent,
                    mDefaultValue,
                    mOnReadValidator,
                    mOnWriteValidator,
                    mMergingPolicy);
        }
    }

    /**
     * Defines a merging policy between two attribute values. Example of merging policies can be
     * strict when it is illegal to try to merge or override a value by another. Another example
     * is a OR merging policy on boolean attribute values.
     */
    interface MergingPolicy {

        /**
         * Returns true if it should be attempted to merge this attribute value with
         * the attribute default value when merging with a node that does not contain
         * the attribute declaration.
         */
        boolean shouldMergeDefaultValues();

        /**
         * Merges the two attributes values and returns the merged value. If the values cannot be
         * merged, return null.
         */
        @Nullable
        String merge(@NonNull String higherPriority, @NonNull String lowerPriority);
    }

    /**
     * Standard attribute value merging policy, generates an error unless both values are equal.
     */
    @NonNull
    static final MergingPolicy STRICT_MERGING_POLICY = new MergingPolicy() {

        @Override
        public boolean shouldMergeDefaultValues() {
            return false;
        }

        @Nullable
        @Override
        public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
            // it's ok if the values are equal, otherwise it's not.
            return higherPriority.equals(lowerPriority)
                    ? higherPriority
                    : null;
        }
    };

    /**
     * Boolean OR merging policy.
     */
    static final MergingPolicy OR_MERGING_POLICY = new MergingPolicy() {
        @Override
        public boolean shouldMergeDefaultValues() {
            return true;
        }

        @Nullable
        @Override
        public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
            return Boolean.toString(BooleanValidator.isTrue(higherPriority) ||
                    BooleanValidator.isTrue(lowerPriority));
        }
    };

    /**
     * Merging policy that will return the higher priority value regardless of the lower priority
     * value
     */
    static final MergingPolicy NO_MERGING_POLICY = new MergingPolicy() {

        @Override
        public boolean shouldMergeDefaultValues() {
            return true;
        }

        @Nullable
        @Override
        public String merge(@NonNull String higherPriority, @NonNull String lowerPriority) {
            return higherPriority;
        }
    };

    /**
     * Decode a decimal or hexadecimal {@link String} into an {@link Integer}.
     * String starting with 0 will be considered decimal, not octal.
     */
    private static int decodeDecOrHexString(@NonNull String s) {
        long decodedValue = s.startsWith("0x") || s.startsWith("0X")
                ? Long.decode(s)
                : Long.parseLong(s);
        if (decodedValue < 0xFFFFFFFFL) {
            return (int) decodedValue;
        } else {
            throw new IllegalArgumentException("Value " + s + " too big for 32 bits.");
        }
    }

    /**
     * Validates an attribute value.
     *
     * The validator can be called when xml documents are read to ensure the xml file contains
     * valid statements.
     *
     * This is a poor-mans replacement for not having a proper XML Schema do perform such
     * validations.
     */
    interface Validator {

        /**
         * Validates a value, issuing a warning or error in case of failure through the passed
         * merging report.
         * @param mergingReport to report validation warnings or error
         * @param attribute the attribute to validate.
         * @param value the proposed or existing attribute value.
         * @return true if the value is legal for this attribute.
         */
        boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute,
                @NonNull String value);
    }

    /**
     * Validates a boolean attribute type.
     */
    static class BooleanValidator implements Validator {

        // TODO: check with @xav where to find the acceptable values by runtime.
        private static final Pattern TRUE_PATTERN = Pattern.compile("true|True|TRUE");
        private static final Pattern FALSE_PATTERN = Pattern.compile("false|False|FALSE");

        private static boolean isTrue(@NonNull String value) {
            return TRUE_PATTERN.matcher(value).matches();
        }

        @Override
        public boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute,
                @NonNull String value) {
            boolean matches = TRUE_PATTERN.matcher(value).matches() ||
                    FALSE_PATTERN.matcher(value).matches();
            if (!matches) {
                attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
                        String.format(
                                "Attribute %1$s at %2$s has an illegal value=(%3$s), "
                                        + "expected 'true' or 'false'",
                                attribute.getId(),
                                attribute.printPosition(),
                                value
                        )
                );
            }
            return matches;
        }
    }

    /**
     * A {@link com.android.manifmerger.AttributeModel.Validator} for verifying that a proposed
     * value is part of the acceptable list of possible values.
     */
    static class MultiValueValidator implements Validator {

        @NonNull
        private final String[] multiValues;
        @NonNull
        private final String allValues;

        MultiValueValidator(@NonNull String... multiValues) {
            this.multiValues = multiValues;
            allValues = Joiner.on(',').join(multiValues);
        }

        @Override
        public boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute, @NonNull String value) {
            for (String multiValue : multiValues) {
                if (multiValue.equals(value)) {
                    return true;
                }
            }
            attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
                    String.format(
                            "Invalid value for attribute %1$s at %2$s, value=(%3$s), "
                                    + "acceptable values are (%4$s)",
                            attribute.getId(),
                            attribute.printPosition(),
                            value,
                            allValues
                    )
            );
            return false;
        }
    }

    /**
     * A {@link com.android.manifmerger.AttributeModel.Validator} for verifying that a proposed
     * value is a numerical integer value.
     */
    static class IntegerValueValidator implements Validator {

        @Override
        public boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute, @NonNull String value) {
            try {
                return Integer.parseInt(value) > 0;
            } catch (NumberFormatException e) {
                attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
                        String.format(
                                "Attribute %1$s at %2$s must be an integer, found %3$s",
                                attribute.getId(),
                                attribute.printPosition(),
                                value)
                );
                return false;
            }
        }
    }

    /**
     * A {@link com.android.manifmerger.AttributeModel.Validator} to validate that a string is
     * a valid 32 bits hexadecimal representation.
     */
    static class Hexadecimal32Bits implements Validator {
        protected static final Pattern PATTERN = Pattern.compile("0[xX]([0-9a-fA-F]+)");

        @Override
        public boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute, @NonNull String value) {
            Matcher matcher = PATTERN.matcher(value);
            boolean valid = matcher.matches() && matcher.group(1).length() <= 8;
            if (!valid) {
                attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
                        String.format(
                                "Attribute %1$s at %2$s is not a valid hexadecimal 32 bit value,"
                                        + " found %3$s",
                                attribute.getId(),
                                attribute.printPosition(),
                                value
                        ));
            }
            return valid;
        }
    }

    /**
     * A {@link com.android.manifmerger.AttributeModel.Validator} to validate that a string is
     * a valid 32 positive hexadecimal representation with a minimum value requirement.
     */
    static class Hexadecimal32BitsWithMinimumValue extends Hexadecimal32Bits {

        private final int mMinimumValue;

        Hexadecimal32BitsWithMinimumValue(int minimumValue) {
            mMinimumValue = minimumValue;
        }

        @Override
        public boolean validates(@NonNull MergingReport.Builder mergingReport,
                @NonNull XmlAttribute attribute, @NonNull String value) {
            boolean valid = super.validates(mergingReport, attribute, value);
            if (valid) {
                try {
                    Long decodedValue = Long.decode(value);
                    valid = decodedValue >= mMinimumValue && decodedValue < 0xFFFFFFFFL;
                } catch(NumberFormatException e) {
                    valid = false;
                }
                if (!valid) {
                    attribute.addMessage(mergingReport, MergingReport.Record.Severity.ERROR,
                            String.format(
                                    "Attribute %1$s at %2$s is not a valid hexadecimal value,"
                                            + " minimum is 0x%3$08X, maximum is 0x%4$08X, found %5$s",
                                    attribute.getId(),
                                    attribute.printPosition(),
                                    mMinimumValue,
                                    Integer.MAX_VALUE,
                                    value
                            ));
                }
                return valid;
            }
            return false;
        }
    }
}