/*
 * Copyright (c) 2013 Hal Hildebrand, all rights reserved.
 * 
 * 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 com.hellblazer.tron;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * @author hhildebrand
 * 
 * @param <Transitions>
 *            the transition interface
 */
final class FiniteStateMachineImpl<Transitions extends FiniteStateMachine<? super Context>, Context>
        implements FiniteStateMachine<Context> {

    private static final ThreadLocal<FiniteStateMachineImpl<? extends FiniteStateMachine<?>, ?>> thisFsm = new ThreadLocal<>();

    static <Transitions> Transitions thisFsm() {
        @SuppressWarnings("unchecked")
        Transitions fsm = (Transitions) thisFsm.get();
        return fsm;
    }

    private State              pendingPush;
    private State              current;
    final InvocationHandler    handler = new InvocationHandler() {

                                           @Override
                                           public Object invoke(Object proxy,
                                                                Method method,
                                                                Object[] args)
                                                                              throws Throwable {
                                               return FiniteStateMachineImpl.this.invoke(method,
                                                                                         args);
                                           }
                                       };
    private State              previous;
    private Method             transition;
    private final Lock         sync;
    private final Deque<State> stack   = new ArrayDeque<>();
    private final Context      context;

    FiniteStateMachineImpl(Context context, boolean sync) {
        this.context = context;
        this.sync = sync ? new ReentrantLock() : null;
    }

    private Object invoke(Method method, Object[] arguments) {
        try {
            Method fsmMethod = current.getClass().getDeclaredMethod(method.getName(),
                                                                    method.getParameterTypes());
            try {
                return fsmMethod.invoke(this, arguments);
            } catch (IllegalAccessException | IllegalArgumentException
                    | InvocationTargetException e) {
                throw new IllegalStateException(e);
            }
        } catch (NoSuchMethodException e) {
            // do nothing, this is a transition
        }
        return fire(method, arguments);
    }

    private Object fire(Method t, Object[] arguments) {
        Method stateTransition;
        try {
            stateTransition = current.getClass().getDeclaredMethod(t.getName(),
                                                                   t.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new NoTransitionException(current, t.getName());
        }
        if (sync != null) {
            try {
                sync.lockInterruptibly();
            } catch (InterruptedException e) {
                return null;
            }
        }
        FiniteStateMachineImpl<? extends FiniteStateMachine<?>, ?> previousFsm = pushThis();
        previous = current;
        transition = t;
        try {
            State nextState = (State) stateTransition.invoke(current, arguments);
            if (nextState != null) {
                current = nextState;
                executeEntryAction();
            }
            if (pendingPush != null) {
                stack.push(current);
                current = pendingPush;
                pendingPush = null;
                executeEntryAction();
            }

        } catch (IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new IllegalStateException(
                                            String.format("Unable to invoke transition %s on state %s",
                                                          t, current), e);
        } finally {
            pop(previousFsm);
            if (sync != null) {
                sync.unlock();
            }
        }
        return null;
    }

    private void executeEntryAction() {
        // TODO Auto-generated method stub

    }

    private void pop(FiniteStateMachineImpl<? extends FiniteStateMachine<?>, ?> previous) {
        thisFsm.set(previous);
    }

    private FiniteStateMachineImpl<? extends FiniteStateMachine<?>, ?> pushThis() {
        FiniteStateMachineImpl<? extends FiniteStateMachine<?>, ?> current = thisFsm.get();
        thisFsm.set(this);
        return current;
    }

    @Override
    public void push(State state) {
        pendingPush = state;
    }

    @Override
    public void pop() {
        previous = current;
        try {
            current = stack.pop();
        } catch (NoSuchElementException e) {
            throw new IllegalStateException("State stack is empty");
        }
    }

    @Override
    public State current() {
        return current;
    }

    @Override
    public State previous() {
        return previous;
    }

    @Override
    public String transition() {
        return transition.toString();
    }

    @Override
    public Context getContext() {
        return context;
    }
}

@@ -4231,32 +4231,69 @@
 tate != null) %7B%0A
+                executeExitAction();%0A
                 
@@ -5126,43 +5126,1000 @@
     
-// TODO Auto-generated method stub%0A
+for (Method action : current.getClass().getDeclaredMethods()) %7B%0A            if (action.isAnnotationPresent(EntryAction.class)) %7B%0A                try %7B%0A                    action.invoke(current, new Object%5B%5D %7B%7D);%0A                    return;%0A                %7D catch (IllegalAccessException %7C IllegalArgumentException%0A                        %7C InvocationTargetException e) %7B%0A                    throw new IllegalStateException(e);%0A                %7D%0A            %7D%0A        %7D%0A    %7D%0A%0A    private void executeExitAction() %7B%0A        for (Method action : current.getClass().getDeclaredMethods()) %7B%0A            if (action.isAnnotationPresent(ExitAction.class)) %7B%0A                try %7B%0A                    action.invoke(current, new Object%5B%5D %7B%7D);%0A                    return;%0A                %7D catch (IllegalAccessException %7C IllegalArgumentException%0A                        %7C InvocationTargetException e) %7B%0A                    throw new IllegalStateException(e);%0A                %7D%0A            %7D%0A        %7D
 %0A   
@@ -6630,32 +6630,61 @@
 ious = current;%0A
+        executeExitAction();%0A
         try %7B%0A  

handle exit/entry actions