/*
 * 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.qi4j.samples.cargo.app1.model.cargo;

import org.qi4j.api.common.Optional;
import org.qi4j.api.entity.EntityComposite;
import org.qi4j.api.entity.association.Association;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.property.Property;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.api.value.ValueBuilderFactory;
import org.qi4j.samples.cargo.app1.model.handling.HandlingHistory;
import org.qi4j.samples.cargo.app1.model.location.Location;
import org.qi4j.samples.cargo.app1.system.factories.DeliveryFactory;

/**
 * A Cargo. This is the central class in the domain model,
 * and it is the root of the Cargo-Itinerary-Leg-Delivery-RouteSpecification aggregate.
 * <p>
 * A cargo is identified by a unique tracking id, and it always has an origin
 * and a route specification. The life cycle of a cargo begins with the booking procedure,
 * when the tracking id is assigned. During a (short) period of time, between booking
 * and initial routing, the cargo has no itinerary.
 * </p>
 * <p>
 * The booking clerk requests a list of possible routes, matching the route specification,
 * and assigns the cargo to one route. The route to which a cargo is assigned is described
 * by an itinerary.
 * </p>
 * <p>
 * A cargo can be re-routed during transport, on demand of the customer, in which case
 * a new route is specified for the cargo and a new route is requested. The old itinerary,
 * being a value object, is discarded and a new one is attached.
 * </p>
 * <p>
 * It may also happen that a cargo is accidentally misrouted, which should notify the proper
 * personnel and also trigger a re-routing procedure.
 * </p>
 * <p>
 * When a cargo is handled, the status of the delivery changes. Everything about the delivery
 * of the cargo is contained in the Delivery value object, which is replaced whenever a cargo
 * is handled by an asynchronous event triggered by the registration of the handling event.
 * </p>
 * <p>
 * The delivery can also be affected by routing changes, i.e. when a the route specification
 * changes, or the cargo is assigned to a new route. In that case, the delivery update is performed
 * synchronously within the cargo aggregate.
 * </p>
 * <p>
 * The life cycle of a cargo ends when the cargo is claimed by the customer.
 * </p>
 * <p>
 * The cargo aggregate, and the entre domain model, is built to solve the problem
 * of booking and tracking cargo. All important business rules for determining whether
 * or not a cargo is misdirected, what the current status of the cargo is (on board carrier,
 * in port etc), are captured in this aggregate.
 * </p>
 */
@Mixins( Cargo.CargoMixin.class )
public interface Cargo
    extends EntityComposite
{

    TrackingId trackingId();

    Location origin();

    RouteSpecification routeSpecification();

    Itinerary itinerary();

    Delivery delivery();

    /**
     * Specifies a new route for this cargo.
     *
     * @param routeSpecification route specification.
     */
    void specifyNewRoute( final RouteSpecification routeSpecification );

    /**
     * Attach a new itinerary to this cargo.
     *
     * @param itinerary an itinerary. May not be null.
     */
    void assignToRoute( final Itinerary itinerary );

    /**
     * Updates all aspects of the cargo aggregate status
     * based on the current route specification, itinerary and handling of the cargo.
     * <p>
     * When either of those three changes, i.e. when a new route is specified for the cargo,
     * the cargo is assigned to a route or when the cargo is handled, the status must be
     * re-calculated.
     * </p>
     * <p>
     * {@link RouteSpecification} and {@link Itinerary} are both inside the Cargo
     * aggregate, so changes to them cause the status to be updated <b>synchronously</b>,
     * but changes to the delivery history (when a cargo is handled) cause the status update
     * to happen <b>asynchronously</b> since {@link org.qi4j.samples.cargo.app1.model.handling.HandlingEvent}
     * is in a different aggregate.
     * </p>
     *
     * @param handlingHistory handling history
     */
    void deriveDeliveryProgress( final HandlingHistory handlingHistory );

    interface State
    {
        Association<Location> origin();

        Property<RouteSpecification> routeSpecification();

        @Optional
        Property<Itinerary> itinerary();

        @Optional
        Property<Delivery> delivery();
    }

    public abstract class CargoMixin
        implements Cargo
    {

        public static final String TRACKING_ID_PREFIX = "TrackingId:";

        @This
        private State state;

        @Service
        private DeliveryFactory deliveryFactory;

        @Structure
        private ValueBuilderFactory vbf;

        private TrackingId trackingId;

        public TrackingId trackingId()
        {
            if( trackingId == null )
            {
                final String identity = identity().get();
                String id = identity.substring( TRACKING_ID_PREFIX.length() );
                final ValueBuilder<TrackingId> builder = vbf.newValueBuilder( TrackingId.class );
                builder.prototype().id().set( id );
                trackingId = builder.newInstance();
            }
            return trackingId;
        }

        public Location origin()
        {
            return state.origin().get();
        }

        public RouteSpecification routeSpecification()
        {
            return state.routeSpecification().get();
        }

        public Itinerary itinerary()
        {
            return state.itinerary().get();
        }

        public Delivery delivery()
        {
            return state.delivery().get();
        }

        public void specifyNewRoute( final RouteSpecification routeSpecification )
        {
            state.routeSpecification().set( routeSpecification );
            // Handling consistency within the Cargo aggregate synchronously
            state.delivery().set( delivery().updateOnRouting( routeSpecification(), itinerary() ) );
        }

        public void assignToRoute( final Itinerary itinerary )
        {
            state.itinerary().set( itinerary );
            // Handling consistency within the Cargo aggregate synchronously
            state.delivery().set( delivery().updateOnRouting( routeSpecification(), itinerary() ) );
        }

        /**
         * Updates all aspects of the cargo aggregate status
         * based on the current route specification, itinerary and handling of the cargo.
         * <p>
         * When either of those three changes, i.e. when a new route is specified for the cargo,
         * the cargo is assigned to a route or when the cargo is handled, the status must be
         * re-calculated.
         * </p>
         * <p>
         * {@link RouteSpecification} and {@link Itinerary} are both inside the Cargo
         * aggregate, so changes to them cause the status to be updated <b>synchronously</b>,
         * but changes to the delivery history (when a cargo is handled) cause the status update
         * to happen <b>asynchronously</b> since
         * {@link org.qi4j.samples.cargo.app1.model.handling.HandlingEvent} is in a different aggregate.
         * </p>
         * @param handlingHistory handling history
         */
        public void deriveDeliveryProgress( final HandlingHistory handlingHistory )
        {
            // TODO filter events on cargo (must be same as this cargo)

            // Delivery is a value object, so we can simply discard the old one
            // and replace it with a new
            state.delivery().set( deliveryFactory.derivedFrom( routeSpecification(), itinerary(), handlingHistory ) );
        }
    }
}