/*******************************************************************************
*   Copyright 2015 Analog Devices, Inc.
*
*   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.analog.lyric.dimple.schedulers;

import org.eclipse.jdt.annotation.Nullable;

import com.analog.lyric.collect.ConstructorRegistry;
import com.analog.lyric.dimple.environment.DimpleEnvironment;
import com.analog.lyric.dimple.events.IDimpleEventSource;
import com.analog.lyric.dimple.model.core.FactorGraph;
import com.analog.lyric.dimple.schedulers.validator.ScheduleValidatorOptionKey;
import com.analog.lyric.dimple.solvers.interfaces.ISolverFactorGraph;
import com.analog.lyric.options.IOptionHolder;
import com.analog.lyric.options.OptionKey;
import com.analog.lyric.options.OptionValidationException;

/**
 * Option key type for schedulers.
 * <p>
 * Unlike most options, scheduler options can hold mutable values and the {@link #defaultValue()}
 * is not a singleton constant but a newly created object.
 * <p>
 * @since 0.08
 * @author Christopher Barber
 */
public class SchedulerOptionKey extends OptionKey<IScheduler>
{
	private static final long serialVersionUID = 1L;

	/*-------
	 * State
	 */
	
	private final Class<? extends IScheduler> _defaultSchedulerClass;
	private final @Nullable ScheduleValidatorOptionKey _validatorKey;
	
	/*--------------
	 * Construction
	 */
	
	/**
	 * Constructs a scheduler option key.
	 * @param declaringClass is the class containing the static field declaration for this key.
	 * @param name is the name of static field declaration for this key.
	 * @param defaultSchedulerClass is the default value of the option. Used when option is not set.
	 * This class should implement {@link IScheduler#isDefaultScheduler()} to return true.
	 * @throws OptionValidationException if {@code defaultSchedulerClass} does not have a public default
	 * constructor.
	 * @since 0.08
	 */
	@SuppressWarnings("null")
	public SchedulerOptionKey(Class<?> declaringClass,
		String name,
		Class<? extends IScheduler> defaultSchedulerClass)
	{
		this(declaringClass, name, defaultSchedulerClass, null);
	}

	/**
	 * Constructs a scheduler option key.
	 * @param declaringClass is the class containing the static field declaration for this key.
	 * @param name is the name of static field declaration for this key.
	 * @param defaultSchedulerClass is the default value of the option. Used when option is not set.
	 * This class should implement {@link IScheduler#isDefaultScheduler()} to return true.
	 * @throws OptionValidationException if {@code defaultSchedulerClass} does not have a public default
	 * constructor.
	 * @since 0.08
	 */
	public SchedulerOptionKey(Class<?> declaringClass,
		String name,
		Class<? extends IScheduler> defaultSchedulerClass,
		ScheduleValidatorOptionKey validatorKey)
	{
		super(declaringClass, name);
		_defaultSchedulerClass = defaultSchedulerClass;
		_validatorKey = validatorKey;
		validateClass(defaultSchedulerClass);
		assert(defaultValue().isDefaultScheduler());
	}

	/*--------------------
	 * IOptionKey methods
	 */
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * This implementation supports conversion from strings and {@link IScheduler} class instances.
	 * The string is used to look up the scheduler class from the current {@linkplain #getRegistry() registry}.
	 */
	@Override
	public IScheduler convertToValue(@Nullable Object value)
	{
		if (value instanceof String)
		{
			value = getRegistry().getClass((String)value);
		}

		if (value instanceof Class<?>)
		{
			value = SchedulerBase.instantiateClass(validateClass((Class<?>)value));
		}
		
		return super.convertToValue(value);
	}
	
	/**
	 * Returns newly constructed default instance.
	 * <p>
	 * Note that unlike most option keys, this will return a new instance everytime it is invoked!
	 */
	@Override
	public IScheduler defaultValue()
	{
		return SchedulerBase.instantiateClass(_defaultSchedulerClass);
	}
	
	@Override
	public void set(IOptionHolder holder, IScheduler value)
	{
		super.set(holder, value);
	}
	
	/**
	 * Sets the option locally on the {@code holder} by instantiating a new instance of {@code schedulerClass}.
	 * <p>
	 * @param holder is the object on which the option will be set locally.
	 * @param schedulerClass must either have a public no-argument constructor or must be an enum, in which
	 * case the first enumerated value will be used.
	 * @since 0.08
	 * @see #set(IOptionHolder, IScheduler)
	 */
	public void set(IOptionHolder holder, Class<? extends IScheduler> schedulerClass)
	{
		convertAndSet(holder, schedulerClass);
	}

	/**
	 * Sets the option locally on the {@code holder} by instantiating a new instance of {@code schedulerClass}.
	 * @param holder is the object on which the option will be set locally.
	 * @param schedulerClass is a string that is used to lookup the scheduler class. It may either be a
	 * fully qualified Java class name, or the simple class name listed in the {@linkplain #getRegistry()
	 * scheduler registry.}
	 * @since 0.08
	 * @see #set(IOptionHolder, Class)
	 */
	public void set(IOptionHolder holder, String schedulerClass)
	{
		convertAndSet(holder, schedulerClass);
	}

	@Override
	public Class<? extends IScheduler> type()
	{
		return IScheduler.class;
	}
	
	/**
	 * {@inheritDoc}
	 * <p>
	 * Returns false if {@code scheduler} fails to {@linkplain IScheduler#validateForGraph(FactorGraph) validate}
	 * graph containing {@code delegator}.
	 */
	@Override
	public boolean validForDelegator(IScheduler scheduler, IOptionHolder delegator)
	{
		if (delegator instanceof IDimpleEventSource)
		{
			IDimpleEventSource source = (IDimpleEventSource)delegator;
			FactorGraph graph = source.getContainingGraph();
			if (graph != null)
			{
				try
				{
					scheduler.validateForGraph(graph);
				}
				catch (Exception ex)
				{
					return false;
				}
			}
		}
		return true;
	}

	@Override
	public IScheduler validate(IScheduler scheduler, @Nullable IOptionHolder optionHolder)
	{
		FactorGraph graph = optionHolder instanceof FactorGraph ? (FactorGraph)optionHolder :
			optionHolder instanceof ISolverFactorGraph ? ((ISolverFactorGraph)optionHolder).getModelObject() : null;
		
		if (graph != null)
		{
			try
			{
				scheduler.validateForGraph(graph);
			}
			catch (Exception ex)
			{
				throw new OptionValidationException("%s cannot be used with %s: %s", scheduler, graph, ex);
			}
		}

		
		return super.validate(scheduler, optionHolder);
	}
	
	/*----------------------------
	 * SchedulerOptionKey methods
	 */
	
	/**
	 * The {@link Class} of the {@linkplain #defaultValue default scheduler value}.
	 * <p>
	 * Use this to check the default without instantiating an instance.
	 * @since 0.08
	 */
	public Class<? extends IScheduler> defaultClass()
	{
		return _defaultSchedulerClass;
	}
	
	/**
	 * Returns {@linkplain DimpleEnvironment#schedulers() scheduler registry} for
	 * {@linkplain DimpleEnvironment#active active environment}, which is used for
	 * locating known scheduler classes by name.
	 */
	public ConstructorRegistry<IScheduler> getRegistry()
	{
		return DimpleEnvironment.active().schedulers();
	}
	
	/**
	 * The option key, if any, for looking up the validator to be used on schedules for this class of scheduler.
	 * @since 0.08
	 */
	public @Nullable ScheduleValidatorOptionKey getValidatorKey()
	{
		return _validatorKey;
	}

	/*-------------------
	 * Protected methods
	 */

	protected Class<? extends IScheduler> validateClass(Class<?> schedulerClass)
	{
		try
		{
			return SchedulerBase.validateClass(schedulerClass);
		}
		catch (IllegalArgumentException ex)
		{
			throw new OptionValidationException("Not valid for option '%s': %s", this, ex.getMessage());
		}
	}
}
