/*
 * Copyright 2016 the original author or authors.
 *
 * 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.gradle.internal.actor.internal;

import org.gradle.internal.actor.Actor;
import org.gradle.internal.actor.ActorFactory;
import org.gradle.internal.concurrent.*;
import org.gradle.internal.dispatch.*;
import org.slf4j.LoggerFactory;

import java.util.IdentityHashMap;
import java.util.Map;

/**
 * A basic {@link ActorFactory} implementation. Currently cannot support creating both a blocking and non-blocking actor for the same target object.
 */
public class DefaultActorFactory implements ActorFactory, Stoppable {
    private final Map<Object, NonBlockingActor> nonBlockingActors = new IdentityHashMap<Object, NonBlockingActor>();
    private final Map<Object, BlockingActor> blockingActors = new IdentityHashMap<Object, BlockingActor>();
    private final Object lock = new Object();
    private final ExecutorFactory executorFactory;

    public DefaultActorFactory(ExecutorFactory executorFactory) {
        this.executorFactory = executorFactory;
    }

    /**
     * Stops all actors.
     */
    public void stop() {
        synchronized (lock) {
            try {
                CompositeStoppable.stoppable(nonBlockingActors.values()).add(blockingActors.values()).stop();
            } finally {
                nonBlockingActors.clear();
            }
        }
    }

    public Actor createActor(Object target) {
        if (target instanceof NonBlockingActor) {
            return (NonBlockingActor) target;
        }
        synchronized (lock) {
            if (blockingActors.containsKey(target)) {
                throw new UnsupportedOperationException("Cannot create a non-blocking and blocking actor for the same object. This is not implemented yet.");
            }
            NonBlockingActor actor = nonBlockingActors.get(target);
            if (actor == null) {
                actor = new NonBlockingActor(target);
                nonBlockingActors.put(target, actor);
            }
            return actor;
        }
    }

    public Actor createBlockingActor(Object target) {
        synchronized (lock) {
            if (nonBlockingActors.containsKey(target)) {
                throw new UnsupportedOperationException("Cannot create a non-blocking and blocking actor for the same object. This is not implemented yet.");
            }
            BlockingActor actor = blockingActors.get(target);
            if (actor == null) {
                actor = new BlockingActor(target);
                blockingActors.put(target, actor);
            }
            return actor;
        }
    }

    private void stopped(NonBlockingActor actor) {
        synchronized (lock) {
            nonBlockingActors.values().remove(actor);
        }
    }

    private void stopped(BlockingActor actor) {
        synchronized (lock) {
            blockingActors.values().remove(actor);
        }
    }

    private class BlockingActor implements Actor {
        private final Dispatch<MethodInvocation> dispatch;
        private final Object lock = new Object();
        private boolean stopped;

        public BlockingActor(Object target) {
            dispatch = new ReflectionDispatch(target);
        }

        public <T> T getProxy(Class<T> type) {
            return new ProxyDispatchAdapter<T>(this, type, ThreadSafe.class).getSource();
        }

        public void stop() throws DispatchException {
            synchronized (lock) {
                stopped = true;
            }
            stopped(this);
        }

        public void dispatch(MethodInvocation message) {
            synchronized (lock) {
                if (stopped) {
                    throw new IllegalStateException("This actor has been stopped.");
                }
                dispatch.dispatch(message);
            }
        }
    }

    private class NonBlockingActor implements Actor {
        private final Dispatch<MethodInvocation> dispatch;
        private final StoppableExecutor executor;
        private final ExceptionTrackingFailureHandler failureHandler;

        public NonBlockingActor(Object targetObject) {
            executor = executorFactory.create("Dispatch " + targetObject);
            failureHandler = new ExceptionTrackingFailureHandler(LoggerFactory.getLogger(NonBlockingActor.class));
            dispatch = new AsyncDispatch<MethodInvocation>(executor,
                    new FailureHandlingDispatch<MethodInvocation>(
                            new ReflectionDispatch(targetObject),
                            failureHandler));
        }

        public <T> T getProxy(Class<T> type) {
            return new ProxyDispatchAdapter<T>(this, type, ThreadSafe.class).getSource();
        }

        public void stop() {
            try {
                CompositeStoppable.stoppable(dispatch, executor, failureHandler).stop();
            } finally {
                stopped(this);
            }
        }

        public void dispatch(MethodInvocation message) {
            dispatch.dispatch(message);
        }
    }
}
