/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.polygene.library.alarm;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.polygene.api.entity.EntityBuilder;
import org.apache.polygene.api.identity.StringIdentity;
import org.apache.polygene.api.injection.scope.Service;
import org.apache.polygene.api.injection.scope.Structure;
import org.apache.polygene.api.mixin.Mixins;
import org.apache.polygene.api.query.Query;
import org.apache.polygene.api.query.QueryBuilder;
import org.apache.polygene.api.query.QueryBuilderFactory;
import org.apache.polygene.api.service.ServiceReference;
import org.apache.polygene.api.unitofwork.UnitOfWork;
import org.apache.polygene.api.unitofwork.UnitOfWorkFactory;
import org.apache.polygene.api.value.ValueBuilder;
import org.apache.polygene.api.value.ValueBuilderFactory;

/**
 * Defines the AlarmSystem interface.
 *
 * <p>
 * The <code>AlarmSystem</code> is a central registry/handler for all
 * <code>AlarmPoint</code> objects. By registering <code>AlarmListener</code>s
 * to the AlarmSystem, objects are able to 'ignore' the fact that there
 * are many <code>AlarmPoint</code> objects in the system.
 * </p>
 * <p>
 * Also, new attributes registered with the AlarmSystem will propagate into
 * all existing and future <code>AlarmPoint</code>s, whereas attributes at <code>
 * AlarmPoint</code> level is individual to an <code>AlarmPoint</code>.
 * </p>
 * <p>
 * Many different AlarmModels can co-exist in the same application.
 * In fact, every AlarmPoint can have its own AlarmModel, and the AlarmModel
 * can be changed in runtime, for unrivaled flexibility. However, typically
 * the AlarmModel is set in the AlarmSystem only, and all Alarms will
 * use the default model.
 * </p>
 * <p>The default alarm model is a service set by during assembly;
 * </p>
 * <pre><code>
 *   public void assemble( ModuleAssembly module )
 *       throws AssemblyException
 *   {
 *       module.addServices( AlarmSystemService.class );
 *       module.addServices( StandardAlarmModelService.class );
 *   }
 * </code></pre>
 *
 * @author Niclas Hedhman
 */
@Mixins( AlarmSystemService.AlarmSystemMixin.class )
public interface AlarmSystem
{

    /**
     * Returns all the AlarmModels that has been installed.
     *
     * @return all the AlarmModels that has been installed.
     */
    List<AlarmModelDescriptor> alarmModels();

    /**
     * Returns the default AlarmModel.
     *
     * @return the default AlarmModel in this AlarmSystem.
     */
    AlarmModel defaultAlarmModel();

    /**
     * Returns a list of all Alarms registered to the service.
     * <p>
     * The returned Collection may not be modified in any way. The
     * implementation is free to return a clone, but not required to do
     * so, and may decide to terminate if the collection is modified.
     * </p>
     *
     * @return a list of all Alarms registered to the service.
     */
    Query<AlarmPoint> alarmList();

    /**
     * Creates an AlarmPoint with the default AlarmModel.
     *
     * @param name the name of the AlarmPoint to be created.
     *
     * @param category The category the created AlarmPoint should belong to.
     *
     * @return the created AlarmPoint with the given name using the default AlarmModel.
     */
    AlarmPoint createAlarm( String name, AlarmCategory category );

    /**
     * Register AlarmListener to recieve <code>AlarmEvents</code> from all
     * <code>Alarms</code> managed by this <code>AlarmSystem</code>.
     *
     * @param listener the global listener to be added.
     */
    void addAlarmListener( AlarmListener listener );

    /**
     * Remove the <code>AlarmListener</code> from the <code>AlarmSystem</code>.
     *
     * @param listener the global listener to be removed.
     */
    void removeAlarmListener( AlarmListener listener );

    /**
     * Returns an immmutable list of all AlarmListeners registered to the service.
     *
     * @return a list of all AlarmListeners registered to the service.
     */
    List<AlarmListener> alarmListeners();

    /**
     * AlarmSystem implementation.
     */
    class AlarmSystemMixin
        implements AlarmSystem
    {
        @Service
        private Iterable<ServiceReference<AlarmModel>> models;

        private final CopyOnWriteArrayList<AlarmListener> alarmListeners;

        @Structure
        private UnitOfWorkFactory uowf;

        @Structure
        private ValueBuilderFactory vbf;

        @Structure
        private QueryBuilderFactory qbf;

        public AlarmSystemMixin()
        {
            alarmListeners = new CopyOnWriteArrayList<>();
        }

        /**
         * Returns all the AlarmModels that has been installed.
         */
        @Override
        public List<AlarmModelDescriptor> alarmModels()
        {
            List<AlarmModelDescriptor> descriptors = new ArrayList<>();
            for( ServiceReference<AlarmModel> model : models )
            {
                descriptors.add( model.metaInfo( AlarmModelDescriptor.class ) );
            }
            return descriptors;
        }

        /**
         * Returns the default AlarmModel.
         */
        @Override
        public AlarmModel defaultAlarmModel()
        {
            AlarmModelDescriptor defaultDefault = null;
            for( AlarmModelDescriptor descriptor : alarmModels() )
            {
                if( descriptor.isDefaultModel() )
                {
                    return alarmModel( descriptor );
                }
                defaultDefault = descriptor;
            }
            return alarmModel( defaultDefault );
        }

        private AlarmModel alarmModel( AlarmModelDescriptor descriptor )
        {
            for( ServiceReference<AlarmModel> model : models )
            {
                if( model.metaInfo( AlarmModelDescriptor.class ).equals( descriptor ) )
                {
                    return model.get();
                }
            }
            return null;
        }

        /**
         * Creates an AlarmPoint with the default AlarmModel.
         * @param name The system name of the AlarmPoint.
         * @param category The AlarmPoint Category the created alarm should belong to.
         */
        @Override
        public AlarmPoint createAlarm( String name, AlarmCategory category )
        {
            UnitOfWork uow = uowf.currentUnitOfWork();
            EntityBuilder<AlarmPoint> builder = uow.newEntityBuilder( AlarmPoint.class, StringIdentity.identityOf( name ) );
            builder.instance().category().set( category );
            AlarmPoint.AlarmState state = builder.instanceFor( AlarmPoint.AlarmState.class );
            state.systemName().set( name );
            state.currentStatus().set( createStatus( AlarmPoint.STATUS_NORMAL ) );
            return builder.newInstance();
        }

        private AlarmStatus createStatus( String status )
        {
            ValueBuilder<AlarmStatus> builder = vbf.newValueBuilder( AlarmStatus.class );
            AlarmStatus.State statePrototype = builder.prototypeFor( AlarmStatus.State.class );
            statePrototype.name().set( status );
            statePrototype.creationDate().set( Instant.now() );
            return builder.newInstance();
        }

        /**
         * Register AlarmListener to recieve <code>AlarmEvents</code> from all
         * <code>Alarms</code> managed by this <code>AlarmSystem</code>.
         */
        @Override
        public void addAlarmListener( AlarmListener listener )
        {
            alarmListeners.add( listener );
        }

        /**
         * Remove the <code>AlarmListener</code> from the <code>AlarmSystem</code>.
         */
        @Override
        public void removeAlarmListener( AlarmListener listener )
        {
            alarmListeners.remove( listener );
        }

        /**
         * Returns a list of all Alarms registered to the service.
         */
        @Override
        public Query<AlarmPoint> alarmList()
        {
            UnitOfWork uow = uowf.currentUnitOfWork();
            QueryBuilder<AlarmPoint> builder = qbf.newQueryBuilder( AlarmPoint.class );
            return uow.newQuery( builder );
        }

        @Override
        public List<AlarmListener> alarmListeners()
        {
            synchronized( alarmListeners )
            {
                return alarmListeners;
            }
        }

        public void alarmFired( AlarmEvent event )
        {
            Iterator list;
            //noinspection SynchronizeOnNonFinalField
            synchronized( alarmListeners )
            {
                list = alarmListeners.iterator();
            }
            while( list.hasNext() )
            {
                AlarmListener listener = (AlarmListener) list.next();
                try
                {
                    listener.alarmFired( event );
                }
                catch( Exception e )
                {
                    // TODO: Utilize a logger system instead.
                    System.err.println( "Exception in AlarmListener: " + listener );
                    e.printStackTrace();
                }
            }
        }
    }
}
