/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.model;

import org.teleal.cling.binding.LocalServiceBinder;
import org.teleal.cling.model.meta.DeviceService;
import org.teleal.cling.model.meta.LocalService;
import org.teleal.cling.model.state.ServiceState;
import org.teleal.common.util.Reflections;

import java.beans.PropertyChangeSupport;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Logger;

/**
 * @author Christian Bauer
 */
public class DefaultServiceManager<T> extends ServiceManager<T> {

    private static Logger log = Logger.getLogger(DefaultServiceManager.class.getName());

    final protected LocalService<T> service;
    final protected Class<T> serviceClass;
    final ReentrantLock lock = new ReentrantLock(true);
    protected T serviceImpl;
    protected ServiceState<T> state;

    protected DefaultServiceManager(DeviceService<LocalService<T>> deviceService) {
        this(deviceService.getService(), null);
    }

    protected DefaultServiceManager(LocalService<T> service) {
        this(service, null);
    }

    public DefaultServiceManager(LocalService<T> service, Class<T> serviceClass) {
        this.service = service;
        this.serviceClass = serviceClass;
    }

    @Override
    public LocalService<T> getService() {
        return service;
    }

    public Class<T> getServiceClass() {
        return serviceClass;
    }

    @Override
    public ServiceState<T> getState() {
        // We have to use the same lock as everybody else, otherwise this would have deadlock potential
        try {
            executeLocked(new Command<T>() {
                public void execute(ServiceManager<T> tServiceManager) throws Exception {
                    if (state == null) {
                        initialize();
                    }
                }

                @Override
                public String toString() {
                    return "Initializing after state==null check";
                }
            });
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        return state;
    }

    @Override
    public T getImplementation() {
        // We have to use the same lock as everybody else, otherwise this would have deadlock potential
        try {
            executeLocked(new Command<T>() {
                public void execute(ServiceManager<T> tServiceManager) throws Exception {
                    if (getInstance() == null) {
                        initialize();
                    }
                }

                @Override
                public String toString() {
                    return "Initializing after getInstance()==null check";
                }
            });
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        return getInstance();
    }

    protected T getInstance() {
        return serviceImpl;
    }

    protected void initialize() {
        log.fine("No service implementation instance available, initializing...");
        try {
            serviceImpl = newInstance();

            state = new ServiceState(
                    getService(),
                    createPropertyChangeSupport(serviceImpl)
            );

        } catch (Exception ex) {
            throw new RuntimeException("Could not initialize implementation: " + ex, ex);
        }
    }

    protected T newInstance() throws Exception {
        if (getServiceClass() == null) {
            throw new IllegalStateException("Subclass has to provide service class or override instantiation");
        }
        try {
            // Use this constructor if possible
            return getServiceClass().getConstructor(LocalService.class).newInstance(getService());
        } catch (NoSuchMethodException ex) {
            log.fine("Creating new service implementation instance with no-arg constructor: " + getServiceClass().getName());
            return getServiceClass().newInstance();
        }
    }

    protected PropertyChangeSupport createPropertyChangeSupport(T serviceImpl) throws Exception {
        Method m;
        if ((m = Reflections.getGetterMethod(serviceImpl.getClass(), "propertyChangeSupport")) != null &&
                PropertyChangeSupport.class.isAssignableFrom(m.getReturnType())) {
            log.fine("Service implementation instance offers PropertyChangeSupport, using that: " + serviceImpl.getClass().getName());
            return (PropertyChangeSupport) m.invoke(serviceImpl);
        }
        log.fine("Creating new PropertyChangeSupport for service implementation: " + serviceImpl.getClass().getName());
        return new PropertyChangeSupport(serviceImpl);
    }

    public void executeLocked(Command<T> cmd) throws Exception {
        log.finer("??? Attempting to obtain lock to execute: " + cmd);
        if (lock.tryLock(getLockTimeoutMillis(), TimeUnit.MILLISECONDS)) {
            log.finer(">>> Acquired lock, executing command");
            try {
                cmd.execute(this);
            } finally {
                log.finer("<<< Releasing lock");
                lock.unlock();
            }
        } else {
            log.finer("!!! Lock acquisition failed, throwing exception");
            throw new Exception("Could not aquire lock on service implementation, aborting execution");
        }
    }

    protected int getLockTimeoutMillis() {
        return 500;
    }

    // Convenience methods (TODO: NOT HAPPY WITH THE DEPENDENCY ON BINDING!)

    public static <T> DeviceService<LocalService<T>> newDeviceService(LocalServiceBinder binder, Class<T> clazz) {
        return newDeviceService(binder.read(clazz), clazz);
    }

    public static <T> DeviceService<LocalService<T>> newDeviceService(DeviceService<LocalService<T>> deviceService, Class<T> clazz) {
        deviceService.getService().setManager(
                new DefaultServiceManager(deviceService.getService(), clazz)
        );
        return deviceService;
    }

    @Override
    public String toString() {
        return "(" + getClass().getSimpleName() + ") Implementation: " + serviceImpl;
    }
}
