// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.tools.Logging;

/**
 * Checks for <a href="http://wiki.openstreetmap.org/wiki/Key:parking:lane">parking lanes</a>
 * @since 18400
 */
public class ParkingLanesConditional extends Test.TagTest {

    private final OpeningHourTest openingHourTest = new OpeningHourTest();
//    private static final Set<String> CONDITIONAL_PARKING_CONDITION_TYPES = new HashSet<>(Arrays.asList("parking:condition:both", "parking:condition:left",
//            "parking:condition:right", "parking:condition:both:maxstay", "parking:condition:left:maxstay", "parking:condition:right:maxstay"));
    private static final Set<String> CONDITIONAL_PARKING_VALUES = new HashSet<>(Arrays.asList("no", "free", "ticket", "disc", "disabled", "residents",
            "no_parking", "no_standing", "no_stopping"));
    private static final Set<String> TRANSPORT_TYPES = new HashSet<>(Arrays.asList("vehicle", "bicycle", "carriage", "trailer", "caravan", "motor_vehicle",
            "motorcycle", "moped", "mofa", "motorcar", "motorhome", "psv", "bus", "taxi", "tourist_bus", "goods", "hgv", "agricultural", "atv", "snowmobile",
            "hgv_articulated", "coach"));

    private static final Pattern CONDITIONAL_PATTERN;
    static {
        final String part = Pattern.compile("([^@\\p{Space}][^@]*?)"
                + "\\s*@\\s*" + "(\\([^)\\p{Space}][^)]+?\\)|[^();\\p{Space}][^();]*?)\\s*").toString();
        CONDITIONAL_PATTERN = Pattern.compile('(' + part + ")(;\\s*" + part + ")*");
    }
    private static final Pattern MAXSTAY_PATTERN = Pattern.compile("^(([1-9][0-9]*(\.[0-9]+)?( (minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years))))$");

    /**
     * Constructs a new {@code ParkingLanes}.
     */
    public ParkingLanesConditional() {
        super(tr("Parking Lanes"), tr("Tests for the correct usage of ''*:conditional'' tags in parking lanes."));
    }

    @Override
    public void initialize() throws Exception {
        super.initialize();
        openingHourTest.initialize();
    }

    /**
     * Check if the value is a valid restriction value
     * @param part The value
     * @return <code>true</code> for allowed restriction values
     */
    public static boolean isConditionalParkingValue(String part) {
        return CONDITIONAL_PARKING_VALUES.contains(part);
    }

    /**
     * Checks if the key denotes a
     * <a href="http://wiki.openstreetmap.org/wiki/Key:access#Transport_mode_restrictions">transport access mode restriction</a>
     * @param part The key (or the restriction part of it, e.g. for lanes)
     * @return <code>true</code> if it is a restriction
     */
    public static boolean isTransportationMode(String part) {
        return TRANSPORT_TYPES.contains(part);
    }

    /**
     * Check if a key part is a valid side
     * @param part The part of the key
     * @return <code>true</code> if it is a side
     */
    public static boolean isSide(String part) {
        return "left".equals(part) || "right".equals(part) || "both".equals(part);
    }

    /**
     * Check if a key part is the maxstay value
     * @param part The part of the key
     * @return <code>true</code> if is the maxstay string
     */
    public static boolean isMaxstay(String part) {
        return "maxstay".equals(part);
    }

    /**
     * Checks if a given key is a valid access key
     * @param key The conditional key
     * @return <code>true</code> if the key is valid
     */
    public boolean isKeyValid(String key) {
        // parking:condition:<side>[:<transportation mode>]:conditional or
        // parking:condition:<side>[:<transportation mode>]:maxstay:conditional
        if (!key.startsWith("parking:condition:") && !key.endsWith(":conditional")) {
            return false;
        }
        final String[] parts = key.replace("parking:condition:", "").replace(":conditional", "").split(":", -1);
        return isKeyValid3Parts(parts) || isKeyValid1Part(parts) || isKeyValid2Parts(parts);
    }

    private static boolean isKeyValid3Parts(String... parts) {
        return parts.length == 3 && isSide(parts[0]) && isTransportationMode(parts[1]) && isMaxstay(parts[2]);
    }

    private static boolean isKeyValid2Parts(String... parts) {
        return parts.length == 2 && ((isSide(parts[0]) && isTransportationMode(parts[1])) || (isSide(parts[0]) && isMaxstay(parts[1])));
    }

    private static boolean isKeyValid1Part(String... parts) {
        return parts.length == 1 && (isSide(parts[0]));
    }

    /**
     * Check if a value is valid
     * @param key The key the value is for
     * @param value The value
     * @return <code>true</code> if it is valid
     */
    public boolean isValueValid(String key, String value) {
        return validateValue(key, value) == null;
    }

    static class ConditionalParsingException extends RuntimeException {
        ConditionalParsingException(String message) {
            super(message);
        }
    }

    /**
     * A conditional value is a value for the access restriction tag that depends on conditions (time, ...)
     */
    public static class ConditionalValue {
        /**
         * The value the tag should have if the condition matches
         */
        public final String conditionalParkingValue;
        /**
         * The conditions for {@link #conditionalParkingValue}
         */
        public final Collection<String> conditions;

        /**
         * Create a new {@link ConditionalValue}
         * @param conditionalParkingValue The value the tag should have if the condition matches
         * @param conditions The conditions for that value
         */
        public ConditionalValue(String conditionalParkingValue, Collection<String> conditions) {
            this.conditionalParkingValue = conditionalParkingValue;
            this.conditions = conditions;
        }

        /**
         * Parses the condition values as string.
         * @param value value, must match {@code <restriction-value> @ <condition>[;<restriction-value> @ <condition>]} pattern
         * @return list of {@code ConditionalValue}s
         * @throws ConditionalParsingException if {@code value} does not match expected pattern
         */
        public static List<ConditionalValue> parse(String value) {
            // <restriction-value> @ <condition>[;<restriction-value> @ <condition>]
            final List<ConditionalValue> r = new ArrayList<>();
            final Matcher m = CONDITIONAL_PATTERN.matcher(value);
            // FIXME: Add MAXSTAY_PATTERN somehow
            if (!m.matches()) {
                throw new ConditionalParsingException(tr("Does not match pattern ''restriction value @ condition''"));
            } else {
                int i = 2;
                while (i + 1 <= m.groupCount() && m.group(i + 1) != null) {
                    final String restrictionValue = m.group(i);
                    final String[] conditions = m.group(i + 1).replace("(", "").replace(")", "").split("\\s+(AND|and)\\s+", -1);
                    r.add(new ConditionalValue(restrictionValue, Arrays.asList(conditions)));
                    i += 3;
                }
            }
            return r;
        }
    }

    /**
     * Validate a key/value pair
     * @param key The key
     * @param value The value
     * @return The error message for that value or <code>null</code> to indicate valid
     */
    public String validateValue(String key, String value) {
        try {
            for (final ConditionalValue conditional : ConditionalValue.parse(value)) {
                // validate restriction value
                if (!isConditionalParkingValue(conditional.conditionalParkingValue)) {
                    return tr("{0} is not a valid parking condition value", conditional.conditionalParkingValue);
                }
                // validate opening hour if the value contains an hour (heuristic)
                for (final String condition : conditional.conditions) {
                    if (condition.matches(".*[0-9]:[0-9]{2}.*")) {
                        final List<TestError> errors = openingHourTest.checkOpeningHourSyntax("", condition);
                        if (!errors.isEmpty()) {
                            return errors.get(0).getDescription();
                        }
                    }
                }
            }
        } catch (ConditionalParsingException ex) {
            Logging.debug(ex);
            return ex.getMessage();
        }
        return null;
    }

    /**
     * Validate a primitive
     * @param p The primitive
     * @return The errors for that primitive or an empty list if there are no errors.
     */
    public List<TestError> validatePrimitive(OsmPrimitive p) {
        final List<TestError> errors = new ArrayList<>();
        final Pattern pattern = Pattern.compile(":conditional(:.*)?$");
        p.visitKeys((primitive, key, value) -> {
            // Only validate parking condition keys
            if (!key.startsWith("parking:condition") || !pattern.matcher(key).find()) {
                return;
            }
            if (!isKeyValid(key)) {
                errors.add(TestError.builder(this, Severity.WARNING, 4101)
                        .message(tr("Wrong syntax in {0} key", key))
                        .primitives(p)
                        .build());
                return;
            }
            final String error = validateValue(key, value);
            if (error != null) {
                errors.add(TestError.builder(this, Severity.WARNING, 4102)
                        .message(tr("Error in {0} value: {1}", key, error))
                        .primitives(p)
                        .build());
            }
        });
        return errors;
    }

    @Override
    public void check(OsmPrimitive p) {
        if (p.isTagged()) {
            errors.addAll(validatePrimitive(p));
        }
    }
}
