package dnd

import (
	"bytes"
	"container/list"
	"context"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"

	"github.com/emmansun/base64"

	"github.com/kovidgoyal/go-parallel"
	"github.com/kovidgoyal/kitty/tools/utils"
	"github.com/kovidgoyal/kitty/tools/utils/streaming_base64"
)

var _ = fmt.Print

const copy_on_drop = 1
const move_on_drop = 2

type drop_dest struct {
	human_name, path string
	dest             io.WriteCloser
	mime_type        string
	completed        bool
	close_on_finish  bool
	b64_decoder      streaming_base64.StreamingBase64Decoder
}

func open_file_for_writing(path string) (*os.File, error) {
	f, err := os.Create(path)
	if errors.Is(err, os.ErrNotExist) {
		dir := filepath.Dir(path)
		if err := os.MkdirAll(dir, 0755); err != nil {
			return nil, err
		}
		return os.Create(path)
	}
	return f, err
}

func (d *drop_dest) write(chunk []byte) (err error) {
	if d.dest == nil {
		if d.path == "" {
			d.dest = &bufferWriteCloser{&bytes.Buffer{}}
		} else {
			d.dest, err = open_file_for_writing(d.path)
			d.close_on_finish = true
			if err != nil {
				return
			}
		}
	}
	_, err = d.dest.Write(chunk)
	return
}

func (d *drop_dest) finish() error {
	defer func() {
		d.completed = true
		if d.dest != nil && d.close_on_finish {
			d.dest.Close()
			d.dest = nil
		}
	}()
	if chunk, err := d.b64_decoder.Finish(); err != nil {
		return err
	} else if len(chunk) > 0 {
		return d.write(chunk)
	}
	return nil
}

func (d *drop_dest) add_data(x []byte, output_buf []byte, has_more bool) error {
	d.completed = false
	for chunk, err := range d.b64_decoder.Decode(x, output_buf) {
		if err == nil {
			err = d.write(chunk)
		}
		if err != nil {
			return err
		}
	}
	if !has_more {
		if chunk, err := d.b64_decoder.Finish(); err != nil {
			return err
		} else if len(chunk) > 0 {
			return d.write(chunk)
		}
	}
	return nil
}

type remote_dir_entry struct {
	base_dir              *dir_handle
	name                  string
	item_type             int
	children              []*remote_dir_entry
	num_children_finished int

	dest        io.WriteCloser
	b64_decoder streaming_base64.StreamingBase64Decoder
}

const case_conflict_template = "case-conflict-%d-%s"

func uniqify_child_names(names []string, is_case_sensitive_filesystem bool) []string {
	if !is_case_sensitive_filesystem {
		seen := utils.NewSet[string](len(names))
		for i, x := range names {
			name := x
			key := strings.ToLower(name)
			for q := 0; seen.Has(key); q++ {
				name = fmt.Sprintf(case_conflict_template, q+1, x)
				key = strings.ToLower(name)
			}
			seen.Add(key)
			names[i] = name
		}
	}
	return names
}

func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (err error) {
	var src_file *os.File
	defer func() {
		if src_file != nil {
			src_file.Close()
		}
	}()
	for _, path := range uri_list {
		if path == "" {
			continue
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}
		if src_file != nil {
			src_file.Close()
		}
		st, err := os.Lstat(path)
		if err != nil {
			return err
		}
		if st.IsDir() {
			if src_file, err = os.Open(path); err != nil {
				return err
			}
			d, err := utils.CreateDirAt(dest_dir, filepath.Base(path), st.Mode().Perm())
			if err != nil {
				return err
			}
			err = utils.CopyFolderContents(ctx, src_file, d, utils.CopyFolderOptions{
				Filter_files: func(parent *os.File, child os.FileInfo) bool {
					return child.IsDir() || child.Mode().IsRegular() || child.Mode()&fs.ModeSymlink != 0
				},
			})
			d.Close()
			if err != nil {
				return err
			}
		} else if st.Mode().IsRegular() {
			// First try a hard link
			dest := filepath.Join(dest_dir.Name(), filepath.Base(path))
			if err = os.Link(path, dest); err == nil {
				continue
			}
			if src_file, err = os.Open(path); err != nil {
				return err
			}
			d, err := utils.CreateAt(dest_dir, filepath.Base(dest), st.Mode().Perm())
			if err != nil {
				return err
			}
			err = utils.CopyFileAndClose(ctx, src_file, d)
			src_file = nil // already closed
			if err != nil {
				return err
			}
		} else if st.Mode()&fs.ModeSymlink != 0 {
			target, err := os.Readlink(path)
			if err != nil {
				return err
			}
			dest := filepath.Join(dest_dir.Name(), filepath.Base(path))
			if err := os.Symlink(target, dest); err != nil {
				return err
			}
		}
	}
	return
}

func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completion chan error, uri_list []string, wakeup func()) {
	var err error
	defer func() {
		if r := recover(); r != nil {
			err = parallel.Format_stacktrace_on_panic(r, 1)
		}
		completion <- err
		wakeup()
	}()
	err = do_local_copy(ctx, dest_dir, uri_list)
}

type path_stack struct {
	s list.List
}

func (p *path_stack) push(x string) { p.s.PushBack(x) }
func (p *path_stack) pop() string   { return p.s.Remove(p.s.Front()).(string) }
func (p *path_stack) empty() bool   { return p.s.Front() == nil }

// Return a list of relative paths for all entries in the tree rooted at
// src_dir that also exist in the tree rooted at dest_dir, except for
// directories that exist in both places.
func find_overwrites(src_dir *os.File, dest_dir *os.File) (ans []string, err error) {
	stack := path_stack{}
	stack.push(".")
	for !stack.empty() {
		relpath := stack.pop()
		if err = func() (err error) {
			sd, err := utils.OpenDirAt(src_dir, relpath)
			if err != nil {
				return err
			}
			defer sd.Close()
			dd, err := utils.OpenDirAt(dest_dir, relpath)
			if err != nil {
				return err
			}
			defer dd.Close()
			dest_children, err := dd.ReadDir(0)
			src_children, err := sd.ReadDir(0)
			if err != nil {
				return err
			}
			dest_map := make(map[string]os.DirEntry)
			for _, x := range dest_children {
				dest_map[x.Name()] = x
			}
			for _, x := range src_children {
				if d, found := dest_map[x.Name()]; found {
					crelpath := utils.IfElse(relpath == ".", x.Name(), filepath.Join(relpath, x.Name()))
					if !d.IsDir() || !x.IsDir() {
						ans = append(ans, crelpath)
					} else {
						stack.push(crelpath)
					}
				} else {
					continue
				}
			}
			return
		}(); err != nil {
			return nil, err
		}
	}
	return
}

// Rename the contents of src_dir into dest_dir, handling the case of
// directories already existing in dest_dir transparently.
func rename_contents(src_dir *os.File, dest_dir *os.File) (err error) {
	stack := path_stack{}
	stack.push(".")
	for !stack.empty() {
		relpath := stack.pop()
		if err = func() error {
			sd, err := utils.OpenDirAt(src_dir, relpath)
			if err != nil {
				return err
			}
			defer sd.Close()
			dd, err := utils.OpenDirAt(dest_dir, relpath)
			if err != nil {
				return err
			}
			defer dd.Close()
			for {
				src_children, err := sd.ReadDir(64)
				if err != nil {
					if errors.Is(err, io.EOF) {
						break
					}
					return err
				}
				for _, child := range src_children {
					crelpath := utils.IfElse(relpath == ".", child.Name(), filepath.Join(relpath, child.Name()))
					rerr := utils.RenameAt(sd, child.Name(), dd, child.Name())
					if rerr != nil {
						if child.IsDir() {
							stack.push(crelpath)
						} else {
							return rerr
						}
					}
				}
			}
			return nil
		}(); err != nil {
			return err
		}
	}
	return
}

func (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, is_case_sensitive_filesystem bool) error {
	if len(data) > 0 {
		for chunk, derr := range d.b64_decoder.Decode(data, output_buf) {
			if derr != nil {
				return derr
			}
			if _, derr = d.dest.Write(chunk); derr != nil {
				return derr
			}
		}
	} else if !has_more {
		if chunk, derr := d.b64_decoder.Finish(); derr != nil {
			return derr
		} else {
			if _, derr = d.dest.Write(chunk); derr != nil {
				return derr
			}
		}
		defer func() {
			d.dest.Close()
			d.dest = nil
			d.base_dir = d.base_dir.unref()
		}()
		if dest, ok := d.dest.(*bufferWriteCloser); ok {
			if d.item_type == 1 {
				if derr := utils.SymlinkAt(d.base_dir.handle, d.name, dest.String()); derr != nil {
					return derr
				}
			} else { // directory
				if f, derr := utils.CreateDirAt(d.base_dir.handle, d.name, 0o755); derr != nil {
					return derr
				} else {
					handle := new_dir_handle(f)
					defer handle.unref()
					s := utils.NewSeparatorScanner("", "\x00")
					for _, name := range uniqify_child_names(s.Split(dest.String()), is_case_sensitive_filesystem) {
						d.children = append(d.children, &remote_dir_entry{name: name, base_dir: handle.newref()})
					}
				}
			}
		}
	}
	return nil
}

type parsed_uri_kind int

const (
	parsed_uri_file parsed_uri_kind = iota
	parsed_uri_data
)

type parsed_uri struct {
	kind parsed_uri_kind
	path string // for file URIs: the local filesystem path (empty if not a valid file URI)
	mime string // for data URIs: the MIME type
	data []byte // for data URIs: the decoded payload
}

// ext_for_mime returns a file extension (with leading dot) for a MIME type.
func ext_for_mime(mime string) string {
	for _, x := range utils.GuessFileExtensions(mime) {
		return x
	}
	return ""
}

// parse_data_uri decodes a data: URI and returns the MIME type and raw data.
func parse_data_uri(uri string) (mime string, data []byte, err error) {
	rest := strings.TrimPrefix(uri, "data:")
	comma_idx := strings.Index(rest, ",")
	if comma_idx < 0 {
		err = fmt.Errorf("invalid data URI: no comma found")
		return
	}
	header := rest[:comma_idx]
	payload := rest[comma_idx+1:]

	is_base64 := strings.HasSuffix(header, ";base64")
	if is_base64 {
		header = header[:len(header)-7]
	}

	mime = strings.TrimSpace(header)
	if mime == "" {
		mime = "text/plain"
	}
	// Strip parameters (e.g. ;charset=UTF-8) so the MIME type is clean.
	mime, _, _ = strings.Cut(mime, ";")

	if is_base64 {
		payload = strings.NewReplacer("\r", "", "\n", "", " ", "").Replace(payload)
		data, err = base64.StdEncoding.DecodeString(payload)
		if err != nil {
			data, err = base64.RawURLEncoding.DecodeString(payload)
		}
	} else {
		var unescaped string
		unescaped, err = url.PathUnescape(payload)
		if err != nil {
			return
		}
		data = []byte(unescaped)
	}
	return
}

func parse_uri_list(src string) (ans []parsed_uri, err error) {
	for _, line := range utils.NewSeparatorScanner("", "\r\n").Split(src) {
		line = strings.TrimSpace(line)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		if strings.HasPrefix(line, "file://") {
			p, err := url.Parse(line)
			if err != nil {
				return nil, err
			}
			ans = append(ans, parsed_uri{kind: parsed_uri_file, path: filepath.Clean(p.Path)})
		} else if strings.HasPrefix(line, "data:") {
			m, d, derr := parse_data_uri(line)
			if derr != nil {
				// Invalid data URI: treat as an unrecognised entry (empty path) so the caller
				// skips it without failing, matching the behaviour for unknown URI schemes.
				ans = append(ans, parsed_uri{kind: parsed_uri_file})
			} else {
				ans = append(ans, parsed_uri{kind: parsed_uri_data, mime: m, data: d})
			}
		} else {
			ans = append(ans, parsed_uri{kind: parsed_uri_file}) // unknown URI scheme: empty entry
		}
	}
	return
}

type drop_status struct {
	offered_mimes      []string
	accepted_mimes     []string
	uri_list           []parsed_uri
	cell_x, cell_y     int
	action             int
	in_window          bool
	reading_data       bool
	is_remote_client   bool
	source_allowed_ops int

	dropping_to          *dir_handle
	root_remote_dir      *remote_dir_entry
	open_remote_dir      *remote_dir_entry
	current_remote_entry *remote_dir_entry // used for m=1 only
	pending_remote_dirs  []*remote_dir_entry
	data_requests        struct {
		pending         []DC
		in_flight_count int
	}
	local_copy struct {
		ctx        context.Context
		cancel_ctx context.CancelFunc
		completion chan error
	}
}

func (d *drop_status) reset() {
	if d.local_copy.ctx != nil {
		d.local_copy.cancel_ctx()
		<-d.local_copy.completion
	}
	if d.dropping_to != nil {
		d.dropping_to = d.dropping_to.unref()
	}
	*d = drop_status{cell_x: -1, cell_y: -1}
}

func (d *drop_dest) reset() {
	if d.dest != nil && d.dest != os.Stdout {
		if d.close_on_finish {
			d.dest.Close()
			d.dest = nil
		} else {
			d.dest.(*bufferWriteCloser).Reset()
		}
	}
	d.completed = false
	d.close_on_finish = false
	d.b64_decoder = streaming_base64.StreamingBase64Decoder{}
}

func (dnd *dnd) reset_drop() {
	if dnd.drop_status.root_remote_dir != nil {
		dnd.drop_status.root_remote_dir.close_tree()
		dnd.drop_status.root_remote_dir = nil
	}
	dnd.drop_status.reset()
	for _, x := range dnd.drop_dests {
		x.reset()
	}
}

func (root *remote_dir_entry) close_tree() {
	if root.base_dir != nil {
		root.base_dir = root.base_dir.unref()
	}
	for _, child := range root.children {
		child.close_tree()
	}
}

func (dnd *dnd) end_drop(success bool) {
	if dnd.drop_status.reading_data {
		dnd.lp.QueueDnDData(DC{
			Type: 'r', Operation: utils.IfElse(success, dnd.drop_status.action, 0)}) // end drop
		if success && dnd.drop_status.action == move_on_drop && !dnd.drop_status.is_remote_client {
			for _, u := range dnd.drop_status.uri_list {
				if u.kind != parsed_uri_file || u.path == "" {
					continue
				}
				st, err := os.Lstat(u.path)
				if err != nil {
					continue
				}
				if st.IsDir() {
					os.RemoveAll(u.path)
				} else {
					os.Remove(u.path)
				}
			}
		}
	}
	dnd.reset_drop()
	if success && dnd.has_exit_on("drop-finish") {
		dnd.lp.Quit(0)
	}
}

func (dnd *dnd) count_files_in_drop() int {
	n := 0
	for _, u := range dnd.drop_status.uri_list {
		if (u.kind == parsed_uri_file && u.path != "") || u.kind == parsed_uri_data {
			n++
		}
	}
	for mime, d := range dnd.drop_dests {
		if mime != "text/uri-list" && d.completed {
			n++
		}
	}
	return n
}

func (dnd *dnd) all_drop_data_received() (err error) {
	dnd.num_dropped_files = dnd.count_files_in_drop()
	var staging_dir *os.File
	if dnd.drop_status.dropping_to != nil {
		staging_dir = dnd.drop_status.dropping_to.handle
		dnd.drop_status.dropping_to = nil
	}
	defer func() {
		if err == nil {
			if len(dnd.confirm_drop.overwrites) == 0 {
				dnd.end_drop(true)
			}
			err = dnd.render_screen()
		} else {
			dnd.end_drop(false)
		}
	}()
	if staging_dir != nil {
		if dnd.opts.ConfirmDropOverwrite {
			overwrites, err := find_overwrites(staging_dir, dnd.drop_output_dir)
			if err != nil {
				return err
			}
			if len(overwrites) > 0 {
				dnd.confirm_drop.overwrites = overwrites
				dnd.confirm_drop.staging_dir = staging_dir
				return dnd.render_screen()
			}
		}
		err := rename_contents(staging_dir, dnd.drop_output_dir)
		staging_dir.Close()
		if err != nil {
			return err
		}
		return nil
	}
	return nil
}

func (dnd *dnd) drop_on_wakeup() error {
	if dnd.drop_status.local_copy.completion == nil {
		return nil
	}
	select {
	case err := <-dnd.drop_status.local_copy.completion:
		dnd.drop_status.local_copy.ctx = nil
		dnd.drop_status.local_copy.completion = nil
		if err != nil {
			return err
		}
		return dnd.all_drop_data_received()
	default:
		return nil
	}
}

func (dnd *dnd) new_tdir() (dir_file *os.File, err error) {
	dnd.tdir_counter++
	name := strconv.Itoa(dnd.tdir_counter)
	return utils.CreateDirAt(dnd.base_tempdir, name, 0o700)
}

func (dnd *dnd) all_mime_data_dropped() (err error) {
	drop_status := &dnd.drop_status
	if len(drop_status.uri_list) == 0 {
		dnd.num_dropped_files = dnd.count_files_in_drop()
		dnd.end_drop(true)
		return dnd.render_screen()
	}
	f, err := dnd.new_tdir()
	if err != nil {
		return err
	}
	dnd.drop_status.dropping_to = new_dir_handle(f)

	// Write data URIs directly into the staging directory (applies to both local and remote).
	data_uri_counter := 0
	for _, u := range drop_status.uri_list {
		if u.kind == parsed_uri_data {
			data_uri_counter++
			fname := fmt.Sprintf("data-uri-%d%s", data_uri_counter, ext_for_mime(u.mime))
			if werr := os.WriteFile(filepath.Join(f.Name(), fname), u.data, 0o644); werr != nil {
				return werr
			}
		}
	}

	if drop_status.is_remote_client {
		drop_status.root_remote_dir = &remote_dir_entry{}
		seen := utils.NewSet[string](len(drop_status.uri_list))
		idx := slices.Index(drop_status.offered_mimes, "text/uri-list")
		for i, u := range drop_status.uri_list {
			var c *remote_dir_entry
			if u.kind == parsed_uri_data || u.path == "" {
				c = &remote_dir_entry{}
				drop_status.root_remote_dir.num_children_finished++
			} else {
				name := filepath.Base(u.path)
				if !dnd.is_case_sensitive_filesystem {
					key := strings.ToLower(name)
					for q := 0; seen.Has(key); q++ {
						name = fmt.Sprintf(case_conflict_template, q+1, filepath.Base(u.path))
						key = strings.ToLower(name)
					}
					seen.Add(key)
				}
				c = &remote_dir_entry{base_dir: dnd.drop_status.dropping_to.newref(), name: name}
				dnd.queue_data_request(DC{Type: 'r', X: idx + 1, Y: i + 1})
			}
			drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c)
		}
		// If all children were pre-finished (e.g., all data URIs), complete the drop immediately.
		if drop_status.root_remote_dir.num_children_finished >= len(drop_status.root_remote_dir.children) {
			return dnd.all_drop_data_received()
		}
		drop_status.open_remote_dir = drop_status.root_remote_dir
	} else {
		// Build a file-path slice for the goroutine; data URI entries are already written above.
		file_paths := make([]string, len(drop_status.uri_list))
		for i, u := range drop_status.uri_list {
			if u.kind == parsed_uri_file {
				file_paths[i] = u.path
			}
		}
		drop_status.local_copy.ctx, drop_status.local_copy.cancel_ctx = context.WithCancel(context.Background())
		drop_status.local_copy.completion = make(chan error, 1)
		go do_local_copy_in_goroutine(drop_status.local_copy.ctx, f, drop_status.local_copy.completion, file_paths, func() { dnd.lp.WakeupMainThread() })
	}
	return
}

func (dnd *dnd) request_mime_data() {
	accepted := utils.NewSetWithItems(dnd.drop_status.accepted_mimes...)
	seen := utils.NewSet[string](len(dnd.drop_status.accepted_mimes))
	for idx, m := range dnd.drop_status.offered_mimes {
		if accepted.Has(m) && !seen.Has(m) {
			dnd.queue_data_request(DC{Type: 'r', X: idx + 1})
			seen.Add(m)
		}
	}
}

var offered_mimes_buf strings.Builder

func (dnd *dnd) on_drop_move(cell_x, cell_y int, has_more bool, offered_mimes string, is_drop bool, source_allowed_ops int) (needs_rerender bool) {
	prev_status := dnd.drop_status
	dnd.drop_status.cell_x, dnd.drop_status.cell_y = cell_x, cell_y
	if offered_mimes != "" {
		offered_mimes_buf.WriteString(offered_mimes)
		if has_more {
			return
		}
		offered_mimes := offered_mimes_buf.String()
		dnd.drop_status.offered_mimes = strings.Fields(offered_mimes)
		dnd.drop_status.accepted_mimes = make([]string, 0, len(dnd.drop_status.offered_mimes))
		seen := utils.NewSet[string](len(dnd.drop_status.offered_mimes))
		for _, x := range dnd.drop_status.offered_mimes {
			if _, found := dnd.drop_dests[x]; found && !seen.Has(x) {
				dnd.drop_status.accepted_mimes = append(dnd.drop_status.accepted_mimes, x)
				seen.Add(x)
			}
		}
	}
	offered_mimes_buf.Reset()
	if source_allowed_ops != 0 {
		dnd.drop_status.source_allowed_ops = source_allowed_ops
	}
	if dnd.copy_button_region.has(cell_x, cell_y) {
		dnd.drop_status.action = copy_on_drop
	} else if dnd.move_button_region.has(cell_x, cell_y) {
		dnd.drop_status.action = move_on_drop
	} else {
		switch dnd.opts.DropAnywhere {
		case "disallowed":
			dnd.drop_status.action = 0
			dnd.drop_status.accepted_mimes = nil
		case "copy":
			dnd.drop_status.action = copy_on_drop
		case "move":
			dnd.drop_status.action = move_on_drop
		}
	}
	// Restrict to operations allowed by the drag source.
	if allowed := dnd.drop_status.source_allowed_ops; allowed != 0 && dnd.drop_status.action != 0 {
		if allowed&dnd.drop_status.action == 0 {
			dnd.drop_status.action = 0
			dnd.drop_status.accepted_mimes = nil
		}
	}
	dnd.drop_status.in_window = cell_x > -1 && cell_y > -1
	if !dnd.drop_status.in_window || dnd.drag_status.active { // disallow self drag and drop
		dnd.reset_drop()
	}
	mimes_changed := !slices.Equal(prev_status.accepted_mimes, dnd.drop_status.accepted_mimes)
	needs_rerender = prev_status.action != dnd.drop_status.action || mimes_changed
	if needs_rerender && !is_drop {
		c := DC{Type: 'm', Operation: dnd.drop_status.action}
		if dnd.drop_status.action != 0 && len(dnd.drop_status.accepted_mimes) > 0 {
			c.Payload = utils.UnsafeStringToBytes(strings.Join(dnd.drop_status.accepted_mimes, " "))
		}
		dnd.lp.QueueDnDData(c)
	}
	needs_rerender = needs_rerender || dnd.drop_status.in_window != prev_status.in_window
	if is_drop {
		needs_rerender = true
		if dnd.drop_status.action == 0 || len(dnd.drop_status.accepted_mimes) == 0 || dnd.drag_status.active {
			dnd.end_drop(false)
			return
		}
		dnd.drop_status.reading_data = true
		dnd.request_mime_data()
	}
	return
}

var drop_buf []byte

const max_inflight_data_rquests = 64

func (dnd *dnd) queue_data_request(cmd DC) {
	if dnd.drop_status.data_requests.in_flight_count < max_inflight_data_rquests {
		dnd.lp.QueueDnDData(cmd)
		dnd.drop_status.data_requests.in_flight_count++
	} else {
		dnd.drop_status.data_requests.pending = append(dnd.drop_status.data_requests.pending, cmd)
	}
}

func (dnd *dnd) data_request_completed() {
	dnd.drop_status.data_requests.in_flight_count--
	if len(dnd.drop_status.data_requests.pending) > 0 && dnd.drop_status.data_requests.in_flight_count < max_inflight_data_rquests {
		dnd.lp.QueueDnDData(dnd.drop_status.data_requests.pending[0])
		dnd.drop_status.data_requests.in_flight_count++
		dnd.drop_status.data_requests.pending = dnd.drop_status.data_requests.pending[1:]
	}
}

func (dnd *dnd) on_remote_drop_data(cmd DC) (err error) {
	drop_status := &dnd.drop_status
	if drop_status.open_remote_dir == nil {
		return fmt.Errorf("got a remote data response form the terminal without an open remote dir")
	}
	if cmd.X == 0 && cmd.Y == 0 && cmd.Yp == 0 {
		if drop_status.current_remote_entry == nil {
			return fmt.Errorf("got a remote data response form the terminal without a current remote entry")
		}
	} else {
		num := utils.IfElse(cmd.Yp != 0 && cmd.Yp != 1, cmd.X, cmd.Y) - 1
		if num < 0 || num >= len(drop_status.open_remote_dir.children) {
			return fmt.Errorf("got a remote data response from the terminal for an entry that does not exist")
		}
		drop_status.current_remote_entry = drop_status.open_remote_dir.children[num]
	}
	e := drop_status.current_remote_entry
	if e.dest == nil {
		e.item_type = cmd.Xp
		switch cmd.Xp {
		case 0:
			f, err := utils.CreateExclusiveAt(e.base_dir.handle, e.name, 0o666)
			if err != nil {
				return err
			}
			e.dest = f
		default:
			e.dest = &bufferWriteCloser{&bytes.Buffer{}}
		}
	}
	if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz {
		drop_buf = make([]byte, sz)
	}
	if err = e.add_remote_data(cmd.Payload, drop_buf, cmd.Has_more, dnd.is_case_sensitive_filesystem); err != nil {
		return err
	}
	if e.dest == nil { // received all data for this entry
		drop_status.current_remote_entry = nil
		parent := drop_status.open_remote_dir
		parent.num_children_finished++
		dnd.data_request_completed()
		if parent.num_children_finished >= len(parent.children) { // parent is finished
			drop_status.open_remote_dir = nil
			if parent.base_dir != nil {
				parent.base_dir = parent.base_dir.unref()
			}
			if parent.item_type != 0 {
				dnd.lp.QueueDnDData(DC{Type: 'r', Yp: parent.item_type}) // close directory in terminal
			}
			for _, c := range parent.children {
				is_pending := false
				if c.item_type != 0 && c.item_type != 1 {
					if len(c.children) > 0 {
						dnd.drop_status.pending_remote_dirs = append(dnd.drop_status.pending_remote_dirs, c)
						is_pending = true
					}
				}
				if !is_pending {
					if c.base_dir != nil {
						c.base_dir = c.base_dir.unref()
					}
				}
			}
			if len(drop_status.pending_remote_dirs) > 0 {
				drop_status.open_remote_dir = drop_status.pending_remote_dirs[0]
				drop_status.pending_remote_dirs = drop_status.pending_remote_dirs[1:]
				for i := range drop_status.open_remote_dir.children {
					dnd.queue_data_request(DC{Type: 'r', X: i + 1, Yp: drop_status.open_remote_dir.item_type})
				}
			} else {
				return dnd.all_drop_data_received()
			}
		}
	}
	return nil
}

func (dnd *dnd) on_drop_data(cmd DC) error {
	drop_status := &dnd.drop_status
	if drop_status.root_remote_dir != nil {
		return dnd.on_remote_drop_data(cmd)
	}
	idx := cmd.X - 1
	if idx < 0 || idx > len(drop_status.offered_mimes) {
		return fmt.Errorf("terminal sent drop data for an index (%d) outside the list of accepted MIMEs (length: %d)", idx, len(drop_status.offered_mimes))
	}
	mime := drop_status.offered_mimes[idx]
	dest := dnd.drop_dests[mime]
	if cmd.Xp == 1 && mime == "text/uri-list" {
		drop_status.is_remote_client = true
	}
	if !cmd.Has_more && len(cmd.Payload) == 0 {
		if err := dest.finish(); err != nil {
			return err
		}
		if mime == "text/uri-list" {
			b := dest.dest.(*bufferWriteCloser)
			var err error
			if drop_status.uri_list, err = parse_uri_list(b.String()); err != nil {
				return err
			}
		}
		pending := false
		expecting := utils.NewSetWithItems(drop_status.accepted_mimes...)
		for _, d := range dnd.drop_dests {
			if !d.completed && expecting.Has(d.mime_type) {
				pending = true
				break
			}
		}
		if !pending {
			return dnd.all_mime_data_dropped()
		}
		return nil
	}
	if sz := max(4096, len(cmd.Payload)+4); len(drop_buf) < sz {
		drop_buf = make([]byte, sz)
	}
	return dest.add_data(cmd.Payload, drop_buf, cmd.Has_more)
}

func (dnd *dnd) drop_confirm(accepted bool) error {
	staging_dir := dnd.confirm_drop.staging_dir
	dnd.confirm_drop.overwrites = nil
	dnd.confirm_drop.staging_dir = nil
	defer staging_dir.Close()
	if !accepted {
		dnd.num_dropped_files = 0
	}
	if accepted {
		if err := rename_contents(staging_dir, dnd.drop_output_dir); err != nil {
			dnd.end_drop(false)
			return err
		}
		dnd.end_drop(true)
	} else {
		dnd.end_drop(false)
	}
	return dnd.render_screen()
}
