"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/* eslint-disable prefer-const */
const Fs = __importStar(require("fs"));
const Path = __importStar(require("path"));
const bluebird_1 = __importDefault(require("bluebird"));
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const parser_1 = __importDefault(require("./parser"));
const protocol_1 = __importDefault(require("./protocol"));
const stats_1 = __importDefault(require("./sync/stats"));
const entry_1 = __importDefault(require("./sync/entry"));
const pushtransfer_1 = __importDefault(require("./sync/pushtransfer"));
const pulltransfer_1 = __importDefault(require("./sync/pulltransfer"));
const TEMP_PATH = '/data/local/tmp';
const DEFAULT_CHMOD = 0o644;
const DATA_MAX_LENGTH = 65536;
const debug = (0, debug_1.default)('adb:sync');
class Sync extends events_1.EventEmitter {
    static temp(path) {
        return `${TEMP_PATH}/${Path.basename(path)}`;
    }
    constructor(connection) {
        super();
        this.connection = connection;
        // this.connection = connection;
        this.parser = this.connection.parser;
    }
    stat(path, callback) {
        this._sendCommandWithArg(protocol_1.default.STAT, path);
        return this.parser
            .readAscii(4)
            .then((reply) => {
            switch (reply) {
                case protocol_1.default.STAT:
                    return this.parser.readBytes(12).then((stat) => {
                        const mode = stat.readUInt32LE(0);
                        const size = stat.readUInt32LE(4);
                        const mtime = stat.readUInt32LE(8);
                        if (mode === 0) {
                            return this._enoent(path);
                        }
                        else {
                            return new stats_1.default(mode, BigInt(size), mtime);
                        }
                    });
                case protocol_1.default.FAIL:
                    return this._readError();
                default:
                    return this.parser.unexpected(reply, 'STAT or FAIL');
            }
        })
            .nodeify(callback);
    }
    sta2(path, callback) {
        this._sendCommandWithArg(protocol_1.default.STA2, path);
        return this.parser
            .readAscii(4)
            .then((reply) => {
            switch (reply) {
                case protocol_1.default.STA2:
                    return this.parser.readBytes(68).then((stat) => {
                        const mode = stat.readUInt32LE(20);
                        const size = stat.readBigUInt64LE(36);
                        const mtime = stat.readBigUInt64LE(52);
                        if (mode === 0) {
                            return this._enoent(path);
                        }
                        else {
                            return new stats_1.default(mode, size, Number(mtime));
                        }
                    });
                case protocol_1.default.FAIL:
                    return this._readError();
                default:
                    return this.parser.unexpected(reply, 'STA2 or FAIL');
            }
        })
            .nodeify(callback);
    }
    readdir(path, callback) {
        const files = [];
        const readNext = () => {
            return this.parser.readAscii(4).then((reply) => {
                switch (reply) {
                    case protocol_1.default.DENT:
                        return this.parser.readBytes(16).then((stat) => {
                            const mode = stat.readUInt32LE(0);
                            const size = stat.readUInt32LE(4);
                            const mtime = stat.readUInt32LE(8);
                            const namelen = stat.readUInt32LE(12);
                            return this.parser.readBytes(namelen).then(function (name) {
                                const nameString = name.toString();
                                // Skip '.' and '..' to match Node's fs.readdir().
                                if (!(nameString === '.' || nameString === '..')) {
                                    files.push(new entry_1.default(nameString, mode, size, mtime));
                                }
                                return readNext();
                            });
                        });
                    case protocol_1.default.DONE:
                        return this.parser.readBytes(16).then(function () {
                            return files;
                        });
                    case protocol_1.default.FAIL:
                        return this._readError();
                    default:
                        return this.parser.unexpected(reply, 'DENT, DONE or FAIL');
                }
            });
        };
        this._sendCommandWithArg(protocol_1.default.LIST, path);
        return readNext().nodeify(callback);
    }
    push(contents, path, mode) {
        if (typeof contents === 'string') {
            return this.pushFile(contents, path, mode);
        }
        else {
            return this.pushStream(contents, path, mode);
        }
    }
    pushFile(file, path, mode = DEFAULT_CHMOD) {
        mode || (mode = DEFAULT_CHMOD);
        return this.pushStream(Fs.createReadStream(file), path, mode);
    }
    pushStream(stream, path, mode = DEFAULT_CHMOD) {
        mode |= stats_1.default.S_IFREG;
        this._sendCommandWithArg(protocol_1.default.SEND, `${path},${mode}`);
        return this._writeData(stream, Math.floor(Date.now() / 1000));
    }
    pull(path) {
        this._sendCommandWithArg(protocol_1.default.RECV, `${path}`);
        return this._readData();
    }
    end() {
        this.connection.end();
        return this;
    }
    tempFile(path) {
        return Sync.temp(path);
    }
    _writeData(stream, timeStamp) {
        const transfer = new pushtransfer_1.default();
        const writeData = () => {
            let readableListener;
            let connErrorListener;
            let endListener;
            let errorListener;
            let resolver = bluebird_1.default.defer();
            const writer = bluebird_1.default.resolve();
            endListener = () => {
                writer.then(() => {
                    this._sendCommandWithLength(protocol_1.default.DONE, timeStamp);
                    return resolver.resolve();
                });
            };
            stream.on('end', endListener);
            const waitForDrain = () => {
                resolver = bluebird_1.default.defer();
                const drainListener = () => {
                    resolver.resolve();
                };
                this.connection.on('drain', drainListener);
                return resolver.promise.finally(() => {
                    return this.connection.removeListener('drain', drainListener);
                });
            };
            const track = () => transfer.pop();
            const writeNext = () => {
                let chunk;
                if ((chunk = stream.read(DATA_MAX_LENGTH) || stream.read())) {
                    this._sendCommandWithLength(protocol_1.default.DATA, chunk.length);
                    transfer.push(chunk.length);
                    if (this.connection.write(chunk, track)) {
                        return writeNext();
                    }
                    else {
                        return waitForDrain().then(writeNext);
                    }
                }
                else {
                    return bluebird_1.default.resolve();
                }
            };
            readableListener = () => writer.then(writeNext);
            stream.on('readable', readableListener);
            errorListener = (err) => resolver.reject(err);
            stream.on('error', errorListener);
            connErrorListener = (err) => {
                stream.destroy(err);
                this.connection.end();
                resolver.reject(err);
            };
            this.connection.on('error', connErrorListener);
            return resolver.promise.finally(() => {
                stream.removeListener('end', endListener);
                stream.removeListener('readable', readableListener);
                stream.removeListener('error', errorListener);
                this.connection.removeListener('error', connErrorListener);
                return writer.cancel();
            });
        };
        const readReply = () => {
            return this.parser.readAscii(4).then((reply) => {
                switch (reply) {
                    case protocol_1.default.OKAY:
                        return this.parser.readBytes(4).then(function () {
                            return true;
                        });
                    case protocol_1.default.FAIL:
                        return this._readError();
                    default:
                        return this.parser.unexpected(reply, 'OKAY or FAIL');
                }
            });
        };
        // While I can't think of a case that would break this double-Promise
        // writer-reader arrangement right now, it's not immediately obvious
        // that the code is correct and it may or may not have some failing
        // edge cases. Refactor pending.
        const writer = writeData()
            // .cancellable()
            .catch(bluebird_1.default.CancellationError, () => {
            return this.connection.end();
        })
            .catch(function (err) {
            transfer.emit('error', err);
            return reader.cancel();
        });
        const reader = readReply()
            .catch(bluebird_1.default.CancellationError, () => true)
            .catch((err) => {
            transfer.emit('error', err);
            return writer.cancel();
        })
            .finally(() => {
            return transfer.end();
        });
        transfer.on('cancel', () => {
            writer.cancel();
            reader.cancel();
        });
        return transfer;
    }
    _readData() {
        const transfer = new pulltransfer_1.default();
        const readEnd = (afterEnd) => {
            transfer.removeListener('cancel', cancelListener);
            let retEnd = transfer.end();
            if (afterEnd) {
                return afterEnd();
            }
            else {
                return retEnd;
            }
        };
        const readNext = () => {
            return this.parser.readAscii(4).then((reply) => {
                switch (reply) {
                    case protocol_1.default.DATA:
                        return this.parser.readBytes(4).then((lengthData) => {
                            const length = lengthData.readUInt32LE(0);
                            return this.parser.readByteFlow(length, transfer).then(() => {
                                readNext();
                            });
                        });
                    case protocol_1.default.DONE:
                        return this.parser.readBytes(4).then(function () {
                            return readEnd();
                        });
                    case protocol_1.default.FAIL:
                        return readEnd(() => {
                            return this._readError();
                        });
                    default:
                        return readEnd(() => {
                            return this.parser.unexpected(reply, 'DATA, DONE or FAIL');
                        });
                }
            });
        };
        const reader = readNext()
            .catch(bluebird_1.default.CancellationError, () => this.connection.end())
            .catch((err) => transfer.emit('error', err));
        const cancelListener = () => reader.cancel();
        transfer.on('cancel', cancelListener);
        return transfer;
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    _readError() {
        return this.parser
            .readBytes(4)
            .then((length) => {
            return this.parser.readBytes(length.readUInt32LE(0)).then((buf) => {
                return bluebird_1.default.reject(new parser_1.default.FailError(buf.toString()));
            });
        })
            .finally(() => {
            return this.parser.end();
        });
    }
    _sendCommandWithLength(cmd, length) {
        if (cmd !== protocol_1.default.DATA) {
            debug(cmd);
        }
        const payload = Buffer.alloc(cmd.length + 4);
        payload.write(cmd, 0, cmd.length);
        payload.writeUInt32LE(length, cmd.length);
        return this.connection.write(payload);
    }
    _sendCommandWithArg(cmd, arg) {
        debug(`${cmd} ${arg}`);
        const arglen = Buffer.byteLength(arg, 'utf-8');
        const payload = Buffer.alloc(cmd.length + 4 + arglen);
        let pos = 0;
        payload.write(cmd, pos, cmd.length);
        pos += cmd.length;
        payload.writeUInt32LE(arglen, pos);
        pos += 4;
        payload.write(arg, pos);
        return this.connection.write(payload);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    _enoent(path) {
        const err = new Error(`ENOENT, no such file or directory '${path}'`);
        err.errno = 34;
        err.code = 'ENOENT';
        err.path = path;
        return bluebird_1.default.reject(err);
    }
}
exports.default = Sync;
//# sourceMappingURL=sync.js.map