/*
 * Copyright 2014 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.api.publication.maven.internal.wagon;

import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.events.*;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyInfoProvider;
import org.apache.maven.wagon.repository.Repository;
import org.apache.maven.wagon.resource.Resource;
import org.gradle.api.GradleException;
import org.gradle.internal.resource.local.FileLocalResource;
import org.gradle.internal.resource.local.LocalResource;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

import static org.apache.maven.wagon.events.SessionEvent.*;
import static org.apache.maven.wagon.events.TransferEvent.*;

/**
 * A maven wagon intended to work with {@link org.apache.maven.artifact.manager.DefaultWagonManager} Maven uses reflection to initialize instances of this wagon see: {@link
 * org.codehaus.plexus.component.factory.java.JavaComponentFactory#newInstance(org.codehaus.plexus.component.repository.ComponentDescriptor, org.codehaus.classworlds.ClassRealm,
 * org.codehaus.plexus.PlexusContainer)}
 */
public class RepositoryTransportDeployWagon implements Wagon {

    private static ThreadLocal<RepositoryTransportWagonAdapter> currentDelegate = new InheritableThreadLocal<RepositoryTransportWagonAdapter>();

    private SessionEventSupport sessionEventSupport = new SessionEventSupport();
    private TransferEventSupport transferEventSupport = new TransferEventSupport();
    private Repository mutatingRepository;

    public static void contextualize(RepositoryTransportWagonAdapter adapter) {
        currentDelegate.set(adapter);
    }

    public static void decontextualize() {
        currentDelegate.remove();
    }

    @Override
    public final void get(String resourceName, File destination) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException {
        Resource resource = new Resource(resourceName);
        this.transferEventSupport.fireTransferInitiated(transferEvent(resource, TRANSFER_INITIATED, REQUEST_GET));
        this.transferEventSupport.fireTransferStarted(transferEvent(resource, TRANSFER_STARTED, REQUEST_GET));
        try {
            if (!destination.exists()) {
                destination.getParentFile().mkdirs();
                destination.createNewFile();
            }
            if (!getDelegate().getRemoteFile(destination, resourceName)) {
                throw new ResourceDoesNotExistException(String.format("Resource '%s' does not exist", resourceName));
            }
            this.transferEventSupport.fireTransferCompleted(transferEvent(resource, TRANSFER_COMPLETED, REQUEST_GET));
        } catch (ResourceDoesNotExistException e) {
            this.transferEventSupport.fireTransferError(transferEvent(resource, e, REQUEST_GET));
            throw e;
        } catch (Exception e) {
            this.transferEventSupport.fireTransferError(transferEvent(resource, e, REQUEST_GET));
            throw new TransferFailedException(String.format("Could not get resource '%s'", resourceName), e);
        }
    }

    @Override
    public final void put(File file, String resourceName) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException {
        Resource resource = new Resource(resourceName);
        this.transferEventSupport.fireTransferInitiated(transferEvent(resource, TRANSFER_INITIATED, REQUEST_PUT));
        try {
            LocalResource localResource = new MavenTransferLoggingFileResource(file, resource);
            getDelegate().putRemoteFile(localResource, resourceName);
        } catch (Exception e) {
            this.transferEventSupport.fireTransferError(transferEvent(resource, e, REQUEST_PUT));
            throw new TransferFailedException(String.format("Could not write to resource '%s'", resourceName), e);
        }
        this.transferEventSupport.fireTransferCompleted(transferEvent(resource, TRANSFER_COMPLETED, REQUEST_PUT));
    }

    private RepositoryTransportWagonAdapter getDelegate() {
        return currentDelegate.get();
    }

    @Override
    public final boolean resourceExists(String resourceName) throws TransferFailedException, AuthorizationException {
        throwNotImplemented("getIfNewer(String resourceName, File file, long timestamp)");
        return false;
    }

    @Override
    public final boolean getIfNewer(String resourceName, File file, long timestamp) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException {
        throwNotImplemented("getIfNewer(String resourceName, File file, long timestamp)");
        return false;
    }

    @Override
    public final void putDirectory(File file, String resourceName) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException {
        throwNotImplemented("putDirectory(File file, String resourceName)");
    }

    @Override
    public final List getFileList(String resourceName) throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException {
        throwNotImplemented("getFileList(String resourceName)");
        return null;
    }

    @Override
    public final boolean supportsDirectoryCopy() {
        return false;
    }

    @Override
    public final Repository getRepository() {
        return this.mutatingRepository;
    }

    @Override
    public final void openConnection() throws ConnectionException, AuthenticationException {
    }

    @Override
    public final void connect(Repository repository) throws ConnectionException, AuthenticationException {
        this.mutatingRepository = repository;
        this.sessionEventSupport.fireSessionLoggedIn(sessionEvent(SESSION_LOGGED_IN));
        this.sessionEventSupport.fireSessionOpened(sessionEvent(SESSION_OPENED));
    }

    @Override
    public final void connect(Repository repository, ProxyInfo proxyInfo) throws ConnectionException, AuthenticationException {
        connect(repository);
    }

    @Override
    public final void connect(Repository repository, ProxyInfoProvider proxyInfoProvider) throws ConnectionException, AuthenticationException {
        connect(repository);
    }

    @Override
    public final void connect(Repository repository, AuthenticationInfo authenticationInfo) throws ConnectionException, AuthenticationException {
        connect(repository);
    }

    @Override
    public final void connect(Repository repository, AuthenticationInfo authenticationInfo, ProxyInfo proxyInfo) throws ConnectionException, AuthenticationException {
        connect(repository);
    }

    @Override
    public final void connect(Repository repository, AuthenticationInfo authenticationInfo, ProxyInfoProvider proxyInfoProvider) throws ConnectionException, AuthenticationException {
        connect(repository);
    }

    @Override
    public final void disconnect() throws ConnectionException {
        this.sessionEventSupport.fireSessionDisconnecting(sessionEvent(SESSION_DISCONNECTING));
        this.sessionEventSupport.fireSessionLoggedOff(sessionEvent(SESSION_LOGGED_OFF));
        this.sessionEventSupport.fireSessionDisconnected(sessionEvent(SESSION_LOGGED_OFF));
    }

    @Override
    public final void addSessionListener(SessionListener sessionListener) {
        this.sessionEventSupport.addSessionListener(sessionListener);
    }

    @Override
    public final void removeSessionListener(SessionListener sessionListener) {
        this.sessionEventSupport.removeSessionListener(sessionListener);
    }

    @Override
    public final boolean hasSessionListener(SessionListener sessionListener) {
        return this.sessionEventSupport.hasSessionListener(sessionListener);
    }

    @Override
    public final void addTransferListener(TransferListener transferListener) {
        this.transferEventSupport.addTransferListener(transferListener);
    }

    @Override
    public final void removeTransferListener(TransferListener transferListener) {
        this.transferEventSupport.removeTransferListener(transferListener);
    }

    @Override
    public final boolean hasTransferListener(TransferListener transferListener) {
        return this.transferEventSupport.hasTransferListener(transferListener);
    }

    @Override
    public final boolean isInteractive() {
        return false;
    }

    @Override
    public final void setInteractive(boolean b) {

    }

    @Override
    public final void setTimeout(int i) {

    }

    @Override
    public final int getTimeout() {
        return 0;
    }

    @Override
    public final void setReadTimeout(int i) {

    }

    @Override
    public final int getReadTimeout() {
        return 0;
    }

    private SessionEvent sessionEvent(int e) {
        return new SessionEvent(this, e);
    }

    private void throwNotImplemented(String s) {
        throw new GradleException("This wagon does not yet support the method:" + s);
    }

    private TransferEvent transferEvent(Resource resource, int eventType, int requestType) {
        TransferEvent transferEvent = new TransferEvent(this, resource, eventType, requestType);
        transferEvent.setTimestamp(new Date().getTime());
        return transferEvent;
    }

    private TransferEvent transferEvent(Resource resource, Exception e, int requestType) {
        return new TransferEvent(this, resource, e, requestType);
    }

    private class MavenTransferLoggingFileResource extends FileLocalResource {
        private final Resource resource;

        private MavenTransferLoggingFileResource(File file, Resource resource) {
            super(file);
            this.resource = resource;
        }

        @Override
        public InputStream open() {
            // Need to do this here, so that the transfer is 'restarted' when HttpClient reopens the resource (DIGEST AUTH only)
            transferEventSupport.fireTransferStarted(transferEvent(resource, TRANSFER_STARTED, REQUEST_PUT));
            return new ObservingInputStream(super.open(), resource);
        }

        protected class ObservingInputStream extends InputStream {
            private final InputStream inputStream;
            private final TransferEvent transferEvent;
            private final byte[] singleByteBuffer = new byte[1];

            public ObservingInputStream(InputStream inputStream, Resource resource) {
                this.inputStream = inputStream;
                this.transferEvent = transferEvent(resource, TransferEvent.TRANSFER_PROGRESS, REQUEST_PUT);
            }

            @Override
            public void close() throws IOException {
                inputStream.close();
            }

            @Override
            public int read() throws IOException {
                int result = inputStream.read();
                if (result >= 0) {
                    singleByteBuffer[0] = (byte) result;
                    logTransfer(singleByteBuffer, 1);
                }
                return result;
            }

            public int read(byte[] b, int off, int len) throws IOException {
                int read = inputStream.read(b, off, len);
                if (read > 0) {
                    logTransfer(b, read);
                }
                return read;
            }

            private void logTransfer(byte[] bytes, int read) {
                transferEventSupport.fireTransferProgress(transferEvent, bytes, read);
            }
        }
    }
}
