/*
 * Copyright (C)2009 - SSHJ Contributors
 *
 * 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 net.schmizz.sshj.xfer.scp;

import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.xfer.LocalDestFile;
import net.schmizz.sshj.xfer.TransferListener;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Support for downloading files over a connected link using SCP. */
public class SCPDownloadClient extends AbstractSCPClient {

    private boolean recursiveMode = true;

    SCPDownloadClient(SCPEngine engine) {
        super(engine);
    }

    SCPDownloadClient(SCPEngine engine, int bandwidthLimit) {
        super(engine, bandwidthLimit);
    }

    /** Download a file from {@code sourcePath} on the connected host to {@code targetPath} locally. */
    public synchronized int copy(String sourcePath, LocalDestFile targetFile) throws IOException {
        return copy(sourcePath, targetFile, ScpCommandLine.EscapeMode.NoEscape);
    }

    public synchronized int copy(String sourcePath, LocalDestFile targetFile, ScpCommandLine.EscapeMode escapeMode)
            throws IOException {
        engine.cleanSlate();
        try {
            startCopy(sourcePath, targetFile, escapeMode);
        } finally {
            engine.exit();
        }
        return engine.getExitStatus();
    }

    public boolean getRecursiveMode() {
        return recursiveMode;
    }

    public void setRecursiveMode(boolean recursive) {
        this.recursiveMode = recursive;
    }

    private void startCopy(String sourcePath, LocalDestFile targetFile, ScpCommandLine.EscapeMode escapeMode)
            throws IOException {
        ScpCommandLine commandLine = ScpCommandLine.with(ScpCommandLine.Arg.SOURCE)
                            .and(ScpCommandLine.Arg.QUIET)
                            .and(ScpCommandLine.Arg.PRESERVE_TIMES)
                            .and(ScpCommandLine.Arg.RECURSIVE, recursiveMode)
                            .and(ScpCommandLine.Arg.LIMIT, String.valueOf(bandwidthLimit), (bandwidthLimit > 0));
        commandLine.withPath(sourcePath, escapeMode);
        engine.execSCPWith(commandLine);

        engine.signal("Start status OK");

        String msg = engine.readMessage();
        do {
            process(engine.getTransferListener(), null, msg, targetFile);
        } while (!(msg = engine.readMessage()).isEmpty());
    }

    private long parseLong(String longString, String valType)
            throws SCPException {
        try {
            return Long.parseLong(longString);
        } catch (NumberFormatException nfe) {
            throw new SCPException("Could not parse " + valType + " from `" + longString + "`", nfe);
        }
    }

    /* e.g. "C0644" -> 0644; "D0755" -> 0755 */

    private int parsePermissions(String cmd)
            throws SCPException {
        if (cmd.length() != 5) {
            throw new SCPException("Could not parse permissions from `" + cmd + "`");
        }
        return Integer.parseInt(cmd.substring(1), 8);
    }

    private boolean process(TransferListener listener, String bufferedTMsg, String msg, LocalDestFile f)
            throws IOException {
        if (msg.length() < 1) {
            throw new SCPException("Could not parse message `" + msg + "`");
        }

        switch (msg.charAt(0)) {

            case 'T':
                engine.signal("ACK: T");
                process(listener, msg, engine.readMessage(), f);
                break;

            case 'C':
                processFile(listener, msg, bufferedTMsg, f);
                break;

            case 'D':
                processDirectory(listener, msg, bufferedTMsg, f);
                break;

            case 'E':
                return true;

            case (char) 1:
            case (char) 2:
                throw new SCPRemoteException("Remote SCP command returned error: " + msg.substring(1), msg.substring(1));

            default:
                final String err = "Unrecognized message: `" + msg + "`";
                engine.sendMessage((char) 2 + err);
                throw new SCPException(err);
        }

        return false;
    }

    private void processDirectory(TransferListener listener, String dMsg, String tMsg, LocalDestFile f)
            throws IOException {
        final List<String> dMsgParts = tokenize(dMsg, 3, true); // D<perms> 0 <dirname>
        final long length = parseLong(dMsgParts.get(1), "dir length");
        final String dirname = dMsgParts.get(2);
        if (length != 0) {
            throw new IOException("Remote SCP command sent strange directory length: " + length);
        }

        final TransferListener dirListener = listener.directory(dirname);
        {
            f = f.getTargetDirectory(dirname);
            engine.signal("ACK: D");
            do {
            } while (!process(dirListener, null, engine.readMessage(), f));
            setAttributes(f, parsePermissions(dMsgParts.get(0)), tMsg);
            engine.signal("ACK: E");
        }
    }

    private void processFile(TransferListener listener, String cMsg, String tMsg, LocalDestFile f)
            throws IOException {
        final List<String> cMsgParts = tokenize(cMsg, 3, true); // C<perms> <size> <filename>
        final long length = parseLong(cMsgParts.get(1), "length");
        final String filename = cMsgParts.get(2);
        {
            f = f.getTargetFile(filename);
            engine.signal("Remote can start transfer");
            final OutputStream dest = f.getOutputStream();
            try {
                engine.transferFromRemote(listener.file(filename, length), dest, length);
            } finally {
                IOUtils.closeQuietly(dest);
            }
            engine.check("Remote agrees transfer done");
            setAttributes(f, parsePermissions(cMsgParts.get(0)), tMsg);
            engine.signal("Transfer done");
        }
    }

    private void setAttributes(LocalDestFile f, int perms, String tMsg)
            throws IOException {
        f.setPermissions(perms);
        if (tMsg != null) {
            List<String> tMsgParts = tokenize(tMsg, 4, false); // e.g. T<mtime> 0 <atime> 0
            f.setLastModifiedTime(parseLong(tMsgParts.get(0).substring(1), "last modified time"));
            f.setLastAccessedTime(parseLong(tMsgParts.get(2), "last access time"));
        }
    }

    private static List<String> tokenize(String msg, int totalParts, boolean consolidateTail)
            throws IOException {
        List<String> parts = Arrays.asList(msg.split(" "));
        if (parts.size() < totalParts || (!consolidateTail && parts.size() != totalParts)) {
            throw new IOException("Could not parse message received from remote SCP: " + msg);
        }

        if (consolidateTail && totalParts < parts.size()) {
            final StringBuilder sb = new StringBuilder(parts.get(totalParts - 1));
            for (int i = totalParts; i < parts.size(); i++) {
                sb.append(" ").append(parts.get(i));
            }
            parts = new ArrayList<String>(parts.subList(0, totalParts - 1));
            parts.add(sb.toString());
        }

        return parts;
    }

}
