/*
 * Copyright © 2016 Canonical Ltd.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3,
 * as published by the Free Software Foundation.
 *
 * 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/>.
 *
 * Authored by: Alexandros Frantzis <alexandros.frantzis@canonical.com>
 */

#include "autobrightness_algorithm.h"
#include "backlight_brightness_control.h"
#include "backlight.h"
#include "brightness_params.h"
#include "chrono.h"
#include "device_quirks.h"
#include "event_loop_handler_registration.h"
#include "light_sensor.h"

#include "src/core/log.h"

#include <cmath>
#include <chrono>
#include <string>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

namespace
{

char const* const log_tag = "BacklightBrightnessControl";
auto const null_handler = [](double){};

double normal_brightness_percent(repowerd::DeviceConfig const& device_config)
{
    auto brightness_params = repowerd::BrightnessParams::from_device_config(device_config);
    return static_cast<double>(brightness_params.default_value) / brightness_params.max_value;
}

double dim_brightness_percent(repowerd::DeviceConfig const& device_config)
{
    auto const brightness_params = repowerd::BrightnessParams::from_device_config(device_config);
    return static_cast<double>(brightness_params.dim_value) / brightness_params.max_value;
}

}

repowerd::BacklightBrightnessControl::BacklightBrightnessControl(
    std::shared_ptr<Backlight> const& backlight,
    std::shared_ptr<LightSensor> const& light_sensor,
    std::shared_ptr<AutobrightnessAlgorithm> const& autobrightness_algorithm,
    std::shared_ptr<Chrono> const& chrono,
    std::shared_ptr<Log> const& log,
    DeviceConfig const& device_config,
    DeviceQuirks const& quirks)
    : backlight{backlight},
      light_sensor{light_sensor},
      autobrightness_algorithm{autobrightness_algorithm},
      chrono{chrono},
      log{log},
      normal_before_display_on_autobrightness{
          quirks.normal_before_display_on_autobrightness()},
      ab_supported{autobrightness_algorithm->init(event_loop)},
      event_loop{"Backlight"},
      brightness_handler{null_handler},
      dim_brightness{dim_brightness_percent(device_config)},
      normal_brightness{normal_brightness_percent(device_config)},
      user_normal_brightness{normal_brightness},
      average_time_passed{0},
      active_brightness_type{ActiveBrightnessType::off},
      ab_active{false}
{
    if (ab_supported)
    {
        ab_handler_registration = autobrightness_algorithm->register_autobrightness_handler(
            [this] (double brightness)
            {
                if (ab_active)
                {
                    this->log->logDebug(log_tag, "new_autobrightness_value(%.2f)", brightness);

                    normal_brightness = brightness;

                    if (active_brightness_type == ActiveBrightnessType::normal)
                    {
                        transition_to_brightness_value(normal_brightness, TransitionSpeed::slow);
                    }
                }
            });

        light_handler_registration = light_sensor->register_light_handler(
            [this] (double light)
            {
                event_loop.enqueue(
                    [this, light]
                    {
                        this->autobrightness_algorithm->new_light_value(light);
                    });
            });
    }
}

void repowerd::BacklightBrightnessControl::disable_autobrightness()
{
    if (!ab_supported) return;

    event_loop.enqueue(
        [this]
        {
            if (ab_active)
            {
                autobrightness_algorithm->stop();
                light_sensor->disable_light_events();
                normal_brightness = user_normal_brightness;
                ab_active = false;
                if (active_brightness_type == ActiveBrightnessType::normal)
                    transition_to_brightness_value(normal_brightness, TransitionSpeed::slow);
            }
        }).get();
}

void repowerd::BacklightBrightnessControl::enable_autobrightness()
{
    if (!ab_supported) return;

    event_loop.enqueue(
        [this]
        { 
            if (!ab_active)
            {
                if (active_brightness_type == ActiveBrightnessType::normal)
                {
                    autobrightness_algorithm->start();
                    light_sensor->enable_light_events();
                }
                ab_active = true;
            }
        }).get();
}

void repowerd::BacklightBrightnessControl::set_dim_brightness()
{
    event_loop.enqueue(
        [this]
        { 
            bool should_transition = true;

            auto const backlight_brightness = get_brightness_value();
            if (backlight_brightness > 0.0 &&
                backlight_brightness < dim_brightness)
            {
                should_transition = false;
            }

            if (should_transition)
                transition_to_brightness_value(dim_brightness, TransitionSpeed::normal);

            active_brightness_type = ActiveBrightnessType::dim;
        }).get();
}

void repowerd::BacklightBrightnessControl::set_normal_brightness()
{
    event_loop.enqueue(
        [this]
        { 
            if (ab_active && active_brightness_type == ActiveBrightnessType::off)
            {
                if (normal_before_display_on_autobrightness)
                    transition_to_brightness_value(normal_brightness, TransitionSpeed::normal);
                autobrightness_algorithm->start();
                light_sensor->enable_light_events();
            }
            else
            {
                transition_to_brightness_value(normal_brightness, TransitionSpeed::normal);
            }

            active_brightness_type = ActiveBrightnessType::normal;
        }).get();
}

void repowerd::BacklightBrightnessControl::set_normal_brightness_value(double v)
{
    event_loop.enqueue(
        [this,v]
        { 
            user_normal_brightness = v;

            if (!ab_active)
                normal_brightness = user_normal_brightness;

            if (active_brightness_type == ActiveBrightnessType::normal && !ab_active)
                transition_to_brightness_value(normal_brightness, TransitionSpeed::normal);
        }).get();
}

void repowerd::BacklightBrightnessControl::set_off_brightness()
{
    event_loop.enqueue(
        [this]
        { 
            transition_to_brightness_value(0, TransitionSpeed::normal);
            active_brightness_type = ActiveBrightnessType::off;
            autobrightness_algorithm->stop();
            light_sensor->disable_light_events();
        }).get();
}

repowerd::HandlerRegistration
repowerd::BacklightBrightnessControl::register_brightness_handler(
    BrightnessHandler const& handler)
{
    return EventLoopHandlerRegistration(
        event_loop,
        [this,&handler] { brightness_handler = handler; },
        [this] { brightness_handler = null_handler; });
}

void repowerd::BacklightBrightnessControl::transition_to_brightness_value(
    double target_brightness, TransitionSpeed transition_speed)
{
    auto step = 0.01;
    auto const backlight_brightness = get_brightness_value();
    auto const starting_brightness =
        backlight_brightness == Backlight::unknown_brightness ?
        target_brightness - step : backlight_brightness;
    auto transition_time = (transition_speed == TransitionSpeed::slow ||
                            starting_brightness == 0.0
                            || target_brightness == 0.0) ?
                            250000us : 1000us;

    if (starting_brightness != target_brightness)
    {
        log->logDebug(log_tag, "Transitioning brightness %.2f => %.2f transition time %lldus",
                 starting_brightness, target_brightness, (long long int)transition_time.count());
    }

    auto current_brightness = starting_brightness;

    if (current_brightness < target_brightness)
    {
        while (current_brightness < target_brightness)
        {
            current_brightness += step;
            if (current_brightness > target_brightness) current_brightness = target_brightness;
            set_brightness(target_brightness, current_brightness, step, transition_time);
        }
    }
    else if (current_brightness > target_brightness)
    {
        while (current_brightness > target_brightness)
        {
            current_brightness -= step;
            if (current_brightness < target_brightness) current_brightness = target_brightness;
            set_brightness(target_brightness, current_brightness, step, transition_time);
        }
    }

    if (starting_brightness != target_brightness)
    {
        log->logDebug(log_tag, "Transitioning brightness %.2f => %.2f done",
                 starting_brightness, current_brightness);
        brightness_handler(target_brightness);
    }
}

void repowerd::BacklightBrightnessControl::set_brightness(double target_brightness, double current_brightness, double &step, std::chrono::microseconds &transition_time)
{
    // Time passed for executing a brightness change - calculate rough average 
    auto time_passed = set_brightness_value(current_brightness);
    if (average_time_passed.count() == 0)
    {
        average_time_passed = time_passed;
    }
    else
    {
        average_time_passed = (average_time_passed + time_passed) / 2;
    }

    // Remaining delta to target brightness - If this is zero, we are done
    auto remaining_delta = std::abs(target_brightness - current_brightness);
    if (remaining_delta == 0)
    {
        return;
    }

    // If the remaining transition_time is less than the time needed for a step
    // set the step size to the remaining brightness delta, finishing the transition immediately
    if (transition_time.count() <= average_time_passed.count())
    {
        step = remaining_delta;
        return;
    }
    // Else if this is the initial step try to determine the necessary step size
    // to finish the transition in the remaining time 
    // Update remaining transition time
    transition_time -= average_time_passed;
    
    if ((step == 0.01 && average_time_passed.count() == 0) || average_time_passed.count() > 0) {
        // Number of steps allowed to not exceed the remaining transition time
        auto maximum_steps = transition_time.count() / average_time_passed.count();

        // New step size 
        step = remaining_delta / maximum_steps;
        /*
        log->logDebug(log_tag, "Adjusted new step size: %.2f max steps: %lld rem delta: %.2f rem time: %lld avg time: %lld",
        step, (long long int)maximum_steps, remaining_delta, (long long int)transition_time.count(), (long long int)average_time_passed.count());    
        */    
    }
}

std::chrono::microseconds repowerd::BacklightBrightnessControl::set_brightness_value(double brightness)
{
    auto t1 =  chrono->steady_now();
    backlight->set_brightness(brightness);
    return   std::chrono::duration_cast<std::chrono::microseconds>(chrono->steady_now() - t1);
}

double repowerd::BacklightBrightnessControl::get_brightness_value()
{
    return backlight->get_brightness();
}
