/*
 * Copyright 1996-2011 Niclas Hedhman.
 *
 * 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 org.qi4j.library.alarm;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import org.qi4j.api.common.Optional;
import org.qi4j.api.common.UseDefaults;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.property.Property;

/**
 * Defines the basic AlarmPoint interface.
 * <p>
 * This is the basic interface for the whole AlarmPoint System. The AlarmPoint
 * is created by calling <code>createAlarm()</code> method in the
 * <code>AlarmSystem</code> or the <code>AlarmModel</code>.
 * </p>
 * <ul>
 * <li>All alarms carries a set of attributes, runtime extendable.</li>
 * <li>All alarms can be activated (on), deactivated (off) and acknowledged.</li>
 * <li>All alarms have an AlarmStatus.</li>
 * <li>All alarms generates AlarmEvents.</li>
 * <li>The behaviour of the AlarmPoint is defined by an AlarmModel.</li>
 * <li>Every AlarmPoint can have its own AlarmModel.</li>
 * <li>Any number of AlarmStates and AlarmEvents can be defined in an AlarmModel.</li>
 * </ul>
 * <p>
 * Alarms can be triggered by an standard trigger, which are java.lang.Strings.
 * 3 triggers are pre-defined; <i>activate</i>,<i>deactivate</i> and
 * <i>acknowledge</i> and must be present in all standard systems and standard impl.
 * </p>
 * <p>
 * The basic usage looks like this;
 * <pre><code>
 *     // Creation
 *     AlarmPoint ala1 = alarmService.createAlarm( "My AlarmPoint" );
 *       :
 *       :
 *     // use
 *     if( alarmcondition )  // The condition should only detect transitions.
 *         ala1.trigger( this, "activate" );
 * </code></pre>
 * <p>
 * It is important to know that every call to <code>trigger()</code>
 * will generate an AlarmEvent, so the <code>trigger()</code> should
 * only be called when the standard condition changes. For this purpose, there is
 * a convenience method, that will create/generate the <code>trigger</code>
 * method calls when a boolean standard condition changes. The usage is fairly
 * simple.
 * </p>
 * <p>
 * Example;
 * </p>
 * <pre><code>
 *     ala1.updateCondition( value &gt; highlimit );
 * </code></pre>
 * <p>
 * It is possible to mix and match the usage of <code>updateCondition()</code>
 * and <code>trigger()</code> methods without any concerns.
 * </p>
 * <p>
 * To create alarms with different AlarmModels, other than the
 * default as shown above, you need to retrieve the AlarmModel that
 * fulfill the needs required. This can be done in the following manner;
 * </p>
 * <pre><code>
 *     AlarmModel[] impl = alarmService.getAlarmModelsAvailable();
 *     // selection algorithm
 *     AlarmPoint ala2 = impl[selected].createAlarm( "My AlarmPoint" );
 * </code></pre>
 * <p>
 * The default AlarmModel can be changed by a call to the
 * <code>AlarmSystem.setDefaultAlarmModel()</code> and
 * ALL ALARMS that has the old AlarmModel assigned to it, will be
 * transferred to the new default AlarmModel. It is important to
 * understand that this is done irregardless of whether the AlarmPoint was
 * created from the <code>AlarmSystem.createAlarm()</code> method or
 * the <code>AlarmModel.createAlarm()</code> method. If distinct different
 * behaviours are required for certain Alarms, and yet want to allow
 * users to freely select AlarmModel for all other Alarms, one need
 * to create two instances of the same AlarmModels, one used solely
 * for the pre-defined AlarmPoint behaviours, and the others for the rest of
 * the Alarms.
 * </p>
 */
public interface AlarmPoint
{
    String STATUS_NORMAL = "Normal";
    String STATUS_ACTIVATED = "Activated";
    String STATUS_DEACTIVATED = "Deactivated";
    String STATUS_REACTIVATED = "Reactivated";
    String STATUS_ACKNOWLEDGED = "Acknowledged";
    String STATUS_DISABLED = "Disabled";
    String STATUS_BLOCKED = "Blocked";

    String EVENT_ENABLING = "enabled";
    String EVENT_DISABLING = "disabled";
    String EVENT_BLOCKING = "blocked";
    String EVENT_UNBLOCKING = "unblocked";
    String EVENT_ACTIVATION = "activation";
    String EVENT_DEACTIVATION = "deactivation";
    String EVENT_ACKNOWLEDGEMENT = "acknowledgement";

    String TRIGGER_ACTIVATE = "activate";
    String TRIGGER_DEACTIVATE = "deactivate";
    String TRIGGER_ACKNOWLEDGE = "acknowledge";
    String TRIGGER_BLOCK = "block";
    String TRIGGER_UNBLOCK = "unblock";
    String TRIGGER_ENABLE = "enable";
    String TRIGGER_DISABLE = "disable";

    /**
     * Trigger a state change.
     * <p>
     * When the AlarmPoint object receives a trigger, it must consult the
     * AlarmModel and figure out if there is an actual state change
     * occurring and if any AlarmEvents should be fired.
     * </p>
     *
     * @param trigger The trigger to execute if existing in the AlarmModel.
     *
     * @throws IllegalArgumentException if a trigger is not a known one.
     */
    void trigger( String trigger )
        throws IllegalArgumentException;

    /**
     * Activates an AlarmPoint.
     * <p>
     * Convenience method for:
     * </p>
     * <pre><code>
     *       trigger( "activate" );
     * </code>
     * </pre>
     */
    void activate();

    /**
     * Deactivates an AlarmPoint.
     * Convinience method for:<pre>
     *     trigger( "deactivate" );
     * </pre>
     */
    void deactivate();

    /**
     * Acknowledges an AlarmPoint.
     * Convinience method for:<pre>
     *     trigger( source, "acknowledge" );
     * </pre>
     */
    void acknowledge();

    /**
     * Get AlarmPoint condition.
     * To reduce AlarmPoint condition calculations for Implementors, each AlarmPoint should
     * be able to work with a "true/false" trigger. Only changes to this trigger
     * will cause an event.
     *
     * @return The condition of the AlarmPoint, which is used to simplify trigging of activate and deactivate.
     */
    boolean currentCondition();

    /**
     * Set AlarmPoint condition.
     * To reduce AlarmPoint condition calculations for Implementors, each AlarmPoint should
     * be able to work with a "true/false" trigger. Only changes to this trigger
     * will cause an event.
     * Causes an Activation or Deactivation if state of condition changes.
     *
     * @param condition Sets the AlarmPoint condition.
     */
    void updateCondition( boolean condition );

    /**
     * Returns the current state of the standard.
     *
     * @return The AlarmStatus (interface) object
     */
    AlarmStatus currentStatus();

    /**
     * Returns the AlarmHistory of the standard.
     *
     * @return The AlarmHistory object, or null if AlarmHistory is not supported.
     */
    AlarmHistory history();

    /**
     * Return all attribute names
     *
     * @return the names of the attributes of this AlarmPoint.
     */
    List<String> attributeNames();

    /**
     * Return the attribute of the given name.
     *
     * @param name The name of the attribute to return.
     *
     * @return the named attribute of this AlarmPoint.
     */
    String attribute( String name );

    /**
     * Sets the attribute of the given name.
     *
     * @param name  The name of the attribute to set.
     * @param value The value to set the named attribute to.
     */
    void setAttribute( String name, @Optional String value );

    /**
     * Returns the Name of the AlarmPoint.
     * This normally returns the human readable technical name of the AlarmPoint.
     *
     * @return the name of the AlarmPoint.
     */
    String name();

    /**
     * Returns a Description of the AlarmPoint.
     * This normally returns a full Description of the AlarmPoint in the
     * default Locale.
     *
     * @return a human-readable description of the AlarmPoint in the default locale.
     */
    String descriptionInDefaultLocale();

    /**
     * Returns a Description of the AlarmPoint.
     * This normally returns a full Description of the AlarmPoint in the
     * Locale. If Locale is <code><b>null</b></code>, then the
     * default Locale is used.
     *
     * @param locale The locale to return the description in, or null to use default locale.
     *
     * @return a human-readable description of the AlarmPoint in the given locale.
     */
    String description( Locale locale );

    /**
     * The {@link AlarmClass} of the AlarmPoint.
     *
     * The {@link AlarmClass} indicates the urgency of {@link AlarmEvent}s emitted from the AlarmPoint. The property
     * has {@link UseDefaults} annotation so that the application developer can set the default during assembly.
     * The default {@link org.qi4j.bootstrap.Assembler} in this library defaults alarms to {@link AlarmClass} <b>B</b>.
     *
     * @return the property instance that contains the {@link AlarmClass}.
     */
    @UseDefaults
    Property<AlarmClass> alarmClass();

    /**
     * The {@link AlarmCategory} of this AlarmPoint.
     *
     * AlarmCategory is used to group Alarms together, which can be used to forward {@link AlarmEvent} to different
     * destinations, produce reports for different target audiences or separation of aggregation.
     *
     * @return the property instance that contains the {@link AlarmCategory}.
     */
    Property<AlarmCategory> category();

    /**
     * The AlarmState is an internal type, used inside the AlarmPoint for all state that needs to be persisted on disk
     * and/or transferred across networks.
     */
    interface AlarmState
    {
        @AlarmNameFormat
        Property<String> systemName();

        @Optional
        Property<String> description();

        @UseDefaults
        Property<Map<String, String>> attributes();

        Property<AlarmStatus> currentStatus();
    }

    abstract class AlarmPointMixin
        implements AlarmPoint
    {
        @Service
        private AlarmModel model;

        @Service
        private AlarmSystem alarmSystem;

        @This
        private AlarmPoint me;

        @This
        private AlarmState state;

        @This
        private AlarmHistory history;

        @Override
        public void setAttribute( String name, String value )
        {
            Map<String, String> properties = state.attributes().get();
            if( value == null )
            {
                properties.remove( name );
            }
            else
            {
                properties.put( name, value );
            }
            state.attributes().set( properties );
        }

        @Override
        public String attribute( String name )
        {
            return state.attributes().get().get( name );
        }

        @Override
        public List<String> attributeNames()
        {
            ArrayList<String> result = new ArrayList<String>();
            for( String name : state.attributes().get().keySet() )
            {
                result.add( name );
            }
            return result;
        }

        private void fireAlarm( AlarmEvent event )
        {
            // TODO: Should possibly just be delegated to AlarmSystem
            for( AlarmListener listener : alarmSystem.alarmListeners() )
            {
                try
                {
                    listener.alarmFired( event );
                }
                catch( Exception e )
                {

                    // ignore.
                }
            }
        }

        @Override
        public String toString()
        {
            return "AlarmPoint[" + name() + " : " + state.currentStatus().get().name(null)
                   + "  : " + descriptionInDefaultLocale() + "]";
        }

        @Override
        public void trigger( String trigger )
        {
            AlarmEvent event;
            synchronized( this )
            {
                event = model.evaluate( me, trigger );
                if( event == null )
                {
                    return;
                }
                state.currentStatus().set( event.newStatus().get() );
                history.addEvent( event, trigger );
            }
            fireAlarm( event );
        }

        @Override
        public void activate()
        {
            trigger( AlarmPoint.TRIGGER_ACTIVATE );
        }

        @Override
        public void deactivate()
        {
            trigger( AlarmPoint.TRIGGER_DEACTIVATE );
        }

        @Override
        public void acknowledge()
        {
            trigger( AlarmPoint.TRIGGER_ACKNOWLEDGE );
        }

        @Override
        public AlarmHistory history()
        {
            return history;
        }

        @Override
        public AlarmStatus currentStatus()
        {
            return state.currentStatus().get();
        }

        /**
         * Returns the Name of the AlarmPoint.
         * This normally returns the human readable technical name of the AlarmPoint.
         */
        @Override
        public String name()
        {
            return state.systemName().get();
        }

        /**
         * Returns a Description of the AlarmPoint.
         * This normally returns a full Description of the AlarmPoint in the
         * default Locale.
         */
        @Override
        public String descriptionInDefaultLocale()
        {
            return description( null );
        }

        /**
         * Returns a Description of the AlarmPoint.
         * This normally returns a full Description of the AlarmPoint in the
         * Locale. If Locale is <code><b>null</b></code>, then the
         * default Locale is used.
         */
        @Override
        public String description( Locale locale )
        {
            if( locale == null )
            {
                locale = Locale.getDefault();
            }
            ResourceBundle rb = ResourceBundle.getBundle( "org.qi4j.library.alarm.user.AlarmDescriptions", locale );
            return rb.getString( name() );
        }

        @Override
        public void updateCondition( boolean condition )
        {
            String trig = model.computeTrigger( state.currentStatus().get(), condition );
            if( trig != null )
            {
                trigger( trig );
            }
        }

        @Override
        public boolean currentCondition()
        {
            return model.computeCondition( state.currentStatus().get() );
        }
    }
}
