# Copyright (C) 2003-2007  Robey Pointer <robeypointer@gmail.com>
#
# This file is part of paramiko.
#
# Paramiko 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.
#
# Paramiko 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 Paramiko; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.

import select
import socket
import struct

from paramiko import util
from paramiko.common import DEBUG, byte_chr, byte_ord
from paramiko.message import Message


(
    CMD_INIT,
    CMD_VERSION,
    CMD_OPEN,
    CMD_CLOSE,
    CMD_READ,
    CMD_WRITE,
    CMD_LSTAT,
    CMD_FSTAT,
    CMD_SETSTAT,
    CMD_FSETSTAT,
    CMD_OPENDIR,
    CMD_READDIR,
    CMD_REMOVE,
    CMD_MKDIR,
    CMD_RMDIR,
    CMD_REALPATH,
    CMD_STAT,
    CMD_RENAME,
    CMD_READLINK,
    CMD_SYMLINK,
) = range(1, 21)
(CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS) = range(101, 106)
(CMD_EXTENDED, CMD_EXTENDED_REPLY) = range(200, 202)

SFTP_OK = 0
(
    SFTP_EOF,
    SFTP_NO_SUCH_FILE,
    SFTP_PERMISSION_DENIED,
    SFTP_FAILURE,
    SFTP_BAD_MESSAGE,
    SFTP_NO_CONNECTION,
    SFTP_CONNECTION_LOST,
    SFTP_OP_UNSUPPORTED,
) = range(1, 9)

SFTP_DESC = [
    "Success",
    "End of file",
    "No such file",
    "Permission denied",
    "Failure",
    "Bad message",
    "No connection",
    "Connection lost",
    "Operation unsupported",
]

SFTP_FLAG_READ = 0x1
SFTP_FLAG_WRITE = 0x2
SFTP_FLAG_APPEND = 0x4
SFTP_FLAG_CREATE = 0x8
SFTP_FLAG_TRUNC = 0x10
SFTP_FLAG_EXCL = 0x20

_VERSION = 3


# for debugging
CMD_NAMES = {
    CMD_INIT: "init",
    CMD_VERSION: "version",
    CMD_OPEN: "open",
    CMD_CLOSE: "close",
    CMD_READ: "read",
    CMD_WRITE: "write",
    CMD_LSTAT: "lstat",
    CMD_FSTAT: "fstat",
    CMD_SETSTAT: "setstat",
    CMD_FSETSTAT: "fsetstat",
    CMD_OPENDIR: "opendir",
    CMD_READDIR: "readdir",
    CMD_REMOVE: "remove",
    CMD_MKDIR: "mkdir",
    CMD_RMDIR: "rmdir",
    CMD_REALPATH: "realpath",
    CMD_STAT: "stat",
    CMD_RENAME: "rename",
    CMD_READLINK: "readlink",
    CMD_SYMLINK: "symlink",
    CMD_STATUS: "status",
    CMD_HANDLE: "handle",
    CMD_DATA: "data",
    CMD_NAME: "name",
    CMD_ATTRS: "attrs",
    CMD_EXTENDED: "extended",
    CMD_EXTENDED_REPLY: "extended_reply",
}


# TODO: rewrite SFTP file/server modules' overly-flexible "make a request with
# xyz components" so we don't need this very silly method of signaling whether
# a given Python integer should be 32- or 64-bit.
# NOTE: this only became an issue when dropping Python 2 support; prior to
# doing so, we had to support actual-longs, which served as that signal. This
# is simply recreating that structure in a more tightly scoped fashion.
class int64(int):
    pass


class SFTPError(Exception):
    pass


class BaseSFTP:
    def __init__(self):
        self.logger = util.get_logger("paramiko.sftp")
        self.sock = None
        self.ultra_debug = False

    # ...internals...

    def _send_version(self):
        m = Message()
        m.add_int(_VERSION)
        self._send_packet(CMD_INIT, m)
        t, data = self._read_packet()
        if t != CMD_VERSION:
            raise SFTPError("Incompatible sftp protocol")
        version = struct.unpack(">I", data[:4])[0]
        #        if version != _VERSION:
        #            raise SFTPError('Incompatible sftp protocol')
        return version

    def _send_server_version(self):
        # winscp will freak out if the server sends version info before the
        # client finishes sending INIT.
        t, data = self._read_packet()
        if t != CMD_INIT:
            raise SFTPError("Incompatible sftp protocol")
        version = struct.unpack(">I", data[:4])[0]
        # advertise that we support "check-file"
        extension_pairs = ["check-file", "md5,sha1"]
        msg = Message()
        msg.add_int(_VERSION)
        msg.add(*extension_pairs)
        self._send_packet(CMD_VERSION, msg)
        return version

    def _log(self, level, msg, *args):
        self.logger.log(level, msg, *args)

    def _write_all(self, out):
        while len(out) > 0:
            n = self.sock.send(out)
            if n <= 0:
                raise EOFError()
            if n == len(out):
                return
            out = out[n:]
        return

    def _read_all(self, n):
        out = bytes()
        while n > 0:
            if isinstance(self.sock, socket.socket):
                # sometimes sftp is used directly over a socket instead of
                # through a paramiko channel.  in this case, check periodically
                # if the socket is closed.  (for some reason, recv() won't ever
                # return or raise an exception, but calling select on a closed
                # socket will.)
                while True:
                    read, write, err = select.select([self.sock], [], [], 0.1)
                    if len(read) > 0:
                        x = self.sock.recv(n)
                        break
            else:
                x = self.sock.recv(n)

            if len(x) == 0:
                raise EOFError()
            out += x
            n -= len(x)
        return out

    def _send_packet(self, t, packet):
        packet = packet.asbytes()
        out = struct.pack(">I", len(packet) + 1) + byte_chr(t) + packet
        if self.ultra_debug:
            self._log(DEBUG, util.format_binary(out, "OUT: "))
        self._write_all(out)

    def _read_packet(self):
        x = self._read_all(4)
        # most sftp servers won't accept packets larger than about 32k, so
        # anything with the high byte set (> 16MB) is just garbage.
        if byte_ord(x[0]):
            raise SFTPError("Garbage packet received")
        size = struct.unpack(">I", x)[0]
        data = self._read_all(size)
        if self.ultra_debug:
            self._log(DEBUG, util.format_binary(data, "IN: "))
        if size > 0:
            t = byte_ord(data[0])
            return t, data[1:]
        return 0, bytes()
