#!/usr/bin/env python
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>


import os
import stat
from asyncio import TimerHandle
from collections import deque
from enum import auto
from itertools import count
from time import monotonic
from typing import IO, Callable, Deque, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Tuple, Union

from kitty.cli_stub import TransferCLIOptions
from kitty.fast_data_types import FILE_TRANSFER_CODE, wcswidth
from kitty.file_transmission import Action, Compression, FileTransmissionCommand, FileType, NameReprEnum, TransmissionType, encode_bypass, split_for_transfer
from kitty.typing import KeyEventType, ScreenSize
from kitty.utils import sanitize_control_codes

from ..tui.handler import Handler
from ..tui.loop import Loop, debug
from ..tui.operations import styled, without_line_wrap
from ..tui.spinners import Spinner
from ..tui.utils import human_size
from .librsync import LoadSignature, delta_for_file
from .utils import (
    IdentityCompressor,
    ZlibCompressor,
    abspath,
    expand_home,
    home_path,
    print_rsync_stats,
    random_id,
    render_progress_in_width,
    safe_divide,
    should_be_compressed,
)

debug


def get_remote_path(local_path: str, remote_base: str) -> str:
    if not remote_base:
        return local_path.replace(os.sep, '/')
    if remote_base.endswith('/'):
        return os.path.join(remote_base, os.path.basename(local_path))
    return remote_base


class FileState(NameReprEnum):
    waiting_for_start = auto()
    waiting_for_data = auto()
    transmitting = auto()
    finished = auto()
    acknowledged = auto()


class File:

    def __init__(
        self, local_path: str, expanded_local_path: str, file_id: int, stat_result: os.stat_result,
        remote_base: str, file_type: FileType,
    ) -> None:
        self.state = FileState.waiting_for_start
        self.local_path = local_path
        self.display_name = sanitize_control_codes(local_path)
        self.expanded_local_path = expanded_local_path
        self.permissions = stat.S_IMODE(stat_result.st_mode)
        self.mtime = stat_result.st_mtime_ns
        self.file_size = self.bytes_to_transmit = stat_result.st_size
        self.file_hash = stat_result.st_dev, stat_result.st_ino
        self.remote_path = get_remote_path(self.local_path, remote_base)
        self.remote_path = self.remote_path.replace(os.sep, '/')
        self.file_id = hex(file_id)[2:]
        self.hard_link_target = ''
        self.symbolic_link_target = ''
        self.stat_result = stat_result
        self.file_type = file_type
        self.rsync_capable = self.file_type is FileType.regular and self.file_size > 4096
        self.compression_capable = self.file_type is FileType.regular and self.file_size > 4096 and should_be_compressed(self.expanded_local_path)
        self.remote_final_path = ''
        self.remote_initial_size = -1
        self.err_msg = ''
        self.actual_file: Optional[IO[bytes]] = None
        self.transmitted_bytes = 0
        self.reported_progress = 0
        self.transmit_started_at = self.transmit_ended_at = self.done_at = 0.
        self.signature_loader: Optional[LoadSignature] = None
        self.delta_loader: Optional[Iterator[memoryview]] = None

    def start_delta_calculation(self) -> None:
        sl = self.signature_loader
        assert sl is not None
        self.state = FileState.transmitting
        self.delta_loader = delta_for_file(self.expanded_local_path, sl.signature)

    def __repr__(self) -> str:
        return f'File(name={self.display_name}, ft={self.file_type}, state={self.state})'

    def next_chunk(self, sz: int = 1024 * 1024) -> Tuple[bytes, int]:
        if self.file_type is FileType.symlink:
            self.state = FileState.finished
            ans = self.symbolic_link_target.encode('utf-8')
            return ans, len(ans)
        if self.file_type is FileType.link:
            self.state = FileState.finished
            ans = self.hard_link_target.encode('utf-8')
            return ans, len(ans)
        is_last = False
        if self.delta_loader is not None:
            try:
                chunk: Union[bytes, memoryview] = next(self.delta_loader)
            except StopIteration:
                is_last = True
                self.delta_loader = None
                chunk = b''
        else:
            if self.actual_file is None:
                self.actual_file = open(self.expanded_local_path, 'rb')
            chunk = self.actual_file.read(sz)
            is_last = not chunk or self.actual_file.tell() >= self.file_size
        uncompressed_sz = len(chunk)
        cchunk = self.compressor.compress(chunk)
        if is_last and not isinstance(self.compressor, IdentityCompressor):
            cchunk += self.compressor.flush()
        if is_last:
            self.state = FileState.finished
            if self.actual_file is not None:
                self.actual_file.close()
                self.actual_file = None
        return cchunk, uncompressed_sz

    def metadata_command(self, use_rsync: bool = False) -> FileTransmissionCommand:
        self.ttype = TransmissionType.rsync if self.rsync_capable and use_rsync else TransmissionType.simple
        self.compression = Compression.zlib if self.compression_capable else Compression.none
        self.compressor: Union[ZlibCompressor, IdentityCompressor] = ZlibCompressor() if self.compression is Compression.zlib else IdentityCompressor()
        return FileTransmissionCommand(
            action=Action.file, compression=self.compression, ftype=self.file_type,
            name=self.remote_path, permissions=self.permissions, mtime=self.mtime,
            file_id=self.file_id, ttype=self.ttype
        )


def process(cli_opts: TransferCLIOptions, paths: Iterable[str], remote_base: str, counter: Iterator[int]) -> Iterator[File]:
    for x in paths:
        expanded = expand_home(x)
        try:
            s = os.stat(expanded, follow_symlinks=False)
        except OSError as err:
            raise SystemExit(f'Failed to stat {x} with error: {err}') from err
        if stat.S_ISDIR(s.st_mode):
            yield File(x, expanded, next(counter), s, remote_base, FileType.directory)
            new_remote_base = remote_base
            if new_remote_base:
                new_remote_base = new_remote_base.rstrip('/') + '/' + os.path.basename(x) + '/'
            else:
                new_remote_base = x.replace(os.sep, '/').rstrip('/') + '/'
            yield from process(cli_opts, [os.path.join(x, y) for y in os.listdir(expanded)], new_remote_base, counter)
        elif stat.S_ISLNK(s.st_mode):
            yield File(x, expanded, next(counter), s, remote_base, FileType.symlink)
        elif stat.S_ISREG(s.st_mode):
            yield File(x, expanded, next(counter), s, remote_base, FileType.regular)


def process_mirrored_files(cli_opts: TransferCLIOptions, args: Sequence[str]) -> Iterator[File]:
    paths = [abspath(x) for x in args]
    try:
        common_path = os.path.commonpath(paths)
    except ValueError:
        common_path = ''
    home = home_path().rstrip(os.sep)
    if common_path and common_path.startswith(home + os.sep):
        paths = [os.path.join('~', os.path.relpath(x, home)) for x in paths]
    yield from process(cli_opts, paths, '', count(1))


def process_normal_files(cli_opts: TransferCLIOptions, args: Sequence[str]) -> Iterator[File]:
    if len(args) < 2:
        raise SystemExit('Must specify at least one local path and one remote path')
    args = list(args)
    remote_base = args.pop().replace(os.sep, '/')
    if len(args) > 1 and not remote_base.endswith('/'):
        remote_base += '/'
    paths = [abspath(x) for x in args]
    yield from process(cli_opts, paths, remote_base, count(1))


def files_for_send(cli_opts: TransferCLIOptions, args: List[str]) -> Tuple[File, ...]:
    if cli_opts.mode == 'mirror':
        files = list(process_mirrored_files(cli_opts, args))
    else:
        files = list(process_normal_files(cli_opts, args))
    groups: Dict[Tuple[int, int], List[File]] = {}

    # detect hard links
    for f in files:
        groups.setdefault(f.file_hash, []).append(f)
    for group in groups.values():
        if len(group) > 1:
            for lf in group[1:]:
                lf.file_type = FileType.link
                lf.hard_link_target = group[0].file_id

    # detect symlinks to other transferred files
    for f in tuple(files):
        if f.file_type is FileType.symlink:
            try:
                link_dest = os.readlink(f.local_path)
            except OSError:
                files.remove(f)
                continue
            f.symbolic_link_target = f'path:{link_dest}'
            is_abs = os.path.isabs(link_dest)
            q = link_dest if is_abs else os.path.join(os.path.dirname(f.local_path), link_dest)
            try:
                st = os.stat(q)
            except OSError:
                pass
            else:
                fh = st.st_dev, st.st_ino
                if fh in groups:
                    g = tuple(x for x in groups[fh] if os.path.samestat(st, x.stat_result))
                    if g:
                        t = g[0]
                        prefix = 'fid_abs' if is_abs else 'fid'
                        f.symbolic_link_target = f'{prefix}:{t.file_id}'
    return tuple(files)


class SendState(NameReprEnum):
    waiting_for_permission = auto()
    permission_granted = auto()
    permission_denied = auto()
    canceled = auto()


class Transfer:

    def __init__(self, amt: int = 0):
        self.amt = amt
        self.at = monotonic()

    def is_too_old(self, now: float) -> bool:
        return now - self.at > 30


class ProgressTracker:

    def __init__(self, total_size_of_all_files: int):
        self.total_size_of_all_files = total_size_of_all_files
        self.total_bytes_to_transfer = total_size_of_all_files
        self.active_file: Optional[File] = None
        self.total_transferred = 0
        self.transfers: Deque[Transfer] = deque()
        self.transfered_stats_amt = 0
        self.transfered_stats_interval = 0.
        self.started_at = 0.
        self.signature_bytes = 0
        self.total_reported_progress = 0

    def change_active_file(self, nf: File) -> None:
        now = monotonic()
        self.active_file = nf
        nf.transmit_started_at = now

    def start_transfer(self) -> None:
        self.transfers.append(Transfer())
        self.started_at = monotonic()

    def on_transmit(self, amt: int) -> None:
        if self.active_file is not None:
            self.active_file.transmitted_bytes += amt
        self.total_transferred += amt
        self.transfers.append(Transfer(amt))
        now = self.transfers[-1].at
        while len(self.transfers) > 2 and self.transfers[0].is_too_old(now):
            self.transfers.popleft()
        self.transfered_stats_interval = now - self.transfers[0].at
        self.transfered_stats_amt = sum(t.amt for t in self.transfers)

    def on_file_progress(self, af: File, delta: int) -> None:
        if delta > 0:
            self.total_reported_progress += delta

    def on_file_done(self, af: File) -> None:
        af.done_at = monotonic()


class SendManager:

    def __init__(
        self, request_id: str, files: Tuple[File, ...],
        bypass: Optional[str] = None, use_rsync: bool = False,
        file_progress: Callable[[File, int], None] = lambda f, i: None,
        file_done: Callable[[File], None] = lambda f: None,
    ):
        self.use_rsync = use_rsync
        self.files = files
        self.bypass = encode_bypass(request_id, bypass) if bypass else ''
        self.fid_map = {f.file_id: f for f in self.files}
        self.request_id = request_id
        self.state = SendState.waiting_for_permission
        self.all_acknowledged = self.all_started = self.has_transmitting = self.has_rsync = False
        self.active_idx: Optional[int] = None
        self.current_chunk_uncompressed_sz: Optional[int] = None
        self.prefix = f'\x1b]{FILE_TRANSFER_CODE};id={self.request_id};'
        self.suffix = '\x1b\\'
        self.progress = ProgressTracker(sum(df.file_size for df in self.files if df.file_size >= 0))
        self.file_done = file_done
        self.file_progress = file_progress
        self.last_progress_file: Optional[File] = None

    @property
    def active_file(self) -> Optional[File]:
        if self.active_idx is not None:
            ans = self.files[self.active_idx]
            if ans.state is FileState.transmitting:
                return ans
        return None

    def activate_next_ready_file(self) -> Optional[File]:
        if self.active_idx is not None:
            paf = self.files[self.active_idx]
            paf.transmit_ended_at = monotonic()
        for i, f in enumerate(self.files):
            if f.state is FileState.transmitting:
                self.active_idx = i
                self.update_collective_statuses()
                self.progress.change_active_file(f)
                return f
        self.active_idx = None
        self.update_collective_statuses()
        return None

    def update_collective_statuses(self) -> None:
        found_not_started = found_not_done = False
        has_rsync = has_transmitting = False
        for f in self.files:
            if f.state is not FileState.acknowledged:
                found_not_done = True
            if f.state is FileState.waiting_for_start:
                found_not_started = True
            elif f.state is FileState.transmitting:
                has_transmitting = True
            if f.ttype is TransmissionType.rsync:
                has_rsync = True

        self.all_acknowledged = not found_not_done
        self.all_started = not found_not_started
        self.has_rsync = has_rsync
        self.has_transmitting = has_transmitting

    def start_transfer(self) -> str:
        return FileTransmissionCommand(action=Action.send, bypass=self.bypass).serialize()

    def next_chunks(self) -> Iterator[str]:
        if self.active_file is None:
            self.activate_next_ready_file()
        af = self.active_file
        if af is None:
            return
        chunk = b''
        self.current_chunk_uncompressed_sz = 0
        while af.state is not FileState.finished and not chunk:
            chunk, usz = af.next_chunk()
            self.current_chunk_uncompressed_sz += usz
        is_last = af.state is FileState.finished
        if len(chunk):
            for ftc in split_for_transfer(chunk, file_id=af.file_id, mark_last=is_last):
                yield ftc.serialize()
        elif is_last:
            yield FileTransmissionCommand(action=Action.end_data, file_id=af.file_id, data=b'').serialize()

    def send_file_metadata(self) -> Iterator[str]:
        for f in self.files:
            yield f.metadata_command(self.use_rsync).serialize()

    def on_file_status_update(self, ftc: FileTransmissionCommand) -> None:
        file = self.fid_map.get(ftc.file_id)
        if file is None:
            return
        if ftc.status == 'STARTED':
            file.remote_final_path = ftc.name
            file.remote_initial_size = ftc.size
            if file.file_type is FileType.directory:
                file.state = FileState.finished
            else:
                file.state = FileState.waiting_for_data if ftc.ttype is TransmissionType.rsync else FileState.transmitting
                if file.state is FileState.waiting_for_data:
                    file.signature_loader = LoadSignature()
            self.update_collective_statuses()
        elif ftc.status == 'PROGRESS':
            self.last_progress_file = file
            change = ftc.size - file.reported_progress
            file.reported_progress = ftc.size
            self.progress.on_file_progress(file, change)
            self.file_progress(file, change)
        else:
            if ftc.name and not file.remote_final_path:
                file.remote_final_path = ftc.name
            file.state = FileState.acknowledged
            if ftc.status == 'OK':
                if ftc.size > 0:
                    change = ftc.size - file.reported_progress
                    file.reported_progress = ftc.size
                    self.progress.on_file_progress(file, change)
                    self.file_progress(file, change)
            else:
                file.err_msg = ftc.status
            self.progress.on_file_done(file)
            self.file_done(file)
            if self.active_idx is not None and file is self.files[self.active_idx]:
                self.active_idx = None
            self.update_collective_statuses()

    def on_signature_data_received(self, ftc: FileTransmissionCommand) -> None:
        file = self.fid_map.get(ftc.file_id)
        if file is None or file.state is not FileState.waiting_for_data:
            return
        sl = file.signature_loader
        assert sl is not None
        sl.add_chunk(ftc.data)
        self.progress.signature_bytes += len(ftc.data)
        if ftc.action is Action.end_data:
            sl.commit()
            file.start_delta_calculation()
            self.update_collective_statuses()

    def on_file_transfer_response(self, ftc: FileTransmissionCommand) -> None:
        if ftc.action is Action.status:
            if ftc.file_id:
                self.on_file_status_update(ftc)
            else:
                self.state = SendState.permission_granted if ftc.status == 'OK' else SendState.permission_denied
        elif ftc.action in (Action.data, Action.end_data):
            if ftc.file_id:
                self.on_signature_data_received(ftc)


class Send(Handler):
    use_alternate_screen = False

    def __init__(self, cli_opts: TransferCLIOptions, files: Tuple[File, ...]):
        Handler.__init__(self)
        self.manager = SendManager(
            random_id(), files, cli_opts.permissions_bypass, cli_opts.transmit_deltas, self.on_file_progress, self.on_file_done)
        self.cli_opts = cli_opts
        self.transmit_started = False
        self.file_metadata_sent = False
        self.quit_after_write_code: Optional[int] = None
        self.check_paths_printed = False
        names = tuple(x.display_name for x in self.manager.files)
        self.max_name_length = max(6, max(map(wcswidth, names)))
        self.spinner = Spinner()
        self.progress_drawn = True
        self.done_files: List[File] = []
        self.done_file_ids: Set[str] = set()
        self.failed_files: List[File] = []
        self.transmit_ok_checked = False
        self.progress_update_call: Optional[TimerHandle] = None

    def send_payload(self, payload: str) -> None:
        self.write(self.manager.prefix)
        self.write(payload)
        self.write(self.manager.suffix)

    def on_file_transfer_response(self, ftc: FileTransmissionCommand) -> None:
        if ftc.id != self.manager.request_id:
            return
        if ftc.status == 'CANCELED' and ftc.action is Action.status:
            self.quit_loop(1)
            return
        if self.quit_after_write_code is not None or self.manager.state is SendState.canceled:
            return
        before = self.manager.state
        self.manager.on_file_transfer_response(ftc)
        if before == SendState.waiting_for_permission:
            if self.manager.state == SendState.permission_denied:
                self.cmd.styled('Permission denied for this transfer', fg='red')
                self.print()
                self.quit_loop(1)
                return
            if self.manager.state == SendState.permission_granted:
                self.cmd.styled('Permission granted for this transfer', fg='green')
                self.print()
                self.send_file_metadata()
        self.asyncio_loop.call_soon(self.loop_tick)

    def start_transfer(self) -> None:
        if self.manager.active_file is None:
            self.manager.activate_next_ready_file()
        if self.manager.active_file is not None:
            self.transmit_started = True
            self.manager.progress.start_transfer()
            self.transmit_next_chunk()
            self.draw_progress()

    def print_check_paths(self) -> None:
        if self.check_paths_printed:
            return
        self.check_paths_printed = True
        self.print('The following file transfers will be performed. A red destination means an existing file will be overwritten.')
        for df in self.manager.files:
            self.cmd.styled(df.file_type.short_text, fg=df.file_type.color)
            self.print(end=' ')
            self.print(df.display_name, '→', end=' ')
            self.cmd.styled(df.remote_final_path, fg='red' if df.remote_initial_size > -1 else None)
            self.print()
        self.print(f'Transferring {len(self.manager.files)} files of total size: {human_size(self.manager.progress.total_bytes_to_transfer)}')
        self.print()
        self.print_continue_msg()

    def print_continue_msg(self) -> None:
        self.print(
            'Press', styled('y', fg='green', bold=True, fg_intense=True), 'to continue or',
            styled('n', fg='red', bold=True, fg_intense=True), 'to abort')

    def on_text(self, text: str, in_bracketed_paste: bool = False) -> None:
        if self.quit_after_write_code is not None:
            return
        if self.check_paths_printed and not self.transmit_started:
            if text.lower() == 'y':
                self.start_transfer()
                if self.manager.all_acknowledged:
                    self.refresh_progress()
                    self.transfer_finished()
                return
            if text.lower() == 'n':
                del self.failed_files[:]
                self.abort_transfer()
                self.print('Sending cancel request to terminal')
                return
            self.print_continue_msg()

    def on_key(self, key_event: KeyEventType) -> None:
        if self.quit_after_write_code is not None:
            return
        if key_event.matches('esc'):
            if self.check_paths_printed and not self.transmit_started:
                del self.failed_files[:]
                self.abort_transfer()
                self.print('Sending cancel request to terminal')
            else:
                self.on_interrupt()

    def check_for_transmit_ok(self) -> None:
        if self.transmit_ok_checked:
            return self.start_transfer()
        if self.manager.state is not SendState.permission_granted:
            return
        if self.cli_opts.confirm_paths:
            if self.manager.all_started:
                self.print_check_paths()
            return
        self.transmit_ok_checked = True
        self.start_transfer()

    def transmit_next_chunk(self) -> None:
        found_chunk = False
        for chunk in self.manager.next_chunks():
            self.send_payload(chunk)
            found_chunk = True
        if not found_chunk:
            if self.manager.all_acknowledged:
                self.transfer_finished()

    def transfer_finished(self) -> None:
        self.send_payload(FileTransmissionCommand(action=Action.finish).serialize())
        self.quit_after_write_code = 1 if self.failed_files else 0

    def on_writing_finished(self) -> None:
        chunk_transmitted = self.manager.current_chunk_uncompressed_sz is not None
        if chunk_transmitted:
            self.manager.progress.on_transmit(self.manager.current_chunk_uncompressed_sz or 0)
            self.manager.current_chunk_uncompressed_sz = None
        if self.quit_after_write_code is not None:
            self.quit_loop(self.quit_after_write_code)
            return
        if self.manager.state is SendState.permission_granted and (not self.transmit_started or chunk_transmitted):
            self.asyncio_loop.call_soon(self.loop_tick)

    def loop_tick(self) -> None:
        if self.manager.state is SendState.waiting_for_permission:
            return
        if self.transmit_started:
            self.transmit_next_chunk()
            self.refresh_progress()
        else:
            self.check_for_transmit_ok()

    def initialize(self) -> None:
        self.send_payload(self.manager.start_transfer())
        if self.cli_opts.permissions_bypass:
            # dont wait for permission, not needed with a bypass and
            # avoids a roundtrip
            self.send_file_metadata()
        self.cmd.set_cursor_visible(False)

    def finalize(self) -> None:
        self.cmd.set_cursor_visible(True)

    def send_file_metadata(self) -> None:
        if not self.file_metadata_sent:
            for payload in self.manager.send_file_metadata():
                self.send_payload(payload)
            self.file_metadata_sent = True

    def on_term(self) -> None:
        if self.quit_after_write_code is not None:
            return
        self.cmd.styled('Terminate requested, cancelling transfer, transferred files are in undefined state', fg='red')
        self.print()
        self.abort_transfer(delay=2)

    def on_interrupt(self) -> None:
        if self.quit_after_write_code is not None:
            return
        if self.manager.state is SendState.canceled:
            self.print('Waiting for canceled acknowledgement from terminal, will abort in a few seconds if no response received')
            return
        self.cmd.styled('Interrupt requested, cancelling transfer, transferred files are in undefined state', fg='red')
        self.print()
        self.abort_transfer()

    def abort_transfer(self, delay: float = 5) -> None:
        self.send_payload(FileTransmissionCommand(action=Action.cancel).serialize())
        self.manager.state = SendState.canceled
        self.asyncio_loop.call_later(delay, self.quit_loop, 1)

    def render_progress(
        self, name: str, spinner_char: str = ' ', bytes_so_far: int = 0, total_bytes: int = 0,
        secs_so_far: float = 0., bytes_per_sec: float = 0., is_complete: bool = False
    ) -> None:
        if is_complete:
            bytes_so_far = total_bytes
        self.write(render_progress_in_width(
            name, width=self.screen_size.cols, max_path_length=self.max_name_length, spinner_char=spinner_char,
            bytes_so_far=bytes_so_far, total_bytes=total_bytes, secs_so_far=secs_so_far,
            bytes_per_sec=bytes_per_sec, is_complete=is_complete
        ))

    def erase_progress(self) -> None:
        if self.progress_drawn:
            self.cmd.move_cursor_by(2, 'up')
            self.write('\r')
            self.cmd.clear_to_end_of_screen()
            self.progress_drawn = False

    def schedule_progress_update(self, delay: float = 0.1) -> None:
        if self.progress_update_call is None:
            self.progress_update_call = self.asyncio_loop.call_later(delay, self.refresh_progress)
        elif self.asyncio_loop.time() + delay < self.progress_update_call.when():
            self.progress_update_call.cancel()
            self.progress_update_call = self.asyncio_loop.call_later(delay, self.refresh_progress)

    def on_file_progress(self, file: File, change: int) -> None:
        self.schedule_progress_update()

    def on_file_done(self, file: File) -> None:
        self.done_files.append(file)
        if file.err_msg:
            self.failed_files.append(file)
        self.schedule_progress_update()

    @Handler.atomic_update
    def draw_progress(self) -> None:
        with without_line_wrap(self.write):
            for df in self.done_files:
                sc = styled('✔', fg='green') if not df.err_msg else styled('✘', fg='red')
                if df.file_type is FileType.regular:
                    self.draw_progress_for_current_file(df, spinner_char=sc, is_complete=True)
                else:
                    self.write(f'{sc} {df.display_name} {styled(df.file_type.name, dim=True, italic=True)}')
                self.print()
                self.done_file_ids.add(df.file_id)
            del self.done_files[:]
            is_complete = self.quit_after_write_code is not None
            if is_complete:
                sc = styled('✔', fg='green') if self.quit_after_write_code == 0 else styled('✘', fg='red')
            else:
                sc = self.spinner()
            p = self.manager.progress
            now = monotonic()
            if is_complete:
                self.cmd.repeat('─', self.screen_size.width)
            else:
                af = self.manager.last_progress_file
                if af is None or af.file_id in self.done_file_ids:
                    if self.manager.has_rsync and not self.manager.has_transmitting:
                        self.print(sc, 'Transferring rsync signatures...', end='')
                    else:
                        self.print(sc, 'Transferring metadata...', end='')
                else:
                    self.draw_progress_for_current_file(af, spinner_char=sc)
            self.print()
            if p.total_reported_progress > 0:
                self.render_progress(
                    'Total', spinner_char=sc,
                    bytes_so_far=p.total_reported_progress, total_bytes=p.total_bytes_to_transfer,
                    secs_so_far=now - p.started_at, is_complete=is_complete,
                    bytes_per_sec=safe_divide(p.transfered_stats_amt, p.transfered_stats_interval)
                )
            else:
                self.print('File data transfer has not yet started', end='')
            self.print()
        self.schedule_progress_update(self.spinner.interval)
        self.progress_drawn = True

    def on_resize(self, screen_size: ScreenSize) -> None:
        super().on_resize(screen_size)
        if self.progress_drawn:
            self.refresh_progress()

    def refresh_progress(self) -> None:
        if not self.transmit_started:
            return
        self.erase_progress()
        self.draw_progress()

    def draw_progress_for_current_file(self, af: File, spinner_char: str = ' ', is_complete: bool = False) -> None:
        p = self.manager.progress
        now = monotonic()
        self.render_progress(
            af.display_name, spinner_char=spinner_char, is_complete=is_complete,
            bytes_so_far=af.reported_progress, total_bytes=af.bytes_to_transmit,
            secs_so_far=(af.done_at or now) - af.transmit_started_at,
            bytes_per_sec=safe_divide(p.transfered_stats_amt, p.transfered_stats_interval)
        )


def send_main(cli_opts: TransferCLIOptions, args: List[str]) -> None:
    print('Scanning files…')
    files = files_for_send(cli_opts, args)
    print(f'Found {len(files)} files and directories, requesting transfer permission…')
    loop = Loop()
    handler = Send(cli_opts, files)
    loop.loop(handler)
    p = handler.manager.progress
    if handler.manager.has_rsync and p.total_transferred + p.signature_bytes:
        tsf = 0
        for f in files:
            if f.ttype is TransmissionType.rsync:
                tsf += f.file_size
        if tsf:
            print_rsync_stats(tsf, p.total_transferred, p.signature_bytes)
    if handler.failed_files:
        print(f'Transfer of {len(handler.failed_files)} out of {len(handler.manager.files)} files failed')
        for ff in handler.failed_files:
            print(styled(ff.display_name, fg='red'))
            print(' ', ff.err_msg)

    raise SystemExit(loop.return_code)
