/*
 * EMS-ESP - https://github.com/emsesp/EMS-ESP
 * Copyright 2020  Paul Derbyshire
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

// code originally written by nomis - https://github.com/nomis

#include "dallassensor.h"
#include "emsesp.h"

#ifdef ESP32
#define YIELD
#else
#define YIELD yield()
#endif

namespace emsesp {

uuid::log::Logger DallasSensor::logger_{F_(dallassensor), uuid::log::Facility::DAEMON};

// start the 1-wire
void DallasSensor::start() {
    reload();

    // disabled if dallas gpio is 0
    if (dallas_gpio_) {
#ifndef EMSESP_STANDALONE
        bus_.begin(dallas_gpio_);
#endif
        // API calls
        Command::add_with_json(
            EMSdevice::DeviceType::DALLASSENSOR,
            F_(info),
            [&](const char * value, const int8_t id, JsonObject & json) { return command_info(value, id, json); },
            F_(info_cmd));
        Command::add_with_json(
            EMSdevice::DeviceType::DALLASSENSOR,
            F_(commands),
            [&](const char * value, const int8_t id, JsonObject & json) { return command_commands(value, id, json); },
            F_(commands_cmd));
    }
}

// load the MQTT settings
void DallasSensor::reload() {
    EMSESP::webSettingsService.read([&](WebSettings & settings) {
        dallas_gpio_ = settings.dallas_gpio;
        parasite_    = settings.dallas_parasite;
    });

    if (Mqtt::ha_enabled()) {
        for (uint8_t i = 0; i < MAX_SENSORS; registered_ha_[i++] = false)
            ;
    }
}

void DallasSensor::loop() {
    if (!dallas_gpio_) {
        return; // dallas gpio is 0 (disabled)
    }

#ifndef EMSESP_STANDALONE
    uint32_t time_now = uuid::get_uptime();

    if (state_ == State::IDLE) {
        if (time_now - last_activity_ >= READ_INTERVAL_MS) {
#ifdef EMSESP_DEBUG_SENSOR
            LOG_DEBUG(F("Read sensor temperature"));
#endif
            if (bus_.reset() || parasite_) {
                YIELD;
                bus_.skip();
                bus_.write(CMD_CONVERT_TEMP, parasite_ ? 1 : 0);
                state_     = State::READING;
                scanretry_ = 0;
            } else {
                // no sensors found
                if (sensors_.size()) {
                    sensorfails_++;
                    if (++scanretry_ > SCAN_MAX) { // every 30 sec
                        scanretry_ = 0;
                        LOG_ERROR(F("Bus reset failed"));
                        for (auto & sensor : sensors_) {
                            sensor.temperature_c = EMS_VALUE_SHORT_NOTSET;
                        }
                    }
                }
            }
            last_activity_ = time_now;
        }
    } else if (state_ == State::READING) {
        if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) {
#ifdef EMSESP_DEBUG_SENSOR
            LOG_DEBUG(F("Scanning for sensors"));
#endif
            bus_.reset_search();
            state_ = State::SCANNING;
        } else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
            LOG_WARNING(F("Dallas sensor read timeout"));
            state_ = State::IDLE;
            sensorfails_++;
        }
    } else if (state_ == State::SCANNING) {
        if (time_now - last_activity_ > SCAN_TIMEOUT_MS) {
            LOG_ERROR(F("Dallas sensor scan timeout"));
            state_ = State::IDLE;
            sensorfails_++;
        } else {
            uint8_t addr[ADDR_LEN] = {0};

            if (bus_.search(addr)) {
                if (!parasite_) {
                    bus_.depower();
                }
                if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) {
                    switch (addr[0]) {
                    case TYPE_DS18B20:
                    case TYPE_DS18S20:
                    case TYPE_DS1822:
                    case TYPE_DS1825:
                        int16_t t;
                        t = get_temperature_c(addr);
                        if ((t >= -550) && (t <= 1250)) {
                            sensorreads_++;
                            // check if we have this sensor already
                            bool found = false;
                            for (auto & sensor : sensors_) {
                                if (sensor.id() == get_id(addr)) {
                                    changed_ |= (t != sensor.temperature_c);
                                    sensor.temperature_c = t;
                                    sensor.read          = true;
                                    found                = true;
                                    break;
                                }
                            }
                            // add new sensor
                            if (!found && (sensors_.size() < (MAX_SENSORS - 1))) {
                                sensors_.emplace_back(addr);
                                sensors_.back().temperature_c = t;
                                sensors_.back().read          = true;
                                changed_                      = true;
                            }
                        } else {
                            sensorfails_++;
                        }
                        break;

                    default:
                        sensorfails_++;
                        LOG_ERROR(F("Unknown dallas sensor %s"), Sensor(addr).to_string().c_str());
                        break;
                    }
                } else {
                    sensorfails_++;
                    LOG_ERROR(F("Invalid dallas sensor %s"), Sensor(addr).to_string().c_str());
                }
            } else {
                if (!parasite_) {
                    bus_.depower();
                }
                // check for missing sensors after some samples
                if (++scancnt_ > SCAN_MAX) {
                    for (auto & sensor : sensors_) {
                        if (!sensor.read) {
                            sensor.temperature_c = EMS_VALUE_SHORT_NOTSET;
                            changed_             = true;
                        }
                        sensor.read = false;
                    }
                    scancnt_ = 0;
                } else if (scancnt_ == SCAN_START + 1) { // startup
                    firstscan_ = sensors_.size();
                    LOG_DEBUG(F("Adding %d dallassensor(s) from first scan"), firstscan_);
                } else if ((scancnt_ <= 0) && (firstscan_ != sensors_.size())) { // check 2 times for no change of sensor #
                    scancnt_ = SCAN_START;
                    sensors_.clear(); // restart scaning and clear to get correct numbering
                }
                state_ = State::IDLE;
            }
        }
    }
#endif
}

bool DallasSensor::temperature_convert_complete() {
#ifndef EMSESP_STANDALONE
    if (parasite_) {
        return true; // don't care, use the minimum time in loop
    }
    return bus_.read_bit() == 1;
#else
    return true;
#endif
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"

int16_t DallasSensor::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE
    if (!bus_.reset()) {
        LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Sensor(addr).to_string().c_str());
        return EMS_VALUE_SHORT_NOTSET;
    }
    YIELD;

    uint8_t scratchpad[SCRATCHPAD_LEN] = {0};
    bus_.select(addr);
    bus_.write(CMD_READ_SCRATCHPAD);
    bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
    YIELD;

    if (!bus_.reset()) {
        LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Sensor(addr).to_string().c_str());
        return EMS_VALUE_SHORT_NOTSET;
    }
    YIELD;

    if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) {
        LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from sensor %s"),
                    scratchpad[0],
                    scratchpad[1],
                    scratchpad[2],
                    scratchpad[3],
                    scratchpad[4],
                    scratchpad[5],
                    scratchpad[6],
                    scratchpad[7],
                    scratchpad[8],
                    Sensor(addr).to_string().c_str());
        return EMS_VALUE_SHORT_NOTSET;
    }

    int16_t raw_value = ((int16_t)scratchpad[SCRATCHPAD_TEMP_MSB] << 8) | scratchpad[SCRATCHPAD_TEMP_LSB];

    if (addr[0] == TYPE_DS18S20) {
        raw_value = (raw_value << 3) + 12 - scratchpad[SCRATCHPAD_CNT_REM];
    } else {
        // Adjust based on sensor resolution
        int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3);
        switch (resolution) {
        case 9:
            raw_value &= ~0x7;
            break;
        case 10:
            raw_value &= ~0x3;
            break;
        case 11:
            raw_value &= ~0x1;
            break;
        case 12:
            break;
        }
    }
    raw_value = ((int32_t)raw_value * 625 + 500) / 1000; // round to 0.1
    return raw_value;
#else
    return EMS_VALUE_SHORT_NOTSET;
#endif
}

#pragma GCC diagnostic pop

const std::vector<DallasSensor::Sensor> DallasSensor::sensors() const {
    return sensors_;
}

// skip crc from id.
DallasSensor::Sensor::Sensor(const uint8_t addr[])
    : id_(((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16)
          | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6])) {
}

uint64_t DallasSensor::get_id(const uint8_t addr[]) {
    return (((uint64_t)addr[0] << 48) | ((uint64_t)addr[1] << 40) | ((uint64_t)addr[2] << 32) | ((uint64_t)addr[3] << 24) | ((uint64_t)addr[4] << 16)
            | ((uint64_t)addr[5] << 8) | ((uint64_t)addr[6]));
}

uint64_t DallasSensor::Sensor::id() const {
    return id_;
}

std::string DallasSensor::Sensor::to_string() const {
    std::string str(20, '\0');
    snprintf_P(&str[0],
               str.capacity() + 1,
               PSTR("%02X-%04X-%04X-%04X"),
               (unsigned int)(id_ >> 48) & 0xFF,
               (unsigned int)(id_ >> 32) & 0xFFFF,
               (unsigned int)(id_ >> 16) & 0xFFFF,
               (unsigned int)(id_)&0xFFFF);
    return str;
}

// check to see if values have been updated
bool DallasSensor::updated_values() {
    if (changed_) {
        changed_ = false;
        return true;
    }
    return false;
}

// list commands
bool DallasSensor::command_commands(const char * value, const int8_t id, JsonObject & json) {
    return Command::list(EMSdevice::DeviceType::DALLASSENSOR, json);
}

// creates JSON doc from values
// returns false if empty
// e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":23.30},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":24.0}}
bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & json) {
    if (sensors_.size() == 0) {
        return false;
    }

    uint8_t i = 1; // sensor count
    for (const auto & sensor : sensors_) {
        char sensorID[10]; // sensor{1-n}
        snprintf_P(sensorID, 10, PSTR("sensor%d"), i++);
        if (id == -1) { // show number and id
            JsonObject dataSensor = json.createNestedObject(sensorID);
            dataSensor["id"]      = sensor.to_string();
            if (Helpers::hasValue(sensor.temperature_c)) {
                dataSensor["temp"] = (float)(sensor.temperature_c) / 10;
            }
        } else { // show according to format
            if (Mqtt::dallas_format() == Mqtt::Dallas_Format::SENSORID && Helpers::hasValue(sensor.temperature_c)) {
                json[sensor.to_string()] = (float)(sensor.temperature_c) / 10;
            } else if (Helpers::hasValue(sensor.temperature_c)) {
                json[sensorID] = (float)(sensor.temperature_c) / 10;
            }
        }
    }

    return (json.size() > 0);
}

// send all dallas sensor values as a JSON package to MQTT
void DallasSensor::publish_values(const bool force) {
    uint8_t num_sensors = sensors_.size();

    if (num_sensors == 0) {
        return;
    }

    DynamicJsonDocument doc(100 * num_sensors);
    uint8_t             sensor_no = 1;

    // dallas format is overriden when using Home Assistant
    // uint8_t dallas_format = Mqtt::ha_enabled() ? Mqtt::Dallas_Format::NUMBER : Mqtt::dallas_format();
    uint8_t dallas_format = Mqtt::dallas_format();

    for (const auto & sensor : sensors_) {
        char sensorID[10]; // sensor{1-n}
        snprintf_P(sensorID, 10, PSTR("sensor%d"), sensor_no);
        if (dallas_format == Mqtt::Dallas_Format::SENSORID) {
            // e.g. dallassensor_data = {"28-EA41-9497-0E03":23.3,"28-233D-9497-0C03":24.0}
            if (Helpers::hasValue(sensor.temperature_c)) {
                doc[sensor.to_string()] = (float)(sensor.temperature_c) / 10;
            }
        } else if (dallas_format == Mqtt::Dallas_Format::NUMBER) {
            // e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03","temp":23.3},"sensor2":{"id":"28-233D-9497-0C03","temp":24.0}}
            JsonObject dataSensor = doc.createNestedObject(sensorID);
            dataSensor["id"]      = sensor.to_string();
            if (Helpers::hasValue(sensor.temperature_c)) {
                dataSensor["temp"] = (float)(sensor.temperature_c) / 10;
            }
        }

        // create the HA MQTT config
        // to e.g. homeassistant/sensor/ems-esp/dallas_28-233D-9497-0C03/config
        if (Mqtt::ha_enabled()) {
            if (!(registered_ha_[sensor_no - 1]) || force) {
                StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
                config["dev_cla"] = FJSON("temperature");

                char stat_t[50];
                snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/dallassensor_data"), Mqtt::base().c_str());
                config["stat_t"] = stat_t;

                config["unit_of_meas"] = FJSON("°C");

                char str[50];
                if (dallas_format == Mqtt::Dallas_Format::SENSORID) {
                    snprintf_P(str, sizeof(str), PSTR("{{value_json['%s']}}"), sensor.to_string().c_str());
                } else {
                    snprintf_P(str, sizeof(str), PSTR("{{value_json.sensor%d.temp}}"), sensor_no);
                }
                config["val_tpl"] = str;

                // name as sensor number not the long unique ID
                if (dallas_format == Mqtt::Dallas_Format::SENSORID) {
                    snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %s"), sensor.to_string().c_str());
                } else {
                    snprintf_P(str, sizeof(str), PSTR("Dallas Sensor %d"), sensor_no);
                }
                config["name"] = str;

                snprintf_P(str, sizeof(str), PSTR("dallas_%s"), sensor.to_string().c_str());
                config["uniq_id"] = str;

                JsonObject dev = config.createNestedObject("dev");
                JsonArray  ids = dev.createNestedArray("ids");
                ids.add("ems-esp");

                char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
                if (dallas_format == Mqtt::Dallas_Format::SENSORID) {
                    // use '_' as HA doesn't like '-' in the topic name
                    std::string topicname = sensor.to_string();
                    std::replace(topicname.begin(), topicname.end(), '-', '_');
                    snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallas_sensor%s/config"), Mqtt::base().c_str(), topicname.c_str());
                } else {
                    snprintf_P(topic, sizeof(topic), PSTR("sensor/%s/dallas_sensor%d/config"), Mqtt::base().c_str(), sensor_no);
                }
                Mqtt::publish_ha(topic, config.as<JsonObject>());

                registered_ha_[sensor_no - 1] = true;
            }
        }
        sensor_no++; // increment sensor count
    }

    Mqtt::publish(F("dallassensor_data"), doc.as<JsonObject>());
}

} // namespace emsesp
