/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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 com.android.ddmlib;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ddmlib.AdbHelper.AdbResponse;
import com.android.ddmlib.FileListingService.FileEntry;
import com.android.ddmlib.SyncException.SyncError;
import com.android.ddmlib.utils.ArrayHelper;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Date;

/**
 * Sync service class to push/pull to/from devices/emulators, through the debug bridge.
 * <p>
 * To get a {@link SyncService} object, use {@link Device#getSyncService()}.
 */
public class SyncService {

    private static final byte[] ID_OKAY = { 'O', 'K', 'A', 'Y' };
    private static final byte[] ID_FAIL = { 'F', 'A', 'I', 'L' };
    private static final byte[] ID_STAT = { 'S', 'T', 'A', 'T' };
    private static final byte[] ID_RECV = { 'R', 'E', 'C', 'V' };
    private static final byte[] ID_DATA = { 'D', 'A', 'T', 'A' };
    private static final byte[] ID_DONE = { 'D', 'O', 'N', 'E' };
    private static final byte[] ID_SEND = { 'S', 'E', 'N', 'D' };
//    private final static byte[] ID_LIST = { 'L', 'I', 'S', 'T' };
//    private final static byte[] ID_DENT = { 'D', 'E', 'N', 'T' };

    private static final NullSyncProgressMonitor sNullSyncProgressMonitor =
            new NullSyncProgressMonitor();

    private static final int S_ISOCK = 0xC000; // type: symbolic link
    private static final int S_IFLNK = 0xA000; // type: symbolic link
    private static final int S_IFREG = 0x8000; // type: regular file
    private static final int S_IFBLK = 0x6000; // type: block device
    private static final int S_IFDIR = 0x4000; // type: directory
    private static final int S_IFCHR = 0x2000; // type: character device
    private static final int S_IFIFO = 0x1000; // type: fifo
/*
    private final static int S_ISUID = 0x0800; // set-uid bit
    private final static int S_ISGID = 0x0400; // set-gid bit
    private final static int S_ISVTX = 0x0200; // sticky bit
    private final static int S_IRWXU = 0x01C0; // user permissions
    private final static int S_IRUSR = 0x0100; // user: read
    private final static int S_IWUSR = 0x0080; // user: write
    private final static int S_IXUSR = 0x0040; // user: execute
    private final static int S_IRWXG = 0x0038; // group permissions
    private final static int S_IRGRP = 0x0020; // group: read
    private final static int S_IWGRP = 0x0010; // group: write
    private final static int S_IXGRP = 0x0008; // group: execute
    private final static int S_IRWXO = 0x0007; // other permissions
    private final static int S_IROTH = 0x0004; // other: read
    private final static int S_IWOTH = 0x0002; // other: write
    private final static int S_IXOTH = 0x0001; // other: execute
*/

    private static final int SYNC_DATA_MAX = 64*1024;
    private static final int REMOTE_PATH_MAX_LENGTH = 1024;

    /**
     * Classes which implement this interface provide methods that deal
     * with displaying transfer progress.
     */
    public interface ISyncProgressMonitor {
        /**
         * Sent when the transfer starts
         * @param totalWork the total amount of work.
         */
        void start(int totalWork);
        /**
         * Sent when the transfer is finished or interrupted.
         */
        void stop();
        /**
         * Sent to query for possible cancellation.
         * @return true if the transfer should be stopped.
         */
        boolean isCanceled();
        /**
         * Sent when a sub task is started.
         * @param name the name of the sub task.
         */
        void startSubTask(String name);
        /**
         * Sent when some progress have been made.
         * @param work the amount of work done.
         */
        void advance(int work);
    }

    public static class FileStat {
      private final int myMode;
      private final int mySize;
      private final Date myLastModified;

      public FileStat(int mode, int size, int lastModifiedSecs) {
        myMode = mode;
        mySize = size;
        myLastModified = new Date((long)(lastModifiedSecs) * 1000);
      }

      public int getMode() {
        return myMode;
      }

      public int getSize() {
        return mySize;
      }

      public Date getLastModified() {
        return myLastModified;
      }
    }

    /**
     * A Sync progress monitor that does nothing
     */
    private static class NullSyncProgressMonitor implements ISyncProgressMonitor {
        @Override
        public void advance(int work) {
        }
        @Override
        public boolean isCanceled() {
            return false;
        }

        @Override
        public void start(int totalWork) {
        }
        @Override
        public void startSubTask(String name) {
        }
        @Override
        public void stop() {
        }
    }

    private InetSocketAddress mAddress;
    private Device mDevice;
    private SocketChannel mChannel;

    /**
     * Buffer used to send data. Allocated when needed and reused afterward.
     */
    private byte[] mBuffer;

    /**
     * Creates a Sync service object.
     * @param address The address to connect to
     * @param device the {@link Device} that the service connects to.
     */
    SyncService(InetSocketAddress address, Device device) {
        mAddress = address;
        mDevice = device;
    }

    /**
     * Opens the sync connection. This must be called before any calls to push[File] / pull[File].
     * @return true if the connection opened, false if adb refuse the connection. This can happen
     * if the {@link Device} is invalid.
     * @throws TimeoutException in case of timeout on the connection.
     * @throws AdbCommandRejectedException if adb rejects the command
     * @throws IOException If the connection to adb failed.
     */
    boolean openSync() throws TimeoutException, AdbCommandRejectedException, IOException {
        try {
            mChannel = SocketChannel.open(mAddress);
            mChannel.configureBlocking(false);

            // target a specific device
            AdbHelper.setDevice(mChannel, mDevice);

            byte[] request = AdbHelper.formAdbRequest("sync:"); //$NON-NLS-1$
            AdbHelper.write(mChannel, request, -1, DdmPreferences.getTimeOut());

            AdbResponse resp = AdbHelper.readAdbResponse(mChannel, false /* readDiagString */);

            if (!resp.okay) {
                Log.w("ddms", "Got unhappy response from ADB sync req: " + resp.message);
                mChannel.close();
                mChannel = null;
                return false;
            }
        } catch (TimeoutException e) {
            if (mChannel != null) {
                try {
                    mChannel.close();
                } catch (IOException e2) {
                    // we want to throw the original exception, so we ignore this one.
                }
                mChannel = null;
            }

            throw e;
        } catch (IOException e) {
            if (mChannel != null) {
                try {
                    mChannel.close();
                } catch (IOException e2) {
                    // we want to throw the original exception, so we ignore this one.
                }
                mChannel = null;
            }

            throw e;
        }

        return true;
    }

    /**
     * Closes the connection.
     */
    public void close() {
        if (mChannel != null) {
            try {
                mChannel.close();
            } catch (IOException e) {
                // nothing to be done really...
            }
            mChannel = null;
        }
    }

    /**
     * Returns a sync progress monitor that does nothing. This allows background tasks that don't
     * want/need to display ui, to pass a valid {@link ISyncProgressMonitor}.
     * <p>This object can be reused multiple times and can be used by concurrent threads.
     */
    public static ISyncProgressMonitor getNullProgressMonitor() {
        return sNullSyncProgressMonitor;
    }

    /**
     * Pulls file(s) or folder(s).
     * @param entries the remote item(s) to pull
     * @param localPath The local destination. If the entries count is &gt; 1 or
     *      if the unique entry is a folder, this should be a folder.
     * @param monitor The progress monitor. Cannot be null.
     * @throws SyncException
     * @throws IOException
     * @throws TimeoutException
     *
     * @see FileListingService.FileEntry
     * @see #getNullProgressMonitor()
     */
    public void pull(FileEntry[] entries, String localPath, ISyncProgressMonitor monitor)
            throws SyncException, IOException, TimeoutException {

        // first we check the destination is a directory and exists
        File f = new File(localPath);
        if (!f.exists()) {
            throw new SyncException(SyncError.NO_DIR_TARGET);
        }
        if (!f.isDirectory()) {
            throw new SyncException(SyncError.TARGET_IS_FILE);
        }

        // get a FileListingService object
        FileListingService fls = new FileListingService(mDevice);

        // compute the number of file to move
        int total = getTotalRemoteFileSize(entries, fls);

        // start the monitor
        monitor.start(total);

        doPull(entries, localPath, fls, monitor);

        monitor.stop();
    }

    /**
     * Pulls a single file.
     * @param remote the remote file
     * @param localFilename The local destination.
     * @param monitor The progress monitor. Cannot be null.
     *
     * @throws IOException in case of an IO exception.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     * @throws SyncException in case of a sync exception.
     *
     * @see FileListingService.FileEntry
     * @see #getNullProgressMonitor()
     */
    public void pullFile(FileEntry remote, String localFilename, ISyncProgressMonitor monitor)
            throws IOException, SyncException, TimeoutException {
        int total = remote.getSizeValue();
        monitor.start(total);

        doPullFile(remote.getFullPath(), localFilename, monitor);

        monitor.stop();
    }

    /**
     * Pulls a single file.
     * <p>Because this method just deals with a String for the remote file instead of a
     * {@link FileEntry}, the size of the file being pulled is unknown and the
     * {@link ISyncProgressMonitor} will not properly show the progress
     * @param remoteFilepath the full path to the remote file
     * @param localFilename The local destination.
     * @param monitor The progress monitor. Cannot be null.
     *
     * @throws IOException in case of an IO exception.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     * @throws SyncException in case of a sync exception.
     *
     * @see #getNullProgressMonitor()
     */
    public void pullFile(String remoteFilepath, String localFilename,
            ISyncProgressMonitor monitor) throws TimeoutException, IOException, SyncException {
        FileStat fileStat = statFile(remoteFilepath);
        if (fileStat == null) {
            // attempts to download anyway
        } else if (fileStat.getMode() == 0) {
            throw new SyncException(SyncError.NO_REMOTE_OBJECT);
        }

        monitor.start(0);
        //TODO: use the {@link FileListingService} to get the file size.

        doPullFile(remoteFilepath, localFilename, monitor);

        monitor.stop();
    }

    /**
     * Push several files.
     * @param local An array of loca files to push
     * @param remote the remote {@link FileEntry} representing a directory.
     * @param monitor The progress monitor. Cannot be null.
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    public void push(String[] local, FileEntry remote, ISyncProgressMonitor monitor)
            throws SyncException, IOException, TimeoutException {
        if (!remote.isDirectory()) {
            throw new SyncException(SyncError.REMOTE_IS_FILE);
        }

        // make a list of File from the list of String
        ArrayList<File> files = new ArrayList<File>();
        for (String path : local) {
            files.add(new File(path));
        }

        // get the total count of the bytes to transfer
        File[] fileArray = files.toArray(new File[files.size()]);
        int total = getTotalLocalFileSize(fileArray);

        monitor.start(total);

        doPush(fileArray, remote.getFullPath(), monitor);

        monitor.stop();
    }

    /**
     * Push a single file.
     * @param local the local filepath.
     * @param remote The remote filepath.
     * @param monitor The progress monitor. Cannot be null.
     *
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    public void pushFile(String local, String remote, ISyncProgressMonitor monitor)
            throws SyncException, IOException, TimeoutException {
        File f = new File(local);
        if (!f.exists()) {
            throw new SyncException(SyncError.NO_LOCAL_FILE);
        }

        if (f.isDirectory()) {
            throw new SyncException(SyncError.LOCAL_IS_DIRECTORY);
        }

        monitor.start((int)f.length());

        doPushFile(local, remote, monitor);

        monitor.stop();
    }

    /**
     * compute the recursive file size of all the files in the list. Folder
     * have a weight of 1.
     * @param entries
     * @param fls
     * @return
     */
    private int getTotalRemoteFileSize(FileEntry[] entries, FileListingService fls) {
        int count = 0;
        for (FileEntry e : entries) {
            int type = e.getType();
            if (type == FileListingService.TYPE_DIRECTORY) {
                // get the children
                FileEntry[] children = fls.getChildren(e, false, null);
                count += getTotalRemoteFileSize(children, fls) + 1;
            } else if (type == FileListingService.TYPE_FILE) {
                count += e.getSizeValue();
            }
        }

        return count;
    }

    /**
     * compute the recursive file size of all the files in the list. Folder
     * have a weight of 1.
     * This does not check for circular links.
     * @param files
     * @return
     */
    private int getTotalLocalFileSize(File[] files) {
        int count = 0;

        for (File f : files) {
            if (f.exists()) {
                if (f.isDirectory()) {
                    return getTotalLocalFileSize(f.listFiles()) + 1;
                } else if (f.isFile()) {
                    count += f.length();
                }
            }
        }

        return count;
    }

    /**
     * Pulls multiple files/folders recursively.
     * @param entries The list of entry to pull
     * @param localPath the localpath to a directory
     * @param fileListingService a FileListingService object to browse through remote directories.
     * @param monitor the progress monitor. Must be started already.
     *
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    private void doPull(FileEntry[] entries, String localPath,
            FileListingService fileListingService,
            ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException {

        for (FileEntry e : entries) {
            // check if we're cancelled
            if (monitor.isCanceled()) {
                throw new SyncException(SyncError.CANCELED);
            }

            // get type (we only pull directory and files for now)
            int type = e.getType();
            if (type == FileListingService.TYPE_DIRECTORY) {
                monitor.startSubTask(e.getFullPath());
                String dest = localPath + File.separator + e.getName();

                // make the directory
                File d = new File(dest);
                d.mkdir();

                // then recursively call the content. Since we did a ls command
                // to get the number of files, we can use the cache
                FileEntry[] children = fileListingService.getChildren(e, true, null);
                doPull(children, dest, fileListingService, monitor);
                monitor.advance(1);
            } else if (type == FileListingService.TYPE_FILE) {
                monitor.startSubTask(e.getFullPath());
                String dest = localPath + File.separator + e.getName();
                doPullFile(e.getFullPath(), dest, monitor);
            }
        }
    }

    /**
     * Pulls a remote file
     * @param remotePath the remote file (length max is 1024)
     * @param localPath the local destination
     * @param monitor the monitor. The monitor must be started already.
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    private void doPullFile(String remotePath, String localPath,
            ISyncProgressMonitor monitor) throws IOException, SyncException, TimeoutException {
        byte[] msg = null;
        byte[] pullResult = new byte[8];

        final int timeOut = DdmPreferences.getTimeOut();

        try {
            byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);

            if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
                throw new SyncException(SyncError.REMOTE_PATH_LENGTH);
            }

            // create the full request message
            msg = createFileReq(ID_RECV, remotePathContent);

            // and send it.
            AdbHelper.write(mChannel, msg, -1, timeOut);

            // read the result, in a byte array containing 2 ints
            // (id, size)
            AdbHelper.read(mChannel, pullResult, -1, timeOut);

            // check we have the proper data back
            if (!checkResult(pullResult, ID_DATA) &&
                    !checkResult(pullResult, ID_DONE)) {
                throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
                        readErrorMessage(pullResult, timeOut));
            }
        } catch (UnsupportedEncodingException e) {
            throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e);
        }

        // access the destination file
        File f = new File(localPath);

        // create the stream to write in the file. We use a new try/catch block to differentiate
        // between file and network io exceptions.
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(f);

            // the buffer to read the data
            byte[] data = new byte[SYNC_DATA_MAX];

            // loop to get data until we're done.
            while (true) {
                // check if we're cancelled
                if (monitor.isCanceled()) {
                    throw new SyncException(SyncError.CANCELED);
                }

                // if we're done, we stop the loop
                if (checkResult(pullResult, ID_DONE)) {
                    break;
                }
                if (!checkResult(pullResult, ID_DATA)) {
                    // hmm there's an error
                    throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
                            readErrorMessage(pullResult, timeOut));
                }
                int length = ArrayHelper.swap32bitFromArray(pullResult, 4);
                if (length > SYNC_DATA_MAX) {
                    // buffer overrun!
                    // error and exit
                    throw new SyncException(SyncError.BUFFER_OVERRUN);
                }

                // now read the length we received
                AdbHelper.read(mChannel, data, length, timeOut);

                // get the header for the next packet.
                AdbHelper.read(mChannel, pullResult, -1, timeOut);

                // write the content in the file
                fos.write(data, 0, length);

                monitor.advance(length);
            }

            fos.flush();
        } catch (IOException e) {
            Log.e("ddms", String.format("Failed to open local file %s for writing, Reason: %s",
                    f.getAbsolutePath(), e.toString()));
            throw new SyncException(SyncError.FILE_WRITE_ERROR);
        } finally {
            if (fos != null) {
                fos.close();
            }
        }
    }


    /**
     * Push multiple files
     * @param fileArray
     * @param remotePath
     * @param monitor
     *
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    private void doPush(File[] fileArray, String remotePath, ISyncProgressMonitor monitor)
            throws SyncException, IOException, TimeoutException {
        for (File f : fileArray) {
            // check if we're canceled
            if (monitor.isCanceled()) {
                throw new SyncException(SyncError.CANCELED);
            }
            if (f.exists()) {
                if (f.isDirectory()) {
                    // append the name of the directory to the remote path
                    String dest = remotePath + "/" + f.getName(); // $NON-NLS-1S
                    monitor.startSubTask(dest);
                    doPush(f.listFiles(), dest, monitor);

                    monitor.advance(1);
                } else if (f.isFile()) {
                    // append the name of the file to the remote path
                    String remoteFile = remotePath + "/" + f.getName(); // $NON-NLS-1S
                    monitor.startSubTask(remoteFile);
                    doPushFile(f.getAbsolutePath(), remoteFile, monitor);
                }
            }
        }
    }

    /**
     * Push a single file
     * @param localPath the local file to push
     * @param remotePath the remote file (length max is 1024)
     * @param monitor the monitor. The monitor must be started already.
     *
     * @throws SyncException if file could not be pushed
     * @throws IOException in case of I/O error on the connection.
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    private void doPushFile(String localPath, String remotePath,
            ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException {
        FileInputStream fis = null;
        byte[] msg;

        final int timeOut = DdmPreferences.getTimeOut();
        File f = new File(localPath);

        try {
            byte[] remotePathContent = remotePath.getBytes(AdbHelper.DEFAULT_ENCODING);

            if (remotePathContent.length > REMOTE_PATH_MAX_LENGTH) {
                throw new SyncException(SyncError.REMOTE_PATH_LENGTH);
            }

            // create the stream to read the file
            fis = new FileInputStream(f);

            // create the header for the action
            msg = createSendFileReq(ID_SEND, remotePathContent, 0644);

            // and send it. We use a custom try/catch block to make the difference between
            // file and network IO exceptions.
            AdbHelper.write(mChannel, msg, -1, timeOut);

            System.arraycopy(ID_DATA, 0, getBuffer(), 0, ID_DATA.length);

            // look while there is something to read
            while (true) {
                // check if we're canceled
                if (monitor.isCanceled()) {
                    throw new SyncException(SyncError.CANCELED);
                }

                // read up to SYNC_DATA_MAX
                int readCount = fis.read(getBuffer(), 8, SYNC_DATA_MAX);

                if (readCount == -1) {
                    // we reached the end of the file
                    break;
                }

                // now send the data to the device
                // first write the amount read
                ArrayHelper.swap32bitsToArray(readCount, getBuffer(), 4);

                // now write it
                AdbHelper.write(mChannel, getBuffer(), readCount+8, timeOut);

                // and advance the monitor
                monitor.advance(readCount);
            }
        } catch (UnsupportedEncodingException e) {
            throw new SyncException(SyncError.REMOTE_PATH_ENCODING, e);
        } finally {
            // close the local file
            if (fis != null) {
                fis.close();
            }
        }

        // create the DONE message
        long time = f.lastModified() / 1000;
        msg = createReq(ID_DONE, (int)time);

        // and send it.
        AdbHelper.write(mChannel, msg, -1, timeOut);

        // read the result, in a byte array containing 2 ints
        // (id, size)
        byte[] result = new byte[8];
        AdbHelper.read(mChannel, result, -1 /* full length */, timeOut);

        if (!checkResult(result, ID_OKAY)) {
            throw new SyncException(SyncError.TRANSFER_PROTOCOL_ERROR,
                    readErrorMessage(result, timeOut));
        }
    }

    /**
     * Reads an error message from the opened {@link #mChannel}.
     * @param result the current adb result. Must contain both FAIL and the length of the message.
     * @param timeOut
     * @return
     * @throws TimeoutException in case of a timeout reading responses from the device.
     * @throws IOException
     */
    private String readErrorMessage(byte[] result, final int timeOut) throws TimeoutException,
            IOException {
        if (checkResult(result, ID_FAIL)) {
            int len = ArrayHelper.swap32bitFromArray(result, 4);

            if (len > 0) {
                AdbHelper.read(mChannel, getBuffer(), len, timeOut);

                String message = new String(getBuffer(), 0, len);
                Log.e("ddms", "transfer error: " + message);

                return message;
            }
        }

        return null;
    }

    /**
     * Returns the stat info of the remote file.
     * @param path the remote file
     * @return an FileStat containing the mode, size and last modified info if all went well or null
     *      otherwise
     * @throws IOException
     * @throws TimeoutException in case of a timeout reading responses from the device.
     */
    @Nullable
    public FileStat statFile(@NonNull String path) throws TimeoutException, IOException {
        // create the stat request message.
        byte[] msg = createFileReq(ID_STAT, path);

        AdbHelper.write(mChannel, msg, -1 /* full length */, DdmPreferences.getTimeOut());

        // read the result, in a byte array containing 4 ints
        // (id, mode, size, time)
        byte[] statResult = new byte[16];
        AdbHelper.read(mChannel, statResult, -1 /* full length */, DdmPreferences.getTimeOut());

        // check we have the proper data back
        if (!checkResult(statResult, ID_STAT)) {
            return null;
        }

        final int mode = ArrayHelper.swap32bitFromArray(statResult, 4);
        final int size = ArrayHelper.swap32bitFromArray(statResult, 8);
        final int lastModifiedSecs = ArrayHelper.swap32bitFromArray(statResult, 12);
        return new FileStat(mode, size, lastModifiedSecs);
    }

    /**
     * Create a command with a code and an int values
     * @param command
     * @param value
     * @return
     */
    private static byte[] createReq(byte[] command, int value) {
        byte[] array = new byte[8];

        System.arraycopy(command, 0, array, 0, 4);
        ArrayHelper.swap32bitsToArray(value, array, 4);

        return array;
    }

    /**
     * Creates the data array for a stat request.
     * @param command the 4 byte command (ID_STAT, ID_RECV, ...)
     * @param path The path of the remote file on which to execute the command
     * @return the byte[] to send to the device through adb
     */
    private static byte[] createFileReq(byte[] command, String path) {
        byte[] pathContent = null;
        try {
            pathContent = path.getBytes(AdbHelper.DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException e) {
            return null;
        }

        return createFileReq(command, pathContent);
    }

    /**
     * Creates the data array for a file request. This creates an array with a 4 byte command + the
     * remote file name.
     * @param command the 4 byte command (ID_STAT, ID_RECV, ...).
     * @param path The path, as a byte array, of the remote file on which to
     *      execute the command.
     * @return the byte[] to send to the device through adb
     */
    private static byte[] createFileReq(byte[] command, byte[] path) {
        byte[] array = new byte[8 + path.length];

        System.arraycopy(command, 0, array, 0, 4);
        ArrayHelper.swap32bitsToArray(path.length, array, 4);
        System.arraycopy(path, 0, array, 8, path.length);

        return array;
    }

    private static byte[] createSendFileReq(byte[] command, byte[] path, int mode) {
        // make the mode into a string
        String modeStr = "," + (mode & 0777); // $NON-NLS-1S
        byte[] modeContent = null;
        try {
            modeContent = modeStr.getBytes(AdbHelper.DEFAULT_ENCODING);
        } catch (UnsupportedEncodingException e) {
            return null;
        }

        byte[] array = new byte[8 + path.length + modeContent.length];

        System.arraycopy(command, 0, array, 0, 4);
        ArrayHelper.swap32bitsToArray(path.length + modeContent.length, array, 4);
        System.arraycopy(path, 0, array, 8, path.length);
        System.arraycopy(modeContent, 0, array, 8 + path.length, modeContent.length);

        return array;


    }

    /**
     * Checks the result array starts with the provided code
     * @param result The result array to check
     * @param code The 4 byte code.
     * @return true if the code matches.
     */
    private static boolean checkResult(byte[] result, byte[] code) {
        return !(result[0] != code[0] ||
                result[1] != code[1] ||
                result[2] != code[2] ||
                result[3] != code[3]);

    }

    private static int getFileType(int mode) {
        if ((mode & S_ISOCK) == S_ISOCK) {
            return FileListingService.TYPE_SOCKET;
        }

        if ((mode & S_IFLNK) == S_IFLNK) {
            return FileListingService.TYPE_LINK;
        }

        if ((mode & S_IFREG) == S_IFREG) {
            return FileListingService.TYPE_FILE;
        }

        if ((mode & S_IFBLK) == S_IFBLK) {
            return FileListingService.TYPE_BLOCK;
        }

        if ((mode & S_IFDIR) == S_IFDIR) {
            return FileListingService.TYPE_DIRECTORY;
        }

        if ((mode & S_IFCHR) == S_IFCHR) {
            return FileListingService.TYPE_CHARACTER;
        }

        if ((mode & S_IFIFO) == S_IFIFO) {
            return FileListingService.TYPE_FIFO;
        }

        return FileListingService.TYPE_OTHER;
    }

    /**
     * Retrieve the buffer, allocating if necessary
     * @return
     */
    private byte[] getBuffer() {
        if (mBuffer == null) {
            // create the buffer used to read.
            // we read max SYNC_DATA_MAX, but we need 2 4 bytes at the beginning.
            mBuffer = new byte[SYNC_DATA_MAX + 8];
        }
        return mBuffer;
    }
}
