/*
* JBoss, Home of Professional Open Source.
* Copyright 2011, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.as.repository;

import static org.jboss.as.protocol.mgmt.ProtocolUtils.expectHeader;

import java.io.BufferedOutputStream;
import java.io.DataInput;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;

import org.jboss.as.protocol.StreamUtils;
import org.jboss.as.protocol.mgmt.ActiveOperation;
import org.jboss.as.protocol.mgmt.FlushableDataOutput;
import org.jboss.as.protocol.mgmt.ManagementRequestContext;
import org.jboss.as.protocol.mgmt.ManagementResponseHeader;
import org.jboss.as.protocol.mgmt.RequestProcessingException;
import org.jboss.logging.BasicLogger;


/**
 * Common protocol code for getting files from master->slave HC and HC->server.
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
public abstract class RemoteFileRequestAndHandler {

    private final RemoteFileProtocolIdMapper protocol;
    private final Executor asyncExecutor;

    protected RemoteFileRequestAndHandler(RemoteFileProtocolIdMapper protocol) {
        this(protocol, null);
    }

    protected RemoteFileRequestAndHandler(RemoteFileProtocolIdMapper protocol, Executor asyncExecutor) {
        this.protocol = protocol;
        this.asyncExecutor = asyncExecutor;
    }

    public void sendRequest(FlushableDataOutput output, byte rootId, String filePath) throws IOException{
        output.writeByte(protocol.paramRootId());
        output.writeByte(rootId);
        output.writeByte(protocol.paramFilePath());
        output.writeUTF(filePath);

    }

    public void handleResponse(DataInput input, File localPath, BasicLogger log, ActiveOperation.ResultHandler<File> resultHandler, ManagementRequestContext<Void> context)
            throws IOException, CannotCreateLocalDirectoryException, DidNotReadEntireFileException{
        expectHeader(input, protocol.paramNumFiles());
        int numFiles = input.readInt();
        log.debugf("Received %d files for %s", numFiles, localPath);
        switch (numFiles) {
            case -1: { // Not found on DC
                break;
            }
            case 0: { // Found on DC, but was an empty dir
                if (!localPath.mkdirs()) {
                    throw new CannotCreateLocalDirectoryException(localPath);
                }
                break;
            }
            default: { // Found on DC
                for (int i = 0; i < numFiles; i++) {
                    expectHeader(input, protocol.fileStart());
                    expectHeader(input, protocol.paramFilePath());
                    final String path = input.readUTF();
                    expectHeader(input, protocol.paramFileSize());
                    final long length = input.readLong();
                    log.debugf("Received file [%s] of length %d", path, length);
                    final File file = new File(localPath, path);
                    if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
                        throw new CannotCreateLocalDirectoryException(localPath.getParentFile());
                    }
                    if(length == 0L) {
                        file.mkdir();
                    } else {
                        long totalRead = 0;
                        try (OutputStream fileOut = new BufferedOutputStream(new FileOutputStream(file))) {
                            final byte[] buffer = new byte[8192];
                            while (totalRead < length) {
                                int len = Math.min((int) (length - totalRead), buffer.length);
                                input.readFully(buffer, 0, len);
                                fileOut.write(buffer, 0, len);
                                totalRead += len;
                            }
                        }
                        if (totalRead != length) {
                            throw new DidNotReadEntireFileException((length - totalRead));
                        }
                    }
                    expectHeader(input, protocol.fileEnd());
                }
            }
        }
        resultHandler.done(localPath);
    }

    public void handleRequest(final DataInput input, final RootFileReader reader,
                              final ActiveOperation.ResultHandler<Void> resultHandler,
                              final ManagementRequestContext<Void> context) throws IOException {
        expectHeader(input, protocol.paramRootId());
        final byte rootId = input.readByte();
        expectHeader(input, protocol.paramFilePath());
        final String filePath = input.readUTF();

        ManagementRequestContext.AsyncTask<Void> task = new ManagementRequestContext.AsyncTask<Void>() {
            @Override
            public void execute(ManagementRequestContext<Void> context) throws RequestProcessingException, IOException {
                final File localPath = reader.readRootFile(rootId, filePath);
                FlushableDataOutput output = context.writeMessage(ManagementResponseHeader.create(context.getRequestHeader()));
                try {
                    writeResponse(localPath, output);
                    output.close();
                    resultHandler.done(null); // call stack (AsyncTaskRunner created by ManagementRequestContext) handles failures
                } finally {
                    StreamUtils.safeClose(output);
                }
            }
        };

        if (asyncExecutor == null) {
            context.executeAsync(task);
        } else {
            context.executeAsync(task, asyncExecutor);
        }
    }

    private void writeResponse(final File localPath, final FlushableDataOutput output) throws IOException {
        output.writeByte(protocol.paramNumFiles());
        if (localPath == null || !localPath.exists()) {
            output.writeInt(-1);
        } else if (localPath.isFile()) {
            output.writeInt(1);
            writeFile(localPath, localPath, output);
        } else {
            final List<File> childFiles = getChildFiles(localPath);
            output.writeInt(childFiles.size());
            for (File child : childFiles) {
                writeFile(localPath, child, output);
            }
        }
    }

    private List<File> getChildFiles(final File base) {
        final List<File> childFiles = new ArrayList<>();
        getChildFiles(base, childFiles);
        return childFiles;
    }

    private void getChildFiles(final File base, final List<File> childFiles) {
        for (File child : base.listFiles()) {
            if (child.isFile() || isEmpty(child)) {
                childFiles.add(child);
            } else {
                getChildFiles(child, childFiles);
            }
        }
    }

    private boolean isEmpty(File file) {
        return file.isDirectory() && (file.list() == null || file.list().length == 0);
    }

    private String getRelativePath(final File parent, final File child) {
        return child.getAbsolutePath().substring(parent.getAbsolutePath().length()+1);
    }

    private void writeFile(final File localPath, final File file, final FlushableDataOutput output) throws IOException {
        output.writeByte(protocol.fileStart());
        output.writeByte(protocol.paramFilePath());
        output.writeUTF(getRelativePath(localPath, file));
        output.writeByte(protocol.paramFileSize());
        if (file.isDirectory()) {
            output.writeLong(0L);
            output.writeByte(protocol.fileEnd());
            return;
        } else {
            output.writeLong(file.length());
        }
        InputStream inputStream = null;
        try {
            inputStream = new FileInputStream(file);
            byte[] buffer = new byte[8192];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                output.write(buffer, 0, len);
            }
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ignored) {
                }
            }
        }
        output.writeByte(protocol.fileEnd());
    }

    /**
     * Maps the expected protocol codes to the actual protocol
     */
    public interface RemoteFileProtocolIdMapper {
        byte paramRootId();
        byte paramNumFiles();
        byte fileStart();
        byte paramFilePath();
        byte paramFileSize();
        byte fileEnd();
    }

    /**
     * Reads the root file being got
     */
    public interface RootFileReader {
        File readRootFile(byte rootId, String filePath) throws RequestProcessingException;
    }

    /**
     *  Indicates a directory could not be created
     */
    public static class CannotCreateLocalDirectoryException extends Exception {
        private static final long serialVersionUID = 1L;
        final File dir;

        private CannotCreateLocalDirectoryException(File dir) {
            this.dir = dir;
        }

        public File getDir() {
            return dir;
        }
    }

    /**
     *  Indicates a file was not completely read
     */
    public static class DidNotReadEntireFileException extends Exception {
        private static final long serialVersionUID = 1L;
        final long missing;

        private DidNotReadEntireFileException(long missing) {
            this.missing = missing;
        }

        public long getMissing() {
            return missing;
        }
    }
}
