IBaseAgent.java

/*
 * @cond LICENSE
 * ######################################################################################
 * # LGPL License                                                                       #
 * #                                                                                    #
 * # This file is part of the LightJason AgentSpeak(L++)                                #
 * # Copyright (c) 2015-19, LightJason (info@lightjason.org)                            #
 * # This program is free software: you can redistribute it and/or modify               #
 * # it under the terms of the GNU Lesser General Public License as                     #
 * # published by the Free Software Foundation, either version 3 of the                 #
 * # License, or (at your option) any later version.                                    #
 * #                                                                                    #
 * # This program is distributed in the hope that it will be useful,                    #
 * # but WITHOUT ANY WARRANTY; without even the implied warranty of                     #
 * # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                      #
 * # GNU Lesser General Public License for more details.                                #
 * #                                                                                    #
 * # You should have received a copy of the GNU Lesser General Public License           #
 * # along with this program. If not, see http://www.gnu.org/licenses/                  #
 * ######################################################################################
 * @endcond
 */

package org.lightjason.agentspeak.agent;

import com.codepoetics.protonpack.StreamUtils;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.TreeMultimap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.lightjason.agentspeak.beliefbase.view.IView;
import org.lightjason.agentspeak.common.IPath;
import org.lightjason.agentspeak.configuration.IAgentConfiguration;
import org.lightjason.agentspeak.error.CIllegalArgumentException;
import org.lightjason.agentspeak.language.CCommon;
import org.lightjason.agentspeak.language.CLiteral;
import org.lightjason.agentspeak.language.ILiteral;
import org.lightjason.agentspeak.language.IStructureHash;
import org.lightjason.agentspeak.language.ITerm;
import org.lightjason.agentspeak.language.execution.IContext;
import org.lightjason.agentspeak.language.execution.IVariableBuilder;
import org.lightjason.agentspeak.language.fuzzy.CFuzzyValue;
import org.lightjason.agentspeak.language.fuzzy.IFuzzyValue;
import org.lightjason.agentspeak.language.fuzzy.operator.IFuzzyBundle;
import org.lightjason.agentspeak.language.instantiable.plan.statistic.CPlanStatistic;
import org.lightjason.agentspeak.language.instantiable.plan.statistic.IPlanStatistic;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.CTrigger;
import org.lightjason.agentspeak.language.instantiable.plan.trigger.ITrigger;
import org.lightjason.agentspeak.language.instantiable.rule.IRule;
import org.lightjason.agentspeak.language.unify.IUnifier;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;


/**
 * agent base structure
 *
 * @tparam T agent type
 */
public abstract class IBaseAgent<T extends IAgent<?>> implements IAgent<T>
{
    /**
     * logger
     */
    protected static final Logger LOGGER = org.lightjason.agentspeak.common.CCommon.logger( IAgent.class );
    /**
     * serial id
     */
    private static final long serialVersionUID = -304366902555398136L;
    /**
     * beliefbase
     */
    protected final IView m_beliefbase;
    /**
     * storage map
     *
     * @note must be thread-safe
     */
    protected final Map<String, Object> m_storage = new ConcurrentHashMap<>();
    /**
     * execution trigger with content hash
     */
    protected final Map<Integer, ITrigger> m_trigger = new ConcurrentHashMap<>();
    /**
     * multimap with rules
     */
    protected final Multimap<IPath, IRule> m_rules = Multimaps.synchronizedMultimap( LinkedHashMultimap.create() );
    /**
     * map with all existing plans and successful / fail runs
     */
    protected final Multimap<ITrigger, IPlanStatistic> m_plans = Multimaps.synchronizedMultimap(
                                                                    TreeMultimap.create( IStructureHash.COMPARATOR, Comparator.<IPlanStatistic>naturalOrder() ) );
    /**
     * nano seconds at the last cycle
     */
    private final AtomicLong m_cycletime = new AtomicLong();
    /**
     * number of sleeping cycles
     *
     * @note values >= 0 defines the sleeping time, Long.MAX_VALUE is infinity sleeping
     * negative values defines the activity
     */
    private final AtomicLong m_sleepingcycles = new AtomicLong( Long.MIN_VALUE );
    /**
     * set for waking-up literals
     */
    private final Set<ITerm> m_sleepingterm = Collections.synchronizedSet( new HashSet<>() );
    /**
     * unifier
     */
    private final IUnifier m_unifier;
    /**
     * variable builder
     */
    private final IVariableBuilder m_variablebuilder;
    /**
     * fuzzy result collector
     */
    private final IFuzzyBundle<Boolean> m_fuzzy;
    /**
     * running plans (thread-safe)
     */
    private final Multimap<IPath, ILiteral> m_runningplans = Multimaps.synchronizedSetMultimap( HashMultimap.create() );



    /**
     * ctor
     *
     * @param p_configuration agent configuration
     */
    public IBaseAgent( @Nonnull final IAgentConfiguration<T> p_configuration )
    {
        // initialize agent
        m_unifier = p_configuration.unifier();
        m_beliefbase = p_configuration.beliefbase();
        m_variablebuilder = p_configuration.variablebuilder();
        m_fuzzy = p_configuration.fuzzy();

        // initial plans and rules
        p_configuration.plans().parallelStream().forEach( i -> m_plans.put( i.trigger(), CPlanStatistic.from( i ) ) );
        p_configuration.rules().parallelStream().forEach( i -> m_rules.put( i.identifier().fqnfunctor(), i ) );
        if ( Objects.nonNull( p_configuration.initialgoal() ) )
            m_trigger.put( p_configuration.initialgoal().hashCode(), p_configuration.initialgoal() );
    }

    @Nonnull
    @Override
    public final IView beliefbase()
    {
        return m_beliefbase;
    }

    @Nonnull
    @Override
    @SafeVarargs
    @SuppressWarnings( "varargs" )
    public final <N extends IInspector> Stream<N> inspect( @Nonnull final N... p_inspector )
    {
        return Arrays.stream( p_inspector )
                     .parallel()
                     .peek( i ->
                     {
                         i.inspectcycletime( m_cycletime.get() );
                         i.inspectsleeping( m_sleepingcycles.get() );
                         i.inspectbelief( m_beliefbase.stream() );
                         i.inspectplans( m_plans.values().stream() );
                         i.inspectrunningplans( m_runningplans.values().stream() );
                         i.inspectstorage( m_storage.entrySet().stream() );
                         i.inspectrules( m_rules.values().stream() );
                     } );
    }

    @Nonnull
    @Override
    public final Multimap<IPath, ILiteral> runningplans()
    {
        return ImmutableMultimap.copyOf( m_runningplans );
    }

    @Override
    public final boolean sleeping()
    {
        return m_sleepingcycles.get() > 0;
    }

    @Nonnull
    @Override
    public final IAgent<T> sleep( final long p_cycles, final ITerm... p_term )
    {
        return this.sleep(
            p_cycles,
            ( Objects.isNull( p_term ) ) || ( p_term.length == 0 )
            ? Stream.of()
            : Arrays.stream( p_term )
        );
    }

    @Nonnull
    @Override
    public final IAgent<T> sleep( final long p_cycles, @Nonnull final Stream<ITerm> p_literal )
    {
        m_sleepingcycles.set( p_cycles );
        p_literal.filter( i -> !i.hasVariable() ).forEach( m_sleepingterm::add );
        return this;
    }

    @Nonnull
    @Override
    public final IAgent<T> wakeup( @Nullable final ITerm... p_term )
    {
        return this.wakeup(
            ( Objects.isNull( p_term ) ) || ( p_term.length == 0 )
            ? Stream.of()
            : Arrays.stream( p_term )
        );
    }

    @Nonnull
    @Override
    public final IAgent<T> wakeup( @Nonnull final Stream<ITerm> p_term )
    {
        p_term.forEach( m_sleepingterm::add );
        this.active( true );
        return this;
    }

    @Nonnull
    @Override
    public final Map<String, Object> storage()
    {
        return m_storage;
    }

    @Nonnull
    @Override
    public final IUnifier unifier()
    {
        return m_unifier;
    }

    @Nonnegative
    @Override
    public final long cycletime()
    {
        return m_cycletime.get();
    }

    @Nonnull
    @Override
    public final Multimap<ITrigger, IPlanStatistic> plans()
    {
        return m_plans;
    }

    @Nonnull
    @Override
    public final IFuzzyBundle<Boolean> fuzzy()
    {
        return m_fuzzy;
    }

    @Nonnull
    @Override
    public final IVariableBuilder variablebuilder()
    {
        return m_variablebuilder;
    }

    @Nonnull
    @Override
    public final Multimap<IPath, IRule> rules()
    {
        return m_rules;
    }

    @Nonnull
    @Override
    @SuppressWarnings( "unchecked" )
    public final <N extends IAgent<?>> N raw()
    {
        return (N) this;
    }

    @Override
    public String toString()
    {
        return MessageFormat.format(
            "{0} ( {1} )",
            super.toString(),
            StringUtils.join(
                StreamUtils.zip(
                    Stream.of( "Trigger", "Running Plans", "Beliefbase" ),
                    Stream.of( m_trigger.values(), m_runningplans.keySet(), m_beliefbase ),
                    ( l, c ) -> MessageFormat.format( "{0}: {1}", l, c )
                ).toArray(),
                " / "
            )
        );
    }

    @Nonnull
    @Override
    public final IFuzzyValue<Boolean> trigger( @Nonnull final ITrigger p_trigger, @Nullable final boolean... p_immediately )
    {
        if ( m_sleepingcycles.get() > 0 )
            return CFuzzyValue.from( false );

        // check if literal does not store any variables
        if ( p_trigger.literal().hasVariable() )
            throw new CIllegalArgumentException( org.lightjason.agentspeak.common.CCommon.languagestring( this, "literalvariable", p_trigger ) );

        // run plan immediatly and return
        if ( ( Objects.nonNull( p_immediately ) ) && ( p_immediately.length > 0 ) && ( p_immediately[0] ) )
            return this.execute( this.generateexecution( Stream.of( p_trigger ) ) );

        // add trigger for the next cycle must be synchronized to avoid indeterministic state during execution
        synchronized ( this )
        {
            m_trigger.putIfAbsent( p_trigger.hashCode(), p_trigger );
        }

        return CFuzzyValue.from( true );
    }

    @Override
    @SuppressWarnings( "unchecked" )
    public T call() throws Exception
    {
        // run beliefbase update, because environment can be changed and decrement sleeping value
        m_beliefbase.update( (T) this );
        if ( !this.active( false ) )
            // check wakup-event otherwise suspend
            return (T) this;

        // update defuzzification
        m_fuzzy.getValue().update( this );

        // clear running plan- and trigger list and execute elements
        this.execute( this.generateexecutionlist() );


        // set the cycle time
        m_cycletime.set( System.nanoTime() );

        return (T) this;
    }

    /**
     * create the plan executionlist with clearing internal structures
     *
     * @note must be synchronized for avoid indeterministic trigger list
     *
     * @return collection with execution plan and context
     */
    @Nonnull
    private synchronized Collection<Pair<IPlanStatistic, IContext>> generateexecutionlist()
    {
        m_runningplans.clear();
        final Collection<Pair<IPlanStatistic, IContext>> l_execution = this.generateexecution(
            Stream.concat(
                m_trigger.values().parallelStream(),
                m_beliefbase.trigger().parallel()
            )
        );
        m_trigger.clear();

        return l_execution;
    }


    /**
     * create execution list with plan and context
     *
     * @param p_trigger trigger stream
     * @return collection with excutable plans, instantiated execution context and plan statistic
     */
    @Nonnull
    private Collection<Pair<IPlanStatistic, IContext>> generateexecution( @Nonnull final Stream<ITrigger> p_trigger )
    {
        return p_trigger
            .filter( Objects::nonNull )
            // get all possible plans
            .flatMap( i -> m_plans.get( i ).stream().map( j -> new ImmutablePair<>( i, j ) ) )
            .parallel()
            // tries to unify trigger literal and filter of valid unification (returns set of unified variables)
            .map( i -> new ImmutablePair<>( i, CCommon.unifytrigger( m_unifier, i.getLeft(), i.getRight().plan().trigger() ) ) )
            // check if unification was possible
            .filter( i -> i.getRight().getLeft() )
            // create execution context
            .map( i -> CCommon.instantiateplan( i.getLeft().getRight(), this, i.getRight().getRight() ) )
            // check plan-condition
            .filter( i -> m_fuzzy.getValue().defuzzify( i.getLeft().plan().condition( i.getRight() ) ) )
            // collectors-call must be toList not toSet because plan-execution can be have equal elements
            // so a set avoid multiple plan-execution
            .collect( Collectors.toList() );
    }

    /**
     * execute list of plans
     *
     * @param p_execution execution collection with instantiated plans and context
     * @return fuzzy result for each executaed plan
     */
    @Nonnull
    private IFuzzyValue<Boolean> execute( @Nonnull final Collection<Pair<IPlanStatistic, IContext>> p_execution )
    {
        // update executable plan list, so that test-goals are defined all the time
        p_execution.parallelStream().forEach( i -> m_runningplans.put(
            i.getLeft().plan().trigger().literal().fqnfunctor(),
            i.getLeft().plan().trigger().literal().unify( i.getRight() )
        ) );

        // execute plan and return values and return execution result
        return p_execution.parallelStream()
                          .map( i ->
                          {
                              final IFuzzyValue<Boolean> l_result = i.getLeft()
                                                                     .plan()
                                                                     .execute( false, i.getRight(), Collections.emptyList(), Collections.emptyList() );
                              if ( m_fuzzy.getValue().defuzzify( l_result ) )
                                  // increment successful runs
                                  i.getLeft().incrementsuccessful();
                              else
                                  // increment failed runs and create delete goal-event
                                  i.getLeft().incrementfail();
                              return l_result;
                          } ).collect( m_fuzzy.getKey() );
    }

    /**
     * runs the wakeup goal
     *
     * @param p_immediatly runs the wake always
     * @return returns true if the agent is active
     */
    private boolean active( final boolean p_immediatly )
    {
        // if the sleeping time ends or the agent will wakedup by a hard call,
        // create the trigger and reset the time value
        if ( ( m_sleepingcycles.compareAndSet( 0, Long.MIN_VALUE ) ) || p_immediatly )
        {
            (
                m_sleepingterm.isEmpty()

                ? Stream.of( CTrigger.from(
                    ITrigger.EType.ADDGOAL, CLiteral.from(
                    "wakeup"
                    )
                ) )

                : m_sleepingterm.parallelStream()
                                .map( i -> CTrigger.from(
                                    ITrigger.EType.ADDGOAL,
                                    CLiteral.from( "wakeup", i )
                                ) )

            ).forEach( i -> m_trigger.put( i.structurehash(), i ) );

            m_sleepingterm.clear();
        }

        // if the sleeping time is not infinity decrese the counter
        if ( ( m_sleepingcycles.get() > 0 ) && ( m_sleepingcycles.get() != Long.MAX_VALUE ) )
            m_sleepingcycles.decrementAndGet();

        return m_sleepingcycles.get() <= 0;
    }

}