namespace esp32 {
    // pylint: disable=bad-whitespace
    const _SET_NET_CMD = 0x10
    const _SET_PASSPHRASE_CMD = 0x11
    const _SET_DEBUG_CMD = 0x1A
    const _GET_TEMP_CMD = 0x1B
    const _GET_CONN_STATUS_CMD = 0x20
    const _GET_IPADDR_CMD = 0x21
    const _GET_MACADDR_CMD = 0x22
    const _GET_CURR_SSID_CMD = 0x23
    const _GET_CURR_RSSI_CMD = 0x25
    const _GET_CURR_ENCT_CMD = 0x26
    const _SCAN_NETWORKS = 0x27
    const _GET_SOCKET_CMD = 0x3F
    const _GET_STATE_TCP_CMD = 0x29
    const _DATA_SENT_TCP_CMD = 0x2A
    const _AVAIL_DATA_TCP_CMD = 0x2B
    const _GET_DATA_TCP_CMD = 0x2C
    const _START_CLIENT_TCP_CMD = 0x2D
    const _STOP_CLIENT_TCP_CMD = 0x2E
    const _GET_CLIENT_STATE_TCP_CMD = 0x2F
    const _DISCONNECT_CMD = 0x30
    const _GET_IDX_RSSI_CMD = 0x32
    const _GET_IDX_ENCT_CMD = 0x33
    const _REQ_HOST_BY_NAME_CMD = 0x34
    const _GET_HOST_BY_NAME_CMD = 0x35
    const _START_SCAN_NETWORKS = 0x36
    const _GET_FW_VERSION_CMD = 0x37
    const _PING_CMD = 0x3E
    const _SEND_DATA_TCP_CMD = 0x44
    const _GET_DATABUF_TCP_CMD = 0x45
    const _SET_ENT_IDENT_CMD = 0x4A
    const _SET_ENT_UNAME_CMD = 0x4B
    const _SET_ENT_PASSWD_CMD = 0x4C
    const _SET_ENT_ENABLE_CMD = 0x4F
    const _SET_PIN_MODE_CMD = 0x50
    const _SET_DIGITAL_WRITE_CMD = 0x51
    const _SET_ANALOG_WRITE_CMD = 0x52
    const _START_CMD = 0xE0
    const _END_CMD = 0xEE
    const _ERR_CMD = 0xEF
    const _REPLY_FLAG = 1 << 7
    const _CMD_FLAG = 0
    export const SOCKET_CLOSED = 0
    export const SOCKET_LISTEN = 1
    export const SOCKET_SYN_SENT = 2
    export const SOCKET_SYN_RCVD = 3
    export const SOCKET_ESTABLISHED = 4
    export const SOCKET_FIN_WAIT_1 = 5
    export const SOCKET_FIN_WAIT_2 = 6
    export const SOCKET_CLOSE_WAIT = 7
    export const SOCKET_CLOSING = 8
    export const SOCKET_LAST_ACK = 9
    export const SOCKET_TIME_WAIT = 10
    export const WL_NO_SHIELD = 0xFF
    export const WL_NO_MODULE = 0xFF
    export const WL_IDLE_STATUS = 0
    export const WL_NO_SSID_AVAIL = 1
    export const WL_SCAN_COMPLETED = 2
    export const WL_CONNECTED = 3
    export const WL_CONNECT_FAILED = 4
    export const WL_CONNECTION_LOST = 5
    export const WL_DISCONNECTED = 6
    export const WL_AP_LISTENING = 7
    export const WL_AP_CONNECTED = 8
    export const WL_AP_FAILED = 9


    function buffer1(ch: number) {
        const b = control.createBuffer(1)
        b[0] = ch
        return b
    }

    export class NinaController extends net.Controller {
        private _socknum_ll: Buffer[];
        private _locked: boolean;

        public wasConnected: boolean;

        constructor(
            private _spi: SPI,
            private _cs: DigitalInOutPin,
            private _busy: DigitalInOutPin,
            private _reset: DigitalInOutPin,
            private _gpio0: DigitalInOutPin = null
        ) {
            super();
            // if nothing connected, pretend the device is ready -
            // we'll check for timeout waiting for response instead
            this._busy.setPull(PinPullMode.PullDown);
            this._busy.digitalRead();
            this._socknum_ll = [buffer1(0)]
            this._spi.setFrequency(8000000);
            this.reset();
            this._locked = false;
        }

        /** 
         * Hard reset the ESP32 using the reset pin 
        */
        public reset(): void {
            if (this._gpio0)
                this._gpio0.digitalWrite(true);
            this._cs.digitalWrite(true)
            this._reset.digitalWrite(false)
            // reset
            pause(10)
            this._reset.digitalWrite(true)
            // wait for it to boot up
            pause(750)
            if (this._gpio0)
                this._gpio0.digitalRead();
            // make sure SPI gets initialized while the CS is up
            this.spiTransfer(control.createBuffer(1), null)
            net.log('reseted esp32')
        }

        private readByte(): number {
            const r = buffer1(0)
            this.spiTransfer(null, r)
            return r[0]
        }

        private checkData(desired: number, msg?: string): boolean {
            const r = this.readByte()
            if (r != desired)
                net.fail(`Expected ${desired} but got ${r}; ` + (msg || ""))
            return false;
        }

        /** Read a byte with a time-out, and if we get it, check that its what we expect */
        private waitSPIChar(desired: number): boolean {
            let times = control.millis()
            while (control.millis() - times < 100) {
                let r = this.readByte()
                if (r == _ERR_CMD) {
                    net.log("error response to command")
                    return false
                }

                if (r == desired) {
                    return true
                }
                //net.log(`read char ${r}, expected ${desired}`)
            }
            net.log("timed out waiting for SPI char")
            return false;
        }

        /**
         * Wait until the ready pin goes low
         */
        private waitForReady() {
            net.debug(`wait for ready ${this._busy.digitalRead()}`);
            if (this._busy.digitalRead()) {
                pauseUntil(() => !this._busy.digitalRead(), 10000);
                net.debug(`busy = ${this._busy.digitalRead()}`);
                // pause(1000)
            }
            if (this._busy.digitalRead()) {
                net.log("timed out waiting for ready")
                return false
            }

            return true
        }

        private _sendCommand(cmd: number, params?: Buffer[], param_len_16?: boolean) {
            params = params || [];

            // compute buffer size
            let n = 3; // START_CMD, cmd, length
            params.forEach(param => {
                n += 1 + (param_len_16 ? 1 : 0) + param.length;
            })
            n += 1; // END_CMD
            // padding
            while (n % 4) n++;

            const packet = control.createBuffer(n);
            let k = 0;
            packet[k++] = _START_CMD;
            packet[k++] = cmd & ~_REPLY_FLAG;
            packet[k++] = params.length;

            params.forEach(param => {
                if (param_len_16)
                    packet[k++] = (param.length >> 8) & 0xFF;
                packet[k++] = param.length & 0xFF;
                packet.write(k, param);
                k += param.length;
            })
            packet[k++] = _END_CMD;
            while (k < n)
                packet[k++] = 0xff;

            net.debug(`send cmd ${packet.toHex()}`)
            if (!this.waitForReady())
                return false
            this._cs.digitalWrite(false)
            this.spiTransfer(packet, null)
            this._cs.digitalWrite(true)
            net.debug(`send done`);
            return true
        }

        private spiTransfer(tx: Buffer, rx: Buffer) {
            if (!tx) tx = control.createBuffer(rx.length)
            if (!rx) rx = control.createBuffer(tx.length)
            this._spi.transfer(tx, rx);
        }

        private _waitResponseCmd(cmd: number, num_responses?: number, param_len_16?: boolean) {
            net.debug(`wait response cmd`);
            if (!this.waitForReady())
                return null

            this._cs.digitalWrite(false)

            let responses: Buffer[] = []
            if (!this.waitSPIChar(_START_CMD)) {
                this._cs.digitalWrite(true)
                return null
            }
            this.checkData(cmd | _REPLY_FLAG)
            if (num_responses !== undefined)
                this.checkData(num_responses, cmd + "")
            else
                num_responses = this.readByte();
            for (let num = 0; num < num_responses; ++num) {
                let param_len = this.readByte()
                if (param_len_16) {
                    param_len <<= 8
                    param_len |= this.readByte()
                }
                net.debug(`\tParameter #${num} length is ${param_len}`)
                const response = control.createBuffer(param_len);
                this.spiTransfer(null, response)
                responses.push(response);
            }
            this.checkData(_END_CMD);

            this._cs.digitalWrite(true)

            net.debug(`responses ${responses.length}`);
            return responses;
        }

        private lock() {
            while (this._locked) {
                pauseUntil(() => !this._locked)
            }
            this._locked = true
        }

        private unlock() {
            if (!this._locked)
                net.fail("not locked!")
            this._locked = false;
        }

        private sendCommandGetResponse(cmd: number, params?: Buffer[],
            reply_params = 1, sent_param_len_16 = false, recv_param_len_16 = false) {

            this.lock()
            this._sendCommand(cmd, params, sent_param_len_16)
            const resp = this._waitResponseCmd(cmd, reply_params, recv_param_len_16)
            this.unlock();
            return resp
        }

        get status(): number {
            const resp = this.sendCommandGetResponse(_GET_CONN_STATUS_CMD)
            if (!resp)
                return WL_NO_SHIELD
            net.debug(`status: ${resp[0][0]}`);
            // one byte response
            return resp[0][0];
        }

        /** A string of the firmware version on the ESP32 */
        get firmwareVersion(): string {
            let resp = this.sendCommandGetResponse(_GET_FW_VERSION_CMD)
            if (!resp)
                return "not connected"
            return resp[0].toString();
        }

        /** A bytearray containing the MAC address of the ESP32 */
        get MACaddress(): Buffer {
            let resp = this.sendCommandGetResponse(_GET_MACADDR_CMD, [hex`ff`])
            if (!resp)
                return null
            // for whatever reason, the mac adderss is backwards
            const res = control.createBuffer(6)
            for (let i = 0; i < 6; ++i)
                res[i] = resp[0][5 - i]
            return res
        }

        /** Begin a scan of visible access points. Follow up with a call
    to 'get_scan_networks' for response
*/
        private startScanNetworks(): void {
            let resp = this.sendCommandGetResponse(_START_SCAN_NETWORKS)
            if (resp[0][0] != 1) {
                net.fail("failed to start AP scan")
            }

        }

        /** The results of the latest SSID scan. Returns a list of dictionaries with
    'ssid', 'rssi' and 'encryption' entries, one for each AP found
*/
        private getScanNetworks(): net.AccessPoint[] {
            let names = this.sendCommandGetResponse(_SCAN_NETWORKS, undefined, undefined)
            // print("SSID names:", names)
            // pylint: disable=invalid-name
            let APs = []
            let i = 0
            for (let name of names) {
                let a_p = new net.AccessPoint(name.toString())
                let rssi = this.sendCommandGetResponse(_GET_IDX_RSSI_CMD, [buffer1(i)])[0]
                a_p.rssi = pins.unpackBuffer("<i", rssi)[0]
                let encr = this.sendCommandGetResponse(_GET_IDX_ENCT_CMD, [buffer1(1)])[0]
                if (encr[0])
                    a_p.flags |= net.WifiAPFlags.HasPassword
                APs.push(a_p)
                i++
            }
            return APs
        }

        /** Scan for visible access points, returns a list of access point details.
     Returns a list of dictionaries with 'ssid', 'rssi' and 'encryption' entries,
     one for each AP found
    */
        protected scanNetworksCore(): net.AccessPoint[] {
            this.startScanNetworks()
            // attempts
            for (let _ = 0; _ < 10; ++_) {
                pause(2000)
                // pylint: disable=invalid-name
                let APs = this.getScanNetworks()
                if (APs) {
                    for (const ap of APs)
                        net.debug(`  ${ap.ssid} => RSSI ${ap.rssi}`)
                    return APs
                }

            }
            return null
        }

        /** Tells the ESP32 to set the access point to the given ssid */
        public wifiSetNetwork(ssid: string): void {
            const ssidbuf = control.createBufferFromUTF8(ssid);
            let resp = this.sendCommandGetResponse(_SET_NET_CMD, [ssidbuf])
            if (resp[0][0] != 1) {
                net.fail("failed to set network")
            }

        }

        /** Sets the desired access point ssid and passphrase */
        public wifiSetPassphrase(ssid: string, passphrase: string): void {
            const ssidbuf = control.createBufferFromUTF8(ssid);
            const passphrasebuf = control.createBufferFromUTF8(passphrase);
            let resp = this.sendCommandGetResponse(_SET_PASSPHRASE_CMD, [ssidbuf, passphrasebuf])
            if (resp[0][0] != 1) {
                net.fail("failed to set passphrase")
            }
        }

        /** Sets the WPA2 Enterprise anonymous identity */
        public wifiSetEntidentity(ident: string): void {
            const ssidbuf = control.createBufferFromUTF8(ident);
            let resp = this.sendCommandGetResponse(_SET_ENT_IDENT_CMD, [ssidbuf])
            if (resp[0][0] != 1) {
                net.fail("failed to set enterprise anonymous identity")
            }

        }

        /** Sets the desired WPA2 Enterprise username */
        public wifiSetEntusername(username: string): void {
            const usernamebuf = control.createBufferFromUTF8(username);
            let resp = this.sendCommandGetResponse(_SET_ENT_UNAME_CMD, [usernamebuf])
            if (resp[0][0] != 1) {
                net.fail("failed to set enterprise username")
            }

        }

        /** Sets the desired WPA2 Enterprise password */
        public wifiSetEntpassword(password: string): void {
            const passwordbuf = control.createBufferFromUTF8(password);
            let resp = this.sendCommandGetResponse(_SET_ENT_PASSWD_CMD, [passwordbuf])
            if (resp[0][0] != 1) {
                net.fail("failed to set enterprise password")
            }

        }

        /** Enables WPA2 Enterprise mode */
        public wifiSetEntenable(): void {
            let resp = this.sendCommandGetResponse(_SET_ENT_ENABLE_CMD)
            if (resp[0][0] != 1) {
                net.fail("failed to enable enterprise mode")
            }

        }

        get ssidBuffer(): Buffer {
            let resp = this.sendCommandGetResponse(_GET_CURR_SSID_CMD, [hex`ff`])
            return resp[0]
        }

        get ssid(): string {
            const b = this.ssidBuffer;
            return b ? b.toString() : "";
        }

        get rssi(): number {
            let resp = this.sendCommandGetResponse(_GET_CURR_RSSI_CMD, [hex`ff`])
            return pins.unpackBuffer("<i", resp[0])[0]
        }

        get networkData(): any {
            let resp = this.sendCommandGetResponse(_GET_IPADDR_CMD, [hex`ff`])
            return resp[0]; //?
        }

        get ipAddress(): string {
            return this.networkData["ip_addr"]
        }

        get isConnected(): boolean {
            return this.status == WL_CONNECTED
        }

        get isIdle(): boolean {
            return this.status == WL_IDLE_STATUS;
        }

        /** 
         * Connect to an access point with given name and password.
         * Will retry up to 10 times and return on success
        */
        connectAP(ssid: string, password: string): boolean {
            net.log(`connect to ${ssid}`)
            if (password) {
                this.wifiSetPassphrase(ssid, password)
            } else {
                this.wifiSetNetwork(ssid)
            }

            // retries
            let stat;
            for (let _ = 0; _ < 10; ++_) {
                stat = this.status
                if (stat == WL_CONNECTED) {
                    this.wasConnected = true;
                    net.log("connected")
                    return true;
                }
                pause(1000)
            }
            if ([WL_CONNECT_FAILED, WL_CONNECTION_LOST, WL_DISCONNECTED].indexOf(stat) >= 0) {
                net.log(`failed to connect to "${ssid}" (${stat})`)
            }

            if (stat == WL_NO_SSID_AVAIL) {
                net.log(`no such ssid: "${ssid}"`)
            }

            return false;
        }

        /** 
         * Convert a hostname to a packed 4-byte IP address. Returns
    a 4 bytearray
    */
        public hostbyName(hostname: string): Buffer {
            if (!this.connect())
                return undefined;

            let resp = this.sendCommandGetResponse(_REQ_HOST_BY_NAME_CMD, [control.createBufferFromUTF8(hostname)])
            if (resp[0][0] != 1) {
                net.fail("failed to request hostname")
            }

            resp = this.sendCommandGetResponse(_GET_HOST_BY_NAME_CMD)
            return resp[0];
        }

        /** Ping a destination IP address or hostname, with a max time-to-live
    (ttl). Returns a millisecond timing value
    */
        public ping(dest: string, ttl: number = 250): number {
            if (!this.connect())
                return -1;

            // convert to IP address
            let ip = this.hostbyName(dest)

            // ttl must be between 0 and 255
            ttl = Math.max(0, Math.min(ttl | 0, 255))
            let resp = this.sendCommandGetResponse(_PING_CMD, [ip, buffer1(ttl)])
            return pins.unpackBuffer("<H", resp[0])[0];
        }

        /** Request a socket from the ESP32, will allocate and return a number that
    can then be passed to the other socket commands
    */
        public socket(): number {
            if (!this.connect())
                net.fail("can't connect");

            net.debug("*** Get socket")
            let resp0 = this.sendCommandGetResponse(_GET_SOCKET_CMD)
            let resp = resp0[0][0]
            if (resp == 255) {
                net.fail("no sockets available")
            }
            net.debug("Allocated socket #" + resp)
            return resp
        }

        /** Open a socket to a destination IP address or hostname
    using the ESP32's internal reference number. By default we use
    'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE
    (dest must be hostname for TLS_MODE!)
    */
        public socketOpen(socket_num: number, dest: Buffer | string, port: number, conn_mode = net.TCP_MODE): void {
            this._socknum_ll[0][0] = socket_num
            net.debug("*** Open socket: " + dest + ":" + port)

            let port_param = pins.packBuffer(">H", [port])
            let resp: Buffer[]
            // use the 5 arg version
            if (typeof dest == "string") {
                const dest2 = control.createBufferFromUTF8(dest)
                resp = this.sendCommandGetResponse(_START_CLIENT_TCP_CMD, [dest2, hex`00000000`, port_param, this._socknum_ll[0], buffer1(conn_mode)])
            } else {
                // ip address, use 4 arg vesion
                resp = this.sendCommandGetResponse(_START_CLIENT_TCP_CMD, [dest, port_param, this._socknum_ll[0], buffer1(conn_mode)])
            }

            if (resp[0][0] != 1) {
                net.fail("could not connect to remote server")
            }

        }

        /** Get the socket connection status, can be SOCKET_CLOSED, SOCKET_LISTEN,
    SOCKET_SYN_SENT, SOCKET_SYN_RCVD, SOCKET_ESTABLISHED, SOCKET_FIN_WAIT_1,
    SOCKET_FIN_WAIT_2, SOCKET_CLOSE_WAIT, SOCKET_CLOSING, SOCKET_LAST_ACK, or
    SOCKET_TIME_WAIT
    */
        public socketStatus(socket_num: number): number {
            this._socknum_ll[0][0] = socket_num
            let resp = this.sendCommandGetResponse(_GET_CLIENT_STATE_TCP_CMD, this._socknum_ll)
            return resp[0][0]
        }

        /** Test if a socket is connected to the destination, returns boolean true/false */
        public socket_connected(socket_num: number): boolean {
            return this.socketStatus(socket_num) == SOCKET_ESTABLISHED
        }

        /** Write the bytearray buffer to a socket */
        public socketWrite(socket_num: number, buffer: Buffer): void {
            net.debug("Writing:" + buffer.length)
            this._socknum_ll[0][0] = socket_num
            let resp = this.sendCommandGetResponse(_SEND_DATA_TCP_CMD, [this._socknum_ll[0], buffer], 1, true)
            let sent = resp[0].getNumber(NumberFormat.UInt16LE, 0)
            if (sent != buffer.length) {
                net.fail(`failed to send ${buffer.length} bytes (sent ${sent})`)
            }

            resp = this.sendCommandGetResponse(_DATA_SENT_TCP_CMD, this._socknum_ll)
            if (resp[0][0] != 1) {
                net.fail("failed to verify data sent")
            }

        }

        /** Determine how many bytes are waiting to be read on the socket */
        public socketAvailable(socket_num: number): number {
            this._socknum_ll[0][0] = socket_num
            let resp = this.sendCommandGetResponse(_AVAIL_DATA_TCP_CMD, this._socknum_ll)
            let reply = pins.unpackBuffer("<H", resp[0])[0]
            net.debug(`ESPSocket: ${reply} bytes available`)
            return reply
        }

        /** Read up to 'size' bytes from the socket number. Returns a bytearray */
        public socketRead(socket_num: number, size: number): Buffer {
            net.debug(`Reading ${size} bytes from ESP socket with status ${this.socketStatus(socket_num)}`)
            this._socknum_ll[0][0] = socket_num
            let resp = this.sendCommandGetResponse(_GET_DATABUF_TCP_CMD,
                [this._socknum_ll[0], pins.packBuffer("<H", [size])],
                1, true, true)
            net.debug(`buf >>${resp[0].toString()}<<`)
            return resp[0]
        }

        /** Open and verify we connected a socket to a destination IP address or hostname
    using the ESP32's internal reference number. By default we use
    'conn_mode' TCP_MODE but can also use UDP_MODE or TLS_MODE (dest must
    be hostname for TLS_MODE!)
    */
        public socketConnect(socket_num: number, dest: string | Buffer, port: number, conn_mode = net.TCP_MODE): boolean {
            net.debug("*** Socket connect mode " + conn_mode)
            this.socketOpen(socket_num, dest, port, conn_mode)
            let times = net.monotonic()
            // wait 3 seconds
            while (net.monotonic() - times < 3) {
                if (this.socket_connected(socket_num)) {
                    return true
                }

                pause(10)
            }
            net.fail("failed to establish connection")
            return false
        }

        /** Close a socket using the ESP32's internal reference number */
        public socketClose(socket_num: number): void {
            net.debug("*** Closing socket #" + socket_num)

            this._socknum_ll[0][0] = socket_num
            let resp = this.sendCommandGetResponse(_STOP_CLIENT_TCP_CMD, this._socknum_ll)
            if (resp[0][0] != 1) {
                net.fail("failed to close socket")
            }

        }

        /** Enable/disable debug mode on the ESP32. Debug messages will be
    written to the ESP32's UART.
    */
        public setESPdebug(enabled: boolean) {
            let resp = this.sendCommandGetResponse(_SET_DEBUG_CMD, [buffer1(enabled ? 1 : 0)])
            if (resp[0][0] != 1) {
                net.fail("failed to set debug mode")
            }
        }

        public getTemperature() {
            let resp = this.sendCommandGetResponse(_GET_TEMP_CMD, [])
            if (resp[0].length != 4) {
                net.fail("failed to get temp")
            }
            return resp[0].getNumber(NumberFormat.Float32LE, 0)
        }

        /** 
    Set the io mode for a GPIO pin.
    
    :param int pin: ESP32 GPIO pin to set.
    :param value: direction for pin, digitalio.Direction or integer (0=input, 1=output).
     
    */
        public setPinMode(pin: number, pin_mode: number): void {

            let resp = this.sendCommandGetResponse(_SET_PIN_MODE_CMD, [buffer1(pin), buffer1(pin_mode)])
            if (resp[0][0] != 1) {
                net.fail("failed to set pin mode")
            }

        }

        /** 
    Set the digital output value of pin.
    
    :param int pin: ESP32 GPIO pin to write to.
    :param bool value: Value for the pin.
     
    */
        public setDigitalWrite(pin: number, value: number): void {
            let resp = this.sendCommandGetResponse(_SET_DIGITAL_WRITE_CMD, [buffer1(pin), buffer1(value)])
            if (resp[0][0] != 1) {
                net.fail("failed to write to pin")
            }

        }

        /** 
    Set the analog output value of pin, using PWM.
    
    :param int pin: ESP32 GPIO pin to write to.
    :param float value: 0=off 1.0=full on
     
    */
        public setAnalogWrite(pin: number, analog_value: number) {
            let value = Math.trunc(255 * analog_value)
            let resp = this.sendCommandGetResponse(_SET_ANALOG_WRITE_CMD, [buffer1(pin), buffer1(value)])
            if (resp[0][0] != 1) {
                net.fail("failed to write to pin")
            }

        }
    }

    //% shim=esp32spi::flashDevice
    export function flashDevice() {
        return
    }
}