"""
Copyright (C) 2020 Piek Solutions LLC

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""

import requests
import binascii
from codecs import getencoder
import time


def enforce_hex(addr):
    if type(addr) == int and addr < 256:
        return hex(addr).lstrip('0x')
    elif type(addr) == str:
        return addr.lstrip('0x')
    else:
        raise ValueError('addr must be hex string or int < 256')


def scanI2c(ip):
    """
    scans devices on i2c bus
        :return: list of hex string addresses present on i2c bus
    """
    try:
        req_url = 'http://' + ip + '/i2c/scan'
        resp = requests.get(url=req_url)
        return resp.content.decode('utf-8')
    except ValueError:
        print("i2c failed scan")


class I2cHttpDevice:

    def __init__(self, ip, dev_addr):
        # device address should be hex string
        self.url = 'http://' + ip + '/i2c/'
        self.dev_addr = enforce_hex(dev_addr)

    def read(self, reg_addr, len_read):
        """
        read len_read bytes starting from register reg_addr
        :param reg_addr: (str) register address to read in hex
        :param len_read: (int) number of bytes to read
        :return: bytestring of data
        """
        assert len_read < 256, "num of bytes to read cannot exceed 255"

        hex_reg_addr = enforce_hex(reg_addr)

        try:
            req_url = '%sread/%s/%s/%d' % (self.url, self.dev_addr, hex_reg_addr, len_read)
            resp = requests.get(url=req_url)
            return binascii.a2b_hex(resp.content)
        except ValueError:
            print("i2c failed read")

    def write(self, reg_addr, data, len_data=0):
        """
        :param reg_addr: (str) register address to write to in hex
        :param data: (str or bytes) hex-encoded bytes, ie: '014ce8'
        :param len_data: (optional int) dummy variable to support code portability
        :return: None
        """

        hex_reg_addr = enforce_hex(reg_addr)
        if type(data) == bytes:
            # to work across python 2+3:
            # https://izziswift.com/whats-the-correct-way-to-convert-bytes-to-a-hex-string-in-python-3/
            data = getencoder('hex')(data)[0].decode('ascii')

        try:
            req_url = '%swrite/%s/%s/%s' % (self.url, self.dev_addr, hex_reg_addr, data)
            requests.get(url=req_url)
        except ValueError:
            print("i2c device 0x%s failed write" % self.dev_addr)


class BME280(I2cHttpDevice):
    """
    Bosch BME280
    https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf

    code adapted from BME280.py, http://abyz.me.uk/rpi/pigpio/examples.html (2016-08-05)
    This example shows that porting the original code to use the Wifi
    Papaya Controller is straightforward and minimal
    """

    _calib00 = 0x88

    _T1 = 0x88 - _calib00
    _T2 = 0x8A - _calib00
    _T3 = 0x8C - _calib00

    _P1 = 0x8E - _calib00
    _P2 = 0x90 - _calib00
    _P3 = 0x92 - _calib00
    _P4 = 0x94 - _calib00
    _P5 = 0x96 - _calib00
    _P6 = 0x98 - _calib00
    _P7 = 0x9A - _calib00
    _P8 = 0x9C - _calib00
    _P9 = 0x9E - _calib00

    _H1 = 0xA1 - _calib00

    _chip_id = 0xD0
    _reset = 0xE0

    _calib26 = 0xE1

    _H2 = 0xE1 - _calib26
    _H3 = 0xE3 - _calib26
    _xE4 = 0xE4 - _calib26
    _xE5 = 0xE5 - _calib26
    _xE6 = 0xE6 - _calib26
    _H6 = 0xE7 - _calib26

    _ctrl_hum = 0xF2
    _status = 0xF3
    _ctrl_meas = 0xF4
    _config = 0xF5

    _rawdata = 0xF7
    _press = 0xF7
    _temp = 0xFA
    _humid = 0xFD

    _p_msb = 0xF7 - _rawdata
    _p_lsb = 0xF8 - _rawdata
    _p_xlsb = 0xF9 - _rawdata
    _t_msb = 0xFA - _rawdata
    _t_lsb = 0xFB - _rawdata
    _t_xlsb = 0xFC - _rawdata
    _h_msb = 0xFD - _rawdata
    _h_lsb = 0xFE - _rawdata

    _os_ms = [0, 1, 2, 4, 8, 16]

    def __init__(self, i2c_conn, gpib_addr, sampling):
        super().__init__(i2c_conn, gpib_addr)

        # additional initialization procedure
        self.sampling = sampling
        self._load_calibration()
        self.measure_delay = self._measurement_time(sampling, sampling, sampling)
        self.t_fine = 0.0

    def _s16(self, _calib, off):
        v = self._u16(_calib, off)
        if v > 32767:
            v -= 65536
        return v

    def _u16(self, _calib, off):
        return _calib[off] | (_calib[off + 1] << 8)

    def _u8(self, _calib, off):
        return _calib[off]

    def _s8(self, _calib, off):
        v = self._u8(_calib, off)
        if v > 127:
            v -= 256
        return v

    def _measurement_time(self, os_temp, os_press, os_hum):
        ms = ((1.25 + 2.3 * self._os_ms[os_temp]) +
              (0.575 + 2.3 * self._os_ms[os_press]) +
              (0.575 + 2.3 * self._os_ms[os_hum]))
        return ms / 1000.0

    def _load_calibration(self):

        d1 = self.read(self._calib00, 26)

        self.T1 = self._u16(d1, self._T1)
        self.T2 = self._s16(d1, self._T2)
        self.T3 = self._s16(d1, self._T3)

        self.P1 = self._u16(d1, self._P1)
        self.P2 = self._s16(d1, self._P2)
        self.P3 = self._s16(d1, self._P3)
        self.P4 = self._s16(d1, self._P4)
        self.P5 = self._s16(d1, self._P5)
        self.P6 = self._s16(d1, self._P6)
        self.P7 = self._s16(d1, self._P7)
        self.P8 = self._s16(d1, self._P8)
        self.P9 = self._s16(d1, self._P9)

        self.H1 = self._u8(d1, self._H1)

        d2 = self.read(self._calib26, 7)

        self.H2 = self._s16(d2, self._H2)

        self.H3 = self._u8(d2, self._H3)

        t = self._u8(d2, self._xE5)

        t_l = t & 15
        t_h = (t >> 4) & 15

        self.H4 = (self._u8(d2, self._xE4) << 4) | t_l

        if self.H4 > 2047:
            self.H4 -= 4096

        self.H5 = (self._u8(d2, self._xE6) << 4) | t_h

        if self.H5 > 2047:
            self.H5 -= 4096

        self.H6 = self._s8(d2, self._H6)

    def _read_raw_data(self):
        # write control bytes for oversampling config
        self.write(self._ctrl_hum, bytes([self.sampling]), 1)
        self.write(self._ctrl_meas, bytes([self.sampling << 5 | self.sampling << 2 | 1]), 1)
        time.sleep(self.measure_delay)

        # read 8 bytes starting from register self._rawdata
        d = self.read(self._rawdata, 8)

        # print(''.join(format(x, '02x') for x in d))
        msb = d[self._t_msb]
        lsb = d[self._t_lsb]
        xlsb = d[self._t_xlsb]
        raw_t = ((msb << 16) | (lsb << 8) | xlsb) >> 4

        msb = d[self._p_msb]
        lsb = d[self._p_lsb]
        xlsb = d[self._p_xlsb]
        raw_p = ((msb << 16) | (lsb << 8) | xlsb) >> 4

        msb = d[self._h_msb]
        lsb = d[self._h_lsb]
        raw_h = (msb << 8) | lsb

        return raw_t, raw_p, raw_h

    def read_temp(self):
        # write measurement control byte
        self.write(self._ctrl_meas, bytes([self.sampling << 5 | self.sampling << 2 | 1]), 1)
        time.sleep(self.measure_delay)

        # read 3 bytes starting from register self._temp
        d = self.read(self._temp, 3)

        # print(''.join(format(x, '02x') for x in d))
        msb, lsb, xlsb = d
        raw_t = ((msb << 16) | (lsb << 8) | xlsb) >> 4

        var1 = (raw_t / 16384.0 - (self.T1) / 1024.0) * float(self.T2)
        var2 = (((raw_t) / 131072.0 - (self.T1) / 8192.0) *
                ((raw_t) / 131072.0 - (self.T1) / 8192.0)) * (self.T3)

        self.t_fine = var1 + var2

        t = (var1 + var2) / 5120.0
        return t

    def read_data(self):
        raw_t, raw_p, raw_h = self._read_raw_data()

        var1 = (raw_t / 16384.0 - (self.T1) / 1024.0) * float(self.T2)
        var2 = (((raw_t) / 131072.0 - (self.T1) / 8192.0) *
                ((raw_t) / 131072.0 - (self.T1) / 8192.0)) * (self.T3)

        self.t_fine = var1 + var2

        t = (var1 + var2) / 5120.0

        var1 = (self.t_fine / 2.0) - 64000.0
        var2 = var1 * var1 * self.P6 / 32768.0
        var2 = var2 + (var1 * self.P5 * 2.0)
        var2 = (var2 / 4.0) + (self.P4 * 65536.0)
        var1 = ((self.P3 * var1 * var1 / 524288.0) + (self.P2 * var1)) / 524288.0
        var1 = (1.0 + var1 / 32768.0) * self.P1
        if var1 != 0.0:
            p = 1048576.0 - raw_p
            p = (p - (var2 / 4096.0)) * 6250.0 / var1
            var1 = self.P9 * p * p / 2147483648.0
            var2 = p * self.P8 / 32768.0
            p = p + (var1 + var2 + self.P7) / 16.0
        else:
            p = 0

        h = self.t_fine - 76800.0

        h = ((raw_h - ((self.H4) * 64.0 + (self.H5) / 16384.0 * h)) *
             ((self.H2) / 65536.0 * (1.0 + (self.H6) / 67108864.0 * h *
                                     (1.0 + (self.H3) / 67108864.0 * h))))

        h = h * (1.0 - self.H1 * h / 524288.0)

        if h > 100.0:
            h = 100.0
        elif h < 0.0:
            h = 0.0

        return t, p, h
