#include "pch.h"
#include "Layout.h"

#include <FancyZonesLib/FancyZonesData/CustomLayouts.h>
#include <FancyZonesLib/FancyZonesWindowProperties.h>
#include <FancyZonesLib/LayoutConfigurator.h>
#include <FancyZonesLib/Settings.h>
#include <FancyZonesLib/WindowUtils.h>

#include <common/logger/logger.h>

namespace ZoneSelectionAlgorithms
{
    constexpr int OVERLAPPING_CENTERS_SENSITIVITY = 75;

    template<class CompareF>
    ZoneIndexSet ZoneSelectPriority(const ZonesMap& zones, const ZoneIndexSet& capturedZones, CompareF compare)
    {
        size_t chosen = 0;

        for (size_t i = 1; i < capturedZones.size(); ++i)
        {
            if (compare(zones.at(capturedZones[i]), zones.at(capturedZones[chosen])))
            {
                chosen = i;
            }
        }

        return { capturedZones[chosen] };
    }

    ZoneIndexSet ZoneSelectSubregion(const ZonesMap& zones, const ZoneIndexSet& capturedZones, POINT pt, int sensitivityRadius)
    {
        auto expand = [&](RECT& rect) {
            rect.top -= sensitivityRadius / 2;
            rect.bottom += sensitivityRadius / 2;
            rect.left -= sensitivityRadius / 2;
            rect.right += sensitivityRadius / 2;
        };

        // Compute the overlapped rectangle.
        RECT overlap = zones.at(capturedZones[0]).GetZoneRect();
        expand(overlap);

        for (size_t i = 1; i < capturedZones.size(); ++i)
        {
            RECT current = zones.at(capturedZones[i]).GetZoneRect();
            expand(current);

            overlap.top = max(overlap.top, current.top);
            overlap.left = max(overlap.left, current.left);
            overlap.bottom = min(overlap.bottom, current.bottom);
            overlap.right = min(overlap.right, current.right);
        }

        // Avoid division by zero
        int width = max(overlap.right - overlap.left, 1);
        int height = max(overlap.bottom - overlap.top, 1);

        bool verticalSplit = height > width;
        ZoneIndex zoneIndex;

        if (verticalSplit)
        {
            zoneIndex = (static_cast<ZoneIndex>(pt.y) - overlap.top) * capturedZones.size() / height;
        }
        else
        {
            zoneIndex = (static_cast<ZoneIndex>(pt.x) - overlap.left) * capturedZones.size() / width;
        }

        zoneIndex = std::clamp(zoneIndex, static_cast<ZoneIndex>(0), static_cast<ZoneIndex>(capturedZones.size()) - 1);

        return { capturedZones[zoneIndex] };
    }

    ZoneIndexSet ZoneSelectClosestCenter(const ZonesMap& zones, const ZoneIndexSet& capturedZones, POINT pt)
    {
        auto getCenter = [](auto zone) {
            RECT rect = zone.GetZoneRect();
            return POINT{ (rect.right + rect.left) / 2, (rect.top + rect.bottom) / 2 };
        };
        auto pointDifference = [](POINT pt1, POINT pt2) {
            return (pt1.x - pt2.x) * (pt1.x - pt2.x) + (pt1.y - pt2.y) * (pt1.y - pt2.y);
        };
        auto distanceFromCenter = [&](auto zone) {
            POINT center = getCenter(zone);
            return pointDifference(center, pt);
        };
        auto closerToCenter = [&](auto zone1, auto zone2) {
            if (pointDifference(getCenter(zone1), getCenter(zone2)) > OVERLAPPING_CENTERS_SENSITIVITY)
            {
                return distanceFromCenter(zone1) < distanceFromCenter(zone2);
            }
            else
            {
                return zone1.GetZoneArea() < zone2.GetZoneArea();
            };
        };
        return ZoneSelectPriority(zones, capturedZones, closerToCenter);
    }
}

Layout::Layout(const LayoutData& data) :
    m_data(data)
{
}

bool Layout::Init(const FancyZonesUtils::Rect& workArea, HMONITOR monitor) noexcept
{
    // invalid work area
    if (workArea.width() == 0 || workArea.height() == 0)
    {
        Logger::error(L"Layout initialization: invalid work area");
        return false;
    }

    // invalid zoneCount, may cause division by zero
    bool isGridType = m_data.type == FancyZonesDataTypes::ZoneSetLayoutType::Columns 
        || m_data.type == FancyZonesDataTypes::ZoneSetLayoutType::Rows 
        || m_data.type == FancyZonesDataTypes::ZoneSetLayoutType::Grid 
        || m_data.type == FancyZonesDataTypes::ZoneSetLayoutType::PriorityGrid;

    if (m_data.zoneCount < 0 || (m_data.zoneCount == 0 && isGridType))
    {
        Logger::error(L"Layout initialization: invalid zone count");
        return false;
    }

    auto spacing = m_data.showSpacing ? m_data.spacing : 0; 

    switch (m_data.type)
    {
    case FancyZonesDataTypes::ZoneSetLayoutType::Blank:
        m_zones = {};
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::Focus:
        m_zones = LayoutConfigurator::Focus(workArea, m_data.zoneCount);
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::Columns:
        m_zones = LayoutConfigurator::Columns(workArea, m_data.zoneCount, spacing);
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::Rows:
        m_zones = LayoutConfigurator::Rows(workArea, m_data.zoneCount, spacing);
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::Grid:
        m_zones = LayoutConfigurator::Grid(workArea, m_data.zoneCount, spacing);
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::PriorityGrid:
        m_zones = LayoutConfigurator::PriorityGrid(workArea, m_data.zoneCount, spacing);
        break;
    case FancyZonesDataTypes::ZoneSetLayoutType::Custom:
    {
        const auto customLayoutData = CustomLayouts::instance().GetCustomLayoutData(m_data.uuid);
        if (customLayoutData.has_value())
        {
            m_zones = LayoutConfigurator::Custom(workArea, monitor, customLayoutData.value(), spacing);
        }
        else
        {
            Logger::error(L"Custom layout not found");
            return false;
        }
    }
    break;
    }

    return m_zones.size() == m_data.zoneCount;
}

GUID Layout::Id() const noexcept
{
    return m_data.uuid;
}

FancyZonesDataTypes::ZoneSetLayoutType Layout::Type() const noexcept
{
    return m_data.type;
}

const ZonesMap& Layout::Zones() const noexcept
{
    return m_zones;
}

ZoneIndexSet Layout::ZonesFromPoint(POINT pt) const noexcept
{
    ZoneIndexSet capturedZones;
    ZoneIndexSet strictlyCapturedZones;
    for (const auto& [zoneId, zone] : m_zones)
    {
        const RECT& zoneRect = zone.GetZoneRect();
        if (zoneRect.left - m_data.sensitivityRadius <= pt.x && pt.x <= zoneRect.right + m_data.sensitivityRadius &&
            zoneRect.top - m_data.sensitivityRadius <= pt.y && pt.y <= zoneRect.bottom + m_data.sensitivityRadius)
        {
            capturedZones.emplace_back(zoneId);
        }

        if (zoneRect.left <= pt.x && pt.x < zoneRect.right &&
            zoneRect.top <= pt.y && pt.y < zoneRect.bottom)
        {
            strictlyCapturedZones.emplace_back(zoneId);
        }
    }

    // If only one zone is captured, but it's not strictly captured
    // don't consider it as captured
    if (capturedZones.size() == 1 && strictlyCapturedZones.size() == 0)
    {
        return {};
    }

    // If captured zones do not overlap, return all of them
    // Otherwise, return one of them based on the chosen selection algorithm.
    bool overlap = false;
    for (size_t i = 0; i < capturedZones.size(); ++i)
    {
        for (size_t j = i + 1; j < capturedZones.size(); ++j)
        {
            RECT rectI;
            RECT rectJ;
            try
            {
                rectI = m_zones.at(capturedZones[i]).GetZoneRect();
                rectJ = m_zones.at(capturedZones[j]).GetZoneRect();
            }
            catch (std::out_of_range)
            {
                return {};
            }

            if (max(rectI.top, rectJ.top) + m_data.sensitivityRadius < min(rectI.bottom, rectJ.bottom) &&
                max(rectI.left, rectJ.left) + m_data.sensitivityRadius < min(rectI.right, rectJ.right))
            {
                overlap = true;
                break;
            }
        }
        if (overlap)
        {
            break;
        }
    }

    if (overlap)
    {
        try
        {
            using Algorithm = OverlappingZonesAlgorithm;

            switch (FancyZonesSettings::settings().overlappingZonesAlgorithm)
            {
            case Algorithm::Smallest:
                return ZoneSelectionAlgorithms::ZoneSelectPriority(m_zones, capturedZones, [&](auto zone1, auto zone2) { return zone1.GetZoneArea() < zone2.GetZoneArea(); });
            case Algorithm::Largest:
                return ZoneSelectionAlgorithms::ZoneSelectPriority(m_zones, capturedZones, [&](auto zone1, auto zone2) { return zone1.GetZoneArea() > zone2.GetZoneArea(); });
            case Algorithm::Positional:
                return ZoneSelectionAlgorithms::ZoneSelectSubregion(m_zones, capturedZones, pt, m_data.sensitivityRadius);
            case Algorithm::ClosestCenter:
                return ZoneSelectionAlgorithms::ZoneSelectClosestCenter(m_zones, capturedZones, pt);
            }
        }
        catch (std::out_of_range)
        {
            Logger::error("Exception out_of_range was thrown in ZoneSet::ZonesFromPoint");
            return { capturedZones[0] };
        }
    }

    return capturedZones;
}

ZoneIndexSet Layout::GetCombinedZoneRange(const ZoneIndexSet& initialZones, const ZoneIndexSet& finalZones) const noexcept
{
    ZoneIndexSet combinedZones, result;
    std::set_union(begin(initialZones), end(initialZones), begin(finalZones), end(finalZones), std::back_inserter(combinedZones));

    RECT boundingRect{};
    bool boundingRectEmpty = true;

    for (ZoneIndex zoneId : combinedZones)
    {
        if (m_zones.contains(zoneId))
        {
            const RECT rect = m_zones.at(zoneId).GetZoneRect();
            if (boundingRectEmpty)
            {
                boundingRect = rect;
                boundingRectEmpty = false;
            }
            else
            {
                boundingRect.left = min(boundingRect.left, rect.left);
                boundingRect.top = min(boundingRect.top, rect.top);
                boundingRect.right = max(boundingRect.right, rect.right);
                boundingRect.bottom = max(boundingRect.bottom, rect.bottom);
            }
        }
    }

    if (!boundingRectEmpty)
    {
        for (const auto& [zoneId, zone] : m_zones)
        {
            const RECT rect = zone.GetZoneRect();
            if (boundingRect.left <= rect.left && rect.right <= boundingRect.right &&
                boundingRect.top <= rect.top && rect.bottom <= boundingRect.bottom)
            {
                result.push_back(zoneId);
            }
        }
    }

    return result;
}

RECT Layout::GetCombinedZonesRect(const ZoneIndexSet& zones)
{
    RECT size{};
    bool sizeEmpty = true;

    for (ZoneIndex id : zones)
    {
        if (m_zones.contains(id))
        {
            const auto& zone = m_zones.at(id);
            const RECT newSize = zone.GetZoneRect();
            if (!sizeEmpty)
            {
                size.left = min(size.left, newSize.left);
                size.top = min(size.top, newSize.top);
                size.right = max(size.right, newSize.right);
                size.bottom = max(size.bottom, newSize.bottom);
            }
            else
            {
                size = newSize;
                sizeEmpty = false;
            }
        }
    }

    return size;
}
