/*
 * Copyright 2011 Marc Grue.
 *
 * 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.sample.dcicargo.sample_a.context.shipping.booking;

import java.util.Date;
import java.util.Iterator;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.cargo.Cargo;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.cargo.RouteSpecification;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.delivery.Delivery;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.delivery.ExpectedHandlingEvent;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.delivery.RoutingStatus;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.delivery.TransportStatus;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.handling.HandlingEvent;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.handling.HandlingEventType;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.itinerary.Itinerary;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.itinerary.Leg;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.location.Location;
import org.qi4j.sample.dcicargo.sample_a.data.shipping.voyage.Voyage;
import org.qi4j.sample.dcicargo.sample_a.infrastructure.dci.Context;
import org.qi4j.sample.dcicargo.sample_a.infrastructure.dci.RoleMixin;

/**
 * Build Delivery Snapshot use case.
 *
 * This is a subfunction level use case (see Cockurn 2001) supporting user-goal level use cases
 * like {@link BookNewCargo} by returning a Delivery snapshot Value object.
 *
 * A Delivery snapshot describes all we know about the current shipping status of a Cargo. It's the
 * heart of the application domain knowledge.
 *
 * The Delivery snapshot is derived from one of the 3 overall stages a Cargo shipping can be in:
 * 1) Cargo is being created  (Route Specification is known)
 * 2) Cargo has been routed   (Route Specification + Itinerary is known)
 * 3) Cargo is in transit     (Route Specification + Itinerary + last Handling Event is known)
 *
 * Code is organized along the contours of the process flow of cargo shipping instead of the data
 * structures. This makes it easier to reason about a certain step in the shipping process (use case)
 * according to the intuitive understanding of the shipping domain.
 *
 * @see RouteSpecification  plays a (methodful) Role of a factory that coordinates the Delivery build.
 * @see HandlingEvent       plays a (methodful) Role that coordinates deriving data from the last Handling Event.
 * @see Itinerary           plays a (methodful) Role supporting the other two roles with Itinerary calculations.
 */
public class BuildDeliverySnapshot extends Context
{
    // ROLES ---------------------------------------------------------------------

    private FactoryRole factory;
    private ItineraryRole itinerary;
    private HandlingEventRole handlingEvent;

    private Delivery newDeliverySnapshot;

    // CONTEXT CONSTRUCTORS ------------------------------------------------------

    public BuildDeliverySnapshot( RouteSpecification routeSpecification )
    {
        // Deviation 2a
        if( routeSpecification.origin().get() == routeSpecification.destination().get() )
        {
            throw new RouteException( "Route specification is invalid. Origin equals destination." );
        }

        // Deviation 2b
        if( routeSpecification.arrivalDeadline().get().before( new Date() ) )
        {
            throw new RouteException( "Arrival deadline is in the past or Today." +
                                        "\nDeadline " + routeSpecification.arrivalDeadline().get() +
                                        "\nToday    " + new Date() );
        }

        factory = rolePlayer( FactoryRole.class, routeSpecification );
        itinerary = null;
        handlingEvent = null;
    }

    public BuildDeliverySnapshot( Cargo cargo )
    {
        factory = rolePlayer( FactoryRole.class, cargo.routeSpecification().get() );
        itinerary = rolePlayer( ItineraryRole.class, cargo.itinerary().get() );
        handlingEvent = rolePlayer( HandlingEventRole.class, cargo.delivery().get().lastHandlingEvent().get() );
    }

    public BuildDeliverySnapshot( Cargo cargo, HandlingEvent registeredHandlingEvent )
    {
        factory = rolePlayer( FactoryRole.class, cargo.routeSpecification().get() );
        itinerary = rolePlayer( ItineraryRole.class, cargo.itinerary().get() );
        handlingEvent = rolePlayer( HandlingEventRole.class, registeredHandlingEvent );
    }

    // INTERACTIONS --------------------------------------------------------------

    public Delivery get()
    {
        return factory.deriveDeliverySnapshot();
    }

    // METHODFUL ROLE IMPLEMENTATIONS --------------------------------------------

    /**
     * The FactoryRole coordinates the overall Delivery snapshot build.
     *
     * When the Cargo has a delivery history the FactoryRole delegates to the LastHandlingEventRole
     * to derive data from the last HandlingEvent.
     */
    @Mixins( FactoryRole.Mixin.class )
    public interface FactoryRole
    {
        void setContext( BuildDeliverySnapshot context );

        Delivery deriveDeliverySnapshot();

        class Mixin
            extends RoleMixin<BuildDeliverySnapshot>
            implements FactoryRole
        {
            @This
            RouteSpecification routeSpecification;

            ValueBuilder<Delivery> deliveryBuilder;

            public Delivery deriveDeliverySnapshot()
            {
                /*
               * Default values:
               *
               * isMisdirected = false
               * nextExpectedHandlingEvent = null
               * lastHandlingEvent = null
               * lastKnownLocation = null
               * currentVoyage = null
               * eta = null
               * isUnloadedAtDestination = false
               *
               * */

                // Build delivery snapshot object
                deliveryBuilder = vbf.newValueBuilder( Delivery.class );
                context.newDeliverySnapshot = deliveryBuilder.prototype();
                context.newDeliverySnapshot.timestamp().set( new Date() );

                // Deviation 2c: Cargo is not routed yet
                if( context.itinerary == null )
                {
                    return deriveWithRouteSpecification();
                }

                if( !routeSpecification.isSatisfiedBy( (Itinerary) context.itinerary ) )
                {
                    // Deviation 2d: Itinerary not satisfying
                    context.newDeliverySnapshot.routingStatus().set( RoutingStatus.MISROUTED );
                }
                else
                {
                    // Step 2
                    context.newDeliverySnapshot.routingStatus().set( RoutingStatus.ROUTED );
                    context.newDeliverySnapshot.eta().set( context.itinerary.eta() );
                }

                // Deviation 3a: Cargo has no handling history yet
                if( context.handlingEvent == null )
                {
                    return deriveWithItinerary();
                }

                // Step 4: Cargo has a handling history
                context.handlingEvent.deriveWithHandlingEvent();

                // Step 5: Return Delivery object
                return deliveryBuilder.newInstance();
            }

            private Delivery deriveWithRouteSpecification()
            {
                // Deviation 2c
                context.newDeliverySnapshot.routingStatus().set( RoutingStatus.NOT_ROUTED );
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.NOT_RECEIVED );
                context.newDeliverySnapshot.nextExpectedHandlingEvent().set( buildExpectedReceiveEvent() );
                return deliveryBuilder.newInstance();
            }

            private Delivery deriveWithItinerary()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.NOT_RECEIVED );

                // Deviation 3a.1.a
                if( context.newDeliverySnapshot.routingStatus().get() == RoutingStatus.ROUTED )
                {
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( buildExpectedReceiveEvent() );
                }

                return deliveryBuilder.newInstance();
            }

            private ExpectedHandlingEvent buildExpectedReceiveEvent()
            {
                ValueBuilder<ExpectedHandlingEvent> builder = vbf.newValueBuilder( ExpectedHandlingEvent.class );
                builder.prototype().handlingEventType().set( HandlingEventType.RECEIVE );
                builder.prototype().location().set( routeSpecification.origin().get() );
                return builder.newInstance();
            }
        }
    }

    /**
     * The HandlingEventRole derives data from the last Handling Event.
     *
     * This is where the core domain knowledge about the shipping flow is. Code is organized by the
     * 6 different handling event types of a cargo:
     * RECEIVE - LOAD - UNLOAD - CLAIM - CUSTOMS - (UNKNOWN)
     *
     * For each HandlingEventType we can reason about the status and what is expected next. This
     * follows the real world shipping flow rather than data structures of the domain.
     *
     * The HandlingEventRole uses the ItineraryRole heavily to calculate values based on Itinerary data.
     */
    @Mixins( HandlingEventRole.Mixin.class )
    public interface HandlingEventRole
    {
        void setContext( BuildDeliverySnapshot context );

        void deriveWithHandlingEvent();

        class Mixin
            extends RoleMixin<BuildDeliverySnapshot>
            implements HandlingEventRole
        {
            /*
           * HandlingEvent can be from last Delivery snapshot (last expected HandlingEvent) or
           * a new registered HandlingEvent.
           * */
            @This
            HandlingEvent handlingEvent;

            // Local convenience fields
            Location lastKnownLocation;
            Voyage currentVoyage;
            Boolean cargoIsNotMisrouted;

            // Flag for determining if we should wasUnexpected a former misdirected Handling Event after a reroute
            Boolean cargoIsRerouted;

            public void deriveWithHandlingEvent()
            {
                lastKnownLocation = handlingEvent.location().get();
                currentVoyage = handlingEvent.voyage().get();
                cargoIsNotMisrouted = context.newDeliverySnapshot.routingStatus().get() == RoutingStatus.ROUTED;

                // Cargo has been rerouted when the last handling event is declared unexpected.
                cargoIsRerouted = handlingEvent.wasUnexpected().get();

                // Step 3
                context.newDeliverySnapshot.lastHandlingEvent().set( handlingEvent );
                context.newDeliverySnapshot.lastKnownLocation().set( lastKnownLocation );

                // Step 4
                switch( handlingEvent.handlingEventType().get() )
                {
                case RECEIVE:
                    cargoReceived();        // Deviation 4a
                    return;

                case LOAD:
                    cargoLoaded();          // Deviation 4b
                    return;

                case UNLOAD:
                    cargoUnloaded();        // Deviation 4c
                    return;

                case CUSTOMS:
                    cargoInCustoms();       // Deviation 4d
                    return;

                case CLAIM:
                    cargoClaimed();         // Deviation 4e
                    return;

                default:
                    unknownHandlingEvent(); // Deviation 4f
                }
            }

            // Deviation 4a
            private void cargoReceived()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT );

                if( !cargoIsRerouted && !context.itinerary.expectsOrigin( lastKnownLocation ) )
                {
                    context.newDeliverySnapshot.eta().set( null );
                    context.newDeliverySnapshot.isMisdirected().set( true );
                }
                else if( cargoIsNotMisrouted )
                {
                    ExpectedHandlingEvent expectedEvent = context.itinerary.expectedEventAfterReceive();
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent );
                }
            }

            // Deviation 4b
            private void cargoLoaded()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.ONBOARD_CARRIER );
                context.newDeliverySnapshot.currentVoyage().set( currentVoyage );

                if( cargoIsRerouted )
                {
                    // After a reroute following a load in an unexpected location, we expected the cargo to be
                    // unloaded at the unload location of the first leg of the new itinerary.
                    ExpectedHandlingEvent expectedEvent = context.itinerary
                        .expectedEventAfterLoadAt( lastKnownLocation );
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent );
                }
                else if( !context.itinerary.expectsLoad( lastKnownLocation, currentVoyage ) )
                {
                    context.newDeliverySnapshot.eta().set( null );
                    context.newDeliverySnapshot.isMisdirected().set( true );
                }
                else if( cargoIsNotMisrouted )
                {
                    ExpectedHandlingEvent expectedEvent = context.itinerary
                        .expectedEventAfterLoadAt( lastKnownLocation );
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent );
                }
            }

            // Deviation 4c
            private void cargoUnloaded()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT );

                if( cargoIsRerouted )
                {
                    // After a reroute following an unload in an unexpected location, we expected the cargo
                    // to be loaded onto a carrier at the first location of the new route specification.
                    ExpectedHandlingEvent expectedEvent = context.itinerary.expectedEventAfterReceive();
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent );
                }
                else if( !context.itinerary.expectsUnload( lastKnownLocation, currentVoyage ) )
                {
                    context.newDeliverySnapshot.eta().set( null );
                    context.newDeliverySnapshot.isMisdirected().set( true );
                }
                else if( cargoIsNotMisrouted )
                {
                    ExpectedHandlingEvent expectedEvent = context.itinerary
                        .expectedEventAfterUnloadAt( lastKnownLocation );
                    context.newDeliverySnapshot.nextExpectedHandlingEvent().set( expectedEvent );

                    Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get();
                    context.newDeliverySnapshot
                        .isUnloadedAtDestination()
                        .set( lastKnownLocation.equals( expectedDestination ) );
                }
            }

            // Deviation 4d
            private void cargoInCustoms()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.IN_PORT );

                Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get();
                context.newDeliverySnapshot
                    .isUnloadedAtDestination()
                    .set( lastKnownLocation.equals( expectedDestination ) );
            }

            // Deviation 4e
            private void cargoClaimed()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.CLAIMED );

                Location expectedDestination = ( (RouteSpecification) context.factory ).destination().get();
                context.newDeliverySnapshot
                    .isUnloadedAtDestination()
                    .set( lastKnownLocation.equals( expectedDestination ) );

                if( !context.itinerary.expectsDestination( lastKnownLocation ) )
                {
                    context.newDeliverySnapshot.eta().set( null );
                    context.newDeliverySnapshot.isMisdirected().set( true );
                }
            }

            // Deviation 4f
            private void unknownHandlingEvent()
            {
                context.newDeliverySnapshot.transportStatus().set( TransportStatus.UNKNOWN );
            }
        }
    }

    /**
     * The ItineraryRole supports the HandlingEventRole with calculated results derived from Itinerary Legs.
     */
    @Mixins( ItineraryRole.Mixin.class )
    public interface ItineraryRole
    {
        void setContext( BuildDeliverySnapshot context );

        Date eta();

        boolean expectsOrigin( Location location );

        boolean expectsLoad( Location location, Voyage voyage );

        boolean expectsUnload( Location location, Voyage voyage );

        boolean expectsDestination( Location location );

        ExpectedHandlingEvent expectedEventAfterReceive();

        ExpectedHandlingEvent expectedEventAfterLoadAt( Location lastLoadLocation );

        ExpectedHandlingEvent expectedEventAfterUnloadAt( Location lastUnloadLocation );

        class Mixin
            extends RoleMixin<BuildDeliverySnapshot>
            implements ItineraryRole
        {
            @This
            Itinerary itinerary;

            public Date eta()
            {
                return itinerary.lastLeg().unloadTime().get();
            }

            // Route expectations ----------------------------------------------------

            public boolean expectsOrigin( Location location )
            {
                return itinerary.firstLeg().loadLocation().get().equals( location );
            }

            public boolean expectsLoad( Location location, Voyage voyage )
            {
                // One leg with same load location and voyage
                for( Leg leg : itinerary.legs().get() )
                {
                    if( leg.loadLocation().get().equals( location ) && leg.voyage().get().equals( voyage ) )
                    {
                        return true;
                    }
                }
                return false;
            }

            public boolean expectsUnload( Location location, Voyage voyage )
            {
                // One leg with same unload location and voyage
                for( Leg leg : itinerary.legs().get() )
                {
                    if( leg.unloadLocation().get().equals( location ) && leg.voyage().get().equals( voyage ) )
                    {
                        return true;
                    }
                }
                return false;
            }

            public boolean expectsDestination( Location location )
            {
                // Last leg destination matches
                return ( itinerary.lastLeg().unloadLocation().get().equals( location ) );
            }

            // Next expected handling event ----------------------------------------------

            public ExpectedHandlingEvent expectedEventAfterReceive()
            {
                // After RECEIVE, expect LOAD location and voyage of first itinerary leg
                final Leg firstLeg = itinerary.legs().get().iterator().next();
                return buildEvent( HandlingEventType.LOAD, firstLeg.loadLocation().get(), firstLeg.loadTime()
                    .get(), firstLeg.voyage().get() );
            }

            public ExpectedHandlingEvent expectedEventAfterLoadAt( Location lastLoadLocation )
            {
                // After LOAD, expect UNLOAD location and voyage of same itinerary leg as LOAD
                for( Leg leg : itinerary.legs().get() )
                {
                    if( leg.loadLocation().get().equals( lastLoadLocation ) )
                    {
                        return buildEvent( HandlingEventType.UNLOAD, leg.unloadLocation().get(), leg.unloadTime()
                            .get(), leg.voyage().get() );
                    }
                }
                return null;
            }

            public ExpectedHandlingEvent expectedEventAfterUnloadAt( Location lastUnloadLocation )
            {
                // After UNLOAD, expect LOAD location and voyage of following itinerary leg, or CLAIM if no more legs
                for( Iterator<Leg> it = itinerary.legs().get().iterator(); it.hasNext(); )
                {
                    final Leg leg = it.next();
                    if( leg.unloadLocation().get().equals( lastUnloadLocation ) )
                    {
                        // Cargo has a matching unload location in itinerary

                        if( it.hasNext() )
                        {
                            // Cargo has not arrived yet (uncompleted legs in itinerary)
                            // We expect it to be loaded onto some Carrier
                            final Leg nextLeg = it.next();
                            return buildEvent( HandlingEventType.LOAD, nextLeg.loadLocation().get(), nextLeg.loadTime()
                                .get(), nextLeg.voyage().get() );
                        }
                        else
                        {
                            // Cargo has arrived (no more legs in itinerary)
                            // We expect it to be claimed by the customer
                            return buildEvent( HandlingEventType.CLAIM, leg.unloadLocation().get(), leg.unloadTime()
                                .get(), null );
                        }
                    }
                }

                // Itinerary doesn't recognize last unload location
                return null;
            }

            private ExpectedHandlingEvent buildEvent( HandlingEventType eventType,
                                                      Location location,
                                                      Date time,
                                                      Voyage voyage
            )
            {
                ValueBuilder<ExpectedHandlingEvent> builder = vbf.newValueBuilder( ExpectedHandlingEvent.class );
                builder.prototype().handlingEventType().set( eventType );
                builder.prototype().location().set( location );
                builder.prototype().time().set( time );
                builder.prototype().voyage().set( voyage );
                return builder.newInstance();
            }
        }
    }
}