/*
 * Copyright (c) 2006-2014 DMDirc Developers
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.dmdirc.plugins;

import com.dmdirc.updater.Version;
import com.dmdirc.util.collections.MapList;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * Validates that the values specified in a plugin's meta data are correct and all requirements are
 * met.
 *
 * @since 0.6.6
 */
public class PluginMetaDataValidator {

    /** The metadata being validated. */
    private final PluginMetaData metadata;
    /** A collection of errors which have been identified. */
    private final Collection<String> errors = new ArrayList<>();

    /**
     * Creates a new metadata validator.
     *
     * @param metadata The metadata to be validated
     */
    public PluginMetaDataValidator(final PluginMetaData metadata) {
        this.metadata = metadata;
    }

    /**
     * Validates the metadata file.
     *
     * @param plugins  A map of known plugins to their short names
     * @param services A map of known services
     *
     * @return A collection of errors that occurred (if any)
     */
    public Collection<String> validate(final Map<String, PluginMetaData> plugins,
            final MapList<String, String> services) {
        errors.clear();

        checkMetaData();
        checkRequirements(plugins, services);

        return Collections.unmodifiableCollection(errors);
    }

    /**
     * Checks that the metadata values are correct.
     */
    protected void checkMetaData() {
        if (!metadata.getVersion().isValid()) {
            errors.add("Missing or invalid 'version'");
        }

        if (metadata.getAuthor() == null || metadata.getAuthor().isEmpty()) {
            errors.add("Missing or invalid 'author'");
        }

        if (metadata.getName() == null || metadata.getName().isEmpty() || metadata.getName().
                indexOf(' ') != -1) {
            errors.add("Missing or invalid 'name'");
        }

        if (metadata.getMainClass() == null || metadata.getMainClass().isEmpty()) {
            errors.add("Missing or invalid 'mainclass'");
        }
    }

    /**
     * Checks that the plugin's requirements are met.
     *
     * @param plugins  A map of known plugins by their short names
     * @param services A map of known services
     */
    protected void checkRequirements(final Map<String, PluginMetaData> plugins,
            final MapList<String, String> services) {
        checkOS(metadata.getRequirements().get("os"), System.getProperty("os.name"), System.
                getProperty("os.version"), System.getProperty("os.arch"));
        checkFiles(metadata.getRequirements().get("files"));
        checkPlugins(plugins, metadata.getRequirements().get("plugins"));
        checkServices(services, metadata.getRequiredServices());
    }

    /**
     * Check if the services required by this plugin are available.
     *
     * @param knownServices A map of known services
     * @param services      Required services
     */
    private void checkServices(final MapList<String, String> knownServices,
            final Collection<String> services) {
        if (services == null || services.isEmpty()) {
            return;
        }

        for (String requirement : services) {
            final String[] bits = requirement.split(" ", 2);
            final String name = bits[0];
            final String type = bits[1];

            if (!knownServices.containsKey(type)
                    || (!name.equalsIgnoreCase("any")
                    && !knownServices.containsValue(type, name))) {
                errors.add("Service " + name + " of type " + type
                        + " not available");
            }
        }
    }

    /**
     * Checks to see if the OS requirements of the plugin are satisfied. If the desired string is
     * empty, the test passes. Otherwise it is used as one to three colon-delimited regular
     * expressions, to test the name, version and architecture of the OS, respectively. On failure,
     * the requirementsError field will contain a user-friendly error message.
     *
     * @param desired       The desired OS requirements
     * @param actualName    The actual name of the OS
     * @param actualVersion The actual version of the OS
     * @param actualArch    The actual architecture of the OS
     */
    protected void checkOS(final String desired, final String actualName, final String actualVersion,
            final String actualArch) {
        if (desired == null || desired.isEmpty()) {
            return;
        }

        final String[] desiredParts = desired.split(":");

        if (!actualName.toLowerCase().matches(desiredParts[0])) {
            errors.add("Invalid OS. (Wanted: '" + desiredParts[0] + "', actual: '" + actualName
                    + "')");
        } else if (desiredParts.length > 1 && !actualVersion.toLowerCase().matches(desiredParts[1])) {
            errors.add("Invalid OS version. (Wanted: '" + desiredParts[1] + "', actual: '"
                    + actualVersion + "')");
        } else if (desiredParts.length > 2 && !actualArch.toLowerCase().matches(desiredParts[2])) {
            errors.add("Invalid OS architecture. (Wanted: '" + desiredParts[2] + "', actual: '"
                    + actualArch + "')");
        }
    }

    /**
     * Checks to see if the file requirements of the plugin are satisfied. If the desired string is
     * empty, the test passes. Otherwise it is passed to File.exists() to see if the file is valid.
     * Multiple files can be specified by using a "," to separate. And either/or files can be
     * specified using a "|" (eg /usr/bin/bash|/bin/bash) If the test fails, the requirementsError
     * field will contain a user-friendly error message.
     *
     * @param desired The desired file requirements
     */
    protected void checkFiles(final String desired) {
        if (desired == null || desired.isEmpty()) {
            return;
        }

        for (String files : desired.split(",")) {
            final String[] filelist = files.split("\\|");
            boolean foundFile = false;
            for (String file : filelist) {
                if (new File(file).exists()) {
                    foundFile = true;
                    break;
                }
            }
            if (!foundFile) {
                errors.add("Required file '" + files + "' not found");
            }
        }
    }

    /**
     * Checks to see if the plugin requirements of the plugin are satisfied. If the desired string
     * is empty, the test passes. Plugins should be specified as:
     * plugin1[:minversion[:maxversion]],plugin2[:minversion[:maxversion]] Plugins will be attempted
     * to be loaded if not loaded, else the test will fail if the versions don't match, or the
     * plugin isn't known. If the test fails, the requirementsError field will contain a
     * user-friendly error message.
     *
     * @param plugins A map of known plugins by their short names
     * @param desired The desired file requirements
     */
    protected void checkPlugins(final Map<String, PluginMetaData> plugins,
            final String desired) {
        if (desired == null || desired.isEmpty()) {
            return;
        }

        for (String pluginName : desired.split(",")) {
            final String[] data = pluginName.split(":");
            final PluginMetaData target = plugins.get(data[0]);

            if (target == null) {
                errors.add("Required plugin '" + data[0] + "' was not found");
            }

            if (data.length > 1) {
                // Check plugin minimum version matches.
                if (target.getVersion().compareTo(new Version(data[1])) < 0) {
                    errors.add("Plugin '" + data[0]
                            + "' is too old (required version: " + data[1]
                            + ", actual version: " + target.getVersion() + ")");
                }

                // Check plugin maximum version matches.
                if (data.length > 2 && target.getVersion().compareTo(
                        new Version(data[2])) > 0) {
                    errors.add("Plugin '" + data[0]
                            + "' is too new (required version: " + data[2]
                            + ", actual version: " + target.getVersion() + ")");
                }
            }
        }
    }

}
