/**
 * Copyright 2011-2021 Asakusa Framework Team.
 *
 * 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.asakusafw.yaess.core;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import com.asakusafw.yaess.core.util.PropertiesUtil;

/**
 * Profile services for YAESS.
 * @since 0.2.3
 */
public class YaessProfile {

    /**
     * The qualifier delimiter for each property keys.
     */
    public static final char QUALIFIER = '.';

    /**
     * The key prefix of core profile.
     * This profile must describes {@link CoreProfile} service.
     */
    public static final String PREFIX_CORE = "core";

    /**
     * The key prefix of monitor profile.
     * This profile must describes {@link ExecutionMonitorProvider} service.
     */
    public static final String PREFIX_MONITOR = "monitor";

    /**
     * The key prefix of lock provider profile.
     * This profile must describes {@link ExecutionLockProvider} service.
     */
    public static final String PREFIX_LOCK = "lock";

    /**
     * The key prefix of job scheduler profile.
     * This profile must describes {@link JobScheduler} service.
     */
    public static final String PREFIX_SCHEDULER = "scheduler";

    /**
     * The key prefix of Hadoop script handler profile.
     * This profile must describes {@link HadoopScriptHandler} service.
     */
    public static final String PREFIX_HADOOP = "hadoop";

    /**
     * The key prefix of command script handlers profile.
     * Each command script handler must be use {@code "command.<profile-name>"} as its prefix.
     * This profile must describes {@link CommandScriptHandler} service.
     */
    public static final String PREFIX_COMMAND = "command";

    private static final String GROUP_PREFIX_COMMAND = PREFIX_COMMAND + QUALIFIER;

    private final ServiceProfile<CoreProfile> core;

    private final ServiceProfile<ExecutionMonitorProvider> monitors;

    private final ServiceProfile<ExecutionLockProvider> locks;

    private final ServiceProfile<JobScheduler> scheduler;

    private final ServiceProfile<HadoopScriptHandler> hadoopHandler;

    private final Map<String, ServiceProfile<CommandScriptHandler>> commandHandlers;

    /**
     * Creates a new instance.
     * @param core core profile
     * @param monitors monitor profile
     * @param locks lock profile
     * @param scheduler scheduler profile
     * @param hadoopHandler hadoop handler profile
     * @param commandHandlers command handler profiles
     * @throws IllegalArgumentException if some parameters were {@code null}
     */
    public YaessProfile(
            ServiceProfile<CoreProfile> core,
            ServiceProfile<ExecutionMonitorProvider> monitors,
            ServiceProfile<ExecutionLockProvider> locks,
            ServiceProfile<JobScheduler> scheduler,
            ServiceProfile<HadoopScriptHandler> hadoopHandler,
            Collection<ServiceProfile<CommandScriptHandler>> commandHandlers) {
        if (core == null) {
            throw new IllegalArgumentException("core must not be null"); //$NON-NLS-1$
        }
        if (monitors == null) {
            throw new IllegalArgumentException("monitors must not be null"); //$NON-NLS-1$
        }
        if (locks == null) {
            throw new IllegalArgumentException("locks must not be null"); //$NON-NLS-1$
        }
        if (scheduler == null) {
            throw new IllegalArgumentException("scheduler must not be null"); //$NON-NLS-1$
        }
        if (hadoopHandler == null) {
            throw new IllegalArgumentException("hadoopHandler must not be null"); //$NON-NLS-1$
        }
        if (commandHandlers == null) {
            throw new IllegalArgumentException("commandHandlers must not be null"); //$NON-NLS-1$
        }
        checkPrefix(core, PREFIX_CORE);
        checkPrefix(monitors, PREFIX_MONITOR);
        checkPrefix(locks, PREFIX_LOCK);
        checkPrefix(scheduler, PREFIX_SCHEDULER);
        checkPrefix(hadoopHandler, PREFIX_HADOOP);
        this.core = core;
        this.monitors = monitors;
        this.locks = locks;
        this.scheduler = scheduler;
        this.hadoopHandler = hadoopHandler;
        Map<String, ServiceProfile<CommandScriptHandler>> map = new TreeMap<>();
        for (ServiceProfile<CommandScriptHandler> profile : commandHandlers) {
            if (profile.getPrefix().startsWith(GROUP_PREFIX_COMMAND) == false) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "Profile \"{1}\" is invalid, this must be \"{0}<profile-name>\" ({2})",
                        GROUP_PREFIX_COMMAND,
                        profile.getPrefix(),
                        profile.getServiceClass().getName()));
            }
            String profileName = profile.getPrefix().substring(GROUP_PREFIX_COMMAND.length());
            if (map.containsKey(profileName)) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "Profile \"{0}\" is duplicated ({1} <=> {2})",
                        profileName,
                        profile.getServiceClass().getName(),
                        map.get(profileName).getServiceClass().getName()));
            }
            map.put(profileName, profile);
        }
        this.commandHandlers = Collections.unmodifiableMap(map);
    }

    private void checkPrefix(ServiceProfile<?> profile, String prefix) {
        assert profile != null;
        assert prefix != null;
        if (profile.getPrefix().equals(prefix) == false) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Profile \"{1}\" is invalid, this must be \"{0}\" ({2})",
                    prefix,
                    profile.getPrefix(),
                    profile.getServiceClass().getName()));
        }
    }

    /**
     * Loads a YAESS profile from the specified properties.
     * @param properties source properties
     * @param classLoader the class loader to load the service class
     * @return the loaded profile
     * @throws IllegalArgumentException if the target profile is invalid, or parameters contain {@code null}
     * @deprecated use {@link #load(Properties, ProfileContext)} instead
     */
    @Deprecated
    public static YaessProfile load(Properties properties, ClassLoader classLoader) {
        if (properties == null) {
            throw new IllegalArgumentException("properties must not be null"); //$NON-NLS-1$
        }
        if (classLoader == null) {
            throw new IllegalArgumentException("classLoader must not be null"); //$NON-NLS-1$
        }
        return load(properties, ProfileContext.system(classLoader));
    }

    /**
     * Loads a YAESS profile from the specified properties.
     * @param properties source properties
     * @param context the current profile context
     * @return the loaded profile
     * @throws IllegalArgumentException if the target profile is invalid, or parameters contain {@code null}
     * @since 0.2.4
     */
    public static YaessProfile load(Properties properties, ProfileContext context) {
        if (properties == null) {
            throw new IllegalArgumentException("properties must not be null"); //$NON-NLS-1$
        }
        if (context == null) {
            throw new IllegalArgumentException("context must not be null"); //$NON-NLS-1$
        }
        ServiceProfile<CoreProfile> core =
            ServiceProfile.load(properties, PREFIX_CORE, CoreProfile.class, context);
        ServiceProfile<ExecutionMonitorProvider> monitors =
            ServiceProfile.load(properties, PREFIX_MONITOR, ExecutionMonitorProvider.class, context);
        ServiceProfile<ExecutionLockProvider> locks =
            ServiceProfile.load(properties, PREFIX_LOCK, ExecutionLockProvider.class, context);
        ServiceProfile<JobScheduler> scheduler =
            ServiceProfile.load(properties, PREFIX_SCHEDULER, JobScheduler.class, context);
        ServiceProfile<HadoopScriptHandler> hadoopHandler =
            ServiceProfile.load(properties, PREFIX_HADOOP, HadoopScriptHandler.class, context);
        List<ServiceProfile<CommandScriptHandler>> commandHandlers = new ArrayList<>();
        for (String commandHandlerPrefix : PropertiesUtil.getChildKeys(properties, GROUP_PREFIX_COMMAND, ".")) {
            ServiceProfile<CommandScriptHandler> profile =
                ServiceProfile.load(properties, commandHandlerPrefix, CommandScriptHandler.class, context);
            commandHandlers.add(profile);
        }
        return new YaessProfile(core, monitors, locks, scheduler, hadoopHandler, commandHandlers);
    }

    /**
     * Returns a core profile.
     * @return core profile
     */
    public ServiceProfile<CoreProfile> getCore() {
        return core;
    }

    /**
     * Returns a monitor provider.
     * @return monitor provider
     */
    public ServiceProfile<ExecutionMonitorProvider> getMonitors() {
        return monitors;
    }

    /**
     * Returns a lock provider.
     * @return lock provider
     */
    public ServiceProfile<ExecutionLockProvider> getLocks() {
        return locks;
    }

    /**
     * Returns a job scheduler.
     * @return the scheduler
     */
    public ServiceProfile<JobScheduler> getScheduler() {
        return scheduler;
    }

    /**
     * Returns a Hadoop script handler.
     * @return Hadoop script handler
     */
    public ServiceProfile<HadoopScriptHandler> getHadoopHandler() {
        return hadoopHandler;
    }

    /**
     * Returns command script handlers for each target profile name.
     * @return the command script handlers
     */
    public Map<String, ServiceProfile<CommandScriptHandler>> getCommandHandlers() {
        return commandHandlers;
    }
}