/*
 * 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.apache.sshd.server.scp;

import java.util.Collection;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;

import org.apache.sshd.common.scp.ScpFileOpener;
import org.apache.sshd.common.scp.ScpFileOpenerHolder;
import org.apache.sshd.common.scp.ScpHelper;
import org.apache.sshd.common.scp.ScpTransferEventListener;
import org.apache.sshd.common.util.EventListenerUtils;
import org.apache.sshd.common.util.ObjectBuilder;
import org.apache.sshd.common.util.threads.ExecutorServiceConfigurer;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.CommandFactory;

/**
 * This <code>CommandFactory</code> can be used as a standalone command factory
 * or can be used to augment another <code>CommandFactory</code> and provides
 * <code>SCP</code> support.
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 * @see ScpCommand
 */
public class ScpCommandFactory
        implements ScpFileOpenerHolder,
        CommandFactory,
        Cloneable,
        ExecutorServiceConfigurer {
    /**
     * A useful {@link ObjectBuilder} for {@link ScpCommandFactory}
     */
    public static class Builder implements ObjectBuilder<ScpCommandFactory> {
        private final ScpCommandFactory factory = new ScpCommandFactory();

        public Builder() {
            super();
        }

        public Builder withFileOpener(ScpFileOpener opener) {
            factory.setScpFileOpener(opener);
            return this;
        }

        public Builder withDelegate(CommandFactory delegate) {
            factory.setDelegateCommandFactory(delegate);
            return this;
        }

        public Builder withExecutorService(ExecutorService service) {
            factory.setExecutorService(service);
            return this;
        }

        public Builder withShutdownOnExit(boolean shutdown) {
            factory.setShutdownOnExit(shutdown);
            return this;
        }

        public Builder withSendBufferSize(int sendSize) {
            factory.setSendBufferSize(sendSize);
            return this;
        }

        public Builder withReceiveBufferSize(int receiveSize) {
            factory.setReceiveBufferSize(receiveSize);
            return this;
        }

        public Builder addEventListener(ScpTransferEventListener listener) {
            factory.addEventListener(listener);
            return this;
        }

        public Builder removeEventListener(ScpTransferEventListener listener) {
            factory.removeEventListener(listener);
            return this;
        }

        @Override
        public ScpCommandFactory build() {
            return factory.clone();
        }
    }

    /*
     * NOTE: we expose setters since there is no problem to change these settings between
     * successive invocations of the 'createCommand' method
     */
    private CommandFactory delegate;
    private ExecutorService executors;
    private boolean shutdownExecutor;
    private ScpFileOpener fileOpener;
    private int sendBufferSize = ScpHelper.MIN_SEND_BUFFER_SIZE;
    private int receiveBufferSize = ScpHelper.MIN_RECEIVE_BUFFER_SIZE;
    private Collection<ScpTransferEventListener> listeners = new CopyOnWriteArraySet<>();
    private ScpTransferEventListener listenerProxy;

    public ScpCommandFactory() {
        listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), listeners);
    }

    @Override
    public ScpFileOpener getScpFileOpener() {
        return fileOpener;
    }

    @Override
    public void setScpFileOpener(ScpFileOpener fileOpener) {
        this.fileOpener = fileOpener;
    }

    public CommandFactory getDelegateCommandFactory() {
        return delegate;
    }

    /**
     * @param factory A {@link CommandFactory} to be used if the
     * command is not an SCP one. If {@code null} then an {@link IllegalArgumentException}
     * will be thrown when attempting to invoke {@link #createCommand(String)}
     * with a non-SCP command
     */
    public void setDelegateCommandFactory(CommandFactory factory) {
        delegate = factory;
    }

    @Override
    public ExecutorService getExecutorService() {
        return executors;
    }

    /**
     * @param service An {@link ExecutorService} to be used when
     * starting {@link ScpCommand} execution. If {@code null} then a single-threaded
     * ad-hoc service is used. <B>Note:</B> the service will <U>not</U> be shutdown
     * when the command is terminated - unless it is the ad-hoc service, which will be
     * shutdown regardless
     */
    @Override
    public void setExecutorService(ExecutorService service) {
        executors = service;
    }

    @Override
    public boolean isShutdownOnExit() {
        return shutdownExecutor;
    }

    @Override
    public void setShutdownOnExit(boolean shutdown) {
        shutdownExecutor = shutdown;
    }

    public int getSendBufferSize() {
        return sendBufferSize;
    }

    /**
     * @param sendSize Size (in bytes) of buffer to use when sending files
     * @see ScpHelper#MIN_SEND_BUFFER_SIZE
     */
    public void setSendBufferSize(int sendSize) {
        if (sendSize < ScpHelper.MIN_SEND_BUFFER_SIZE) {
            throw new IllegalArgumentException("<ScpCommandFactory>() send buffer size "
                    + "(" + sendSize + ") below minimum required (" + ScpHelper.MIN_SEND_BUFFER_SIZE + ")");
        }
        sendBufferSize = sendSize;
    }

    public int getReceiveBufferSize() {
        return receiveBufferSize;
    }

    /**
     * @param receiveSize Size (in bytes) of buffer to use when receiving files
     * @see ScpHelper#MIN_RECEIVE_BUFFER_SIZE
     */
    public void setReceiveBufferSize(int receiveSize) {
        if (receiveSize < ScpHelper.MIN_RECEIVE_BUFFER_SIZE) {
            throw new IllegalArgumentException("<ScpCommandFactory>() receive buffer size "
                    + "(" + receiveSize + ") below minimum required (" + ScpHelper.MIN_RECEIVE_BUFFER_SIZE + ")");
        }
        receiveBufferSize = receiveSize;
    }

    /**
     * @param listener The {@link ScpTransferEventListener} to add
     * @return {@code true} if this is a <U>new</U> listener instance,
     * {@code false} if the listener is already registered
     * @throws IllegalArgumentException if {@code null} listener
     */
    public boolean addEventListener(ScpTransferEventListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("No listener instance");
        }

        return listeners.add(listener);
    }

    /**
     * @param listener The {@link ScpTransferEventListener} to remove
     * @return {@code true} if the listener was registered and removed,
     * {@code false} if the listener was not registered to begin with
     * @throws IllegalArgumentException if {@code null} listener
     */
    public boolean removeEventListener(ScpTransferEventListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("No listener instance");
        }

        return listeners.remove(listener);
    }

    /**
     * Parses a command string and verifies that the basic syntax is
     * correct. If parsing fails the responsibility is delegated to
     * the configured {@link CommandFactory} instance; if one exist.
     *
     * @param command command to parse
     * @return configured {@link Command} instance
     * @throws IllegalArgumentException if not an SCP command and no
     *                                  delegate command factory is available
     * @see ScpHelper#SCP_COMMAND_PREFIX
     */
    @Override
    public Command createCommand(String command) {
        if (command.startsWith(ScpHelper.SCP_COMMAND_PREFIX)) {
            return new ScpCommand(command,
                    getExecutorService(), isShutdownOnExit(),
                    getSendBufferSize(), getReceiveBufferSize(),
                    getScpFileOpener(), listenerProxy);
        }

        CommandFactory factory = getDelegateCommandFactory();
        if (factory != null) {
            return factory.createCommand(command);
        }

        throw new IllegalArgumentException("Unknown command, does not begin with '" + ScpHelper.SCP_COMMAND_PREFIX + "': " + command);
    }

    @Override
    public ScpCommandFactory clone() {
        try {
            ScpCommandFactory other = getClass().cast(super.clone());
            // clone the listeners set as well
            other.listeners = new CopyOnWriteArraySet<>(this.listeners);
            other.listenerProxy = EventListenerUtils.proxyWrapper(ScpTransferEventListener.class, getClass().getClassLoader(), other.listeners);
            return other;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);    // un-expected...
        }
    }
}
