# -*- coding: utf-8 -*-
#
# Isoliner - грид и изолинии (QGIS).
# © 2026 ООО «Информ++» (www.informpp.ru).
# SPDX-License-Identifier: GPL-2.0-or-later
#
"""Манифест модели: какую роль играет каждый слой проекта.

После сборки проект превращается в кучу растров и таблиц без явной
структуры: сорок поверхностей, устья, интервалы, справочник, зеркало. Что
из этого что, знает пользователь, но голова не передаётся вместе с
проектом.

Манифест это явная запись роли для каждого слоя. Хранится он в
пользовательских свойствах проекта QGIS - штатное место, которое
сохраняется в файле проекта, переносится вместе с ним и не требует
ничего внешнего.

Правила три, и второе важнее остальных.

Инструменты пишут манифест сами, в момент создания выходов: пользователь
не обязан размечать руками то, что сделала машина.

Инструменты читают манифест, но НЕ требуют его. Если роли записаны, входы
находятся сами и диалог из десяти списков сжимается до подтверждения.
Если ролей нет - унаследованный проект, чужие данные - всё работает по
явному выбору, как раньше. Манифест сокращает путь, а не становится
условием работы.

Манифест можно построить по чужому проекту: роли угадываются по именам, а
пользователь подтверждает или правит.
"""

import re

SCOPE = "isoliner"          # раздел в свойствах проекта
KEY = "manifest"            # ключ внутри раздела

# Роли, которые знает модель. Список не закрыт: чужой инструмент может
# записать свою роль, и читатель обязан её сохранить, а не потерять.
ROLE_RELIEF = "relief"           # дневная поверхность
ROLE_DATUM = "datum"             # опорная поверхность сборки
ROLE_CONTACT = "contact"         # кровля или подошва тела
ROLE_MIRROR = "mirror"           # уровень растворения, зеркало
ROLE_THICK = "thickness"         # мощность тела
ROLE_COLLAR = "collar"           # устья скважин
ROLE_INTERVAL = "interval"       # интервалы по стволу
ROLE_SURVEY = "survey"           # замеры оси
ROLE_REF = "reference"           # справочник пластов
ROLE_OUTCROP = "outcrop"         # наземные наблюдения и линии контакта
ROLE_SECTION = "section"         # линия разреза
ROLE_GAUGE = "gauge"             # створ гидрологии

ROLES = (ROLE_RELIEF, ROLE_DATUM, ROLE_CONTACT, ROLE_MIRROR, ROLE_THICK,
         ROLE_COLLAR, ROLE_INTERVAL, ROLE_SURVEY, ROLE_REF, ROLE_OUTCROP,
         ROLE_SECTION, ROLE_GAUGE)

# Подсказки для разметки чужого проекта: по имени слоя. Порядок важен,
# первое совпадение выигрывает.
HINTS = (
    (ROLE_MIRROR, r"зеркал|раствор|mirror|dissolution"),
    (ROLE_RELIEF, r"рельеф|цмр|dem|relief|топоплан"),
    (ROLE_DATUM, r"опорн|datum|маркир"),
    (ROLE_THICK, r"мощност|thick|толщин"),
    (ROLE_COLLAR, r"устья|collar"),
    (ROLE_INTERVAL, r"интервал|interval|разбивк"),
    (ROLE_SURVEY, r"замер|инклином|survey|ось скваж"),
    (ROLE_REF, r"справочник|reference|легенд"),
    (ROLE_OUTCROP, r"обнажен|выход|контакт|outcrop|наблюден"),
    # створ раньше разреза: у гидрологов створ называют профилем, и
    # подсказка разреза перехватывала бы его первой
    (ROLE_GAUGE, r"створ|gauge"),
    (ROLE_SECTION, r"разрез|section|профил"),
    (ROLE_CONTACT, r"кровл|подошв|контакт|top|bot"),
)


def guess_role(name):
    """Роль по имени слоя или None, если имя ни о чём не говорит.

    Догадка это подсказка пользователю, а не решение за него: имя слоя
    ничего не гарантирует, и разметка подтверждается человеком.
    """
    low = (name or "").lower()
    for role, pat in HINTS:
        if re.search(pat, low):
            return role
    return None


def parse(text):
    """Разбор записи манифеста: строки вида «идентификатор=роль»."""
    out = {}
    for line in (text or "").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _sep, val = line.partition("=")
        key, val = key.strip(), val.strip()
        if key and val:
            out[key] = val
    return out


def dump(roles):
    """Запись манифеста в текст: по строке на слой, порядок устойчивый."""
    return "\n".join("%s=%s" % (k, roles[k]) for k in sorted(roles))


def read(project):
    """Роли из проекта: {идентификатор слоя: роль}. Пустой словарь, если
    манифеста нет.

    Читающая сторона обязана обходиться без манифеста: правило второе из
    трёх. Отсутствие ролей это не ошибка, а обычный случай чужого или
    унаследованного проекта.
    """
    if project is None:
        return {}
    try:
        text, _ok = project.readEntry(SCOPE, KEY, "")
    except Exception:  # nosec - проекта может не быть вовсе
        return {}
    return parse(text)


def layers_by_role(project, role):
    """Слои проекта с заданной ролью, в порядке дерева слоёв.

    Порядок важен для пачки пластов: кровли и подошвы идут сверху вниз,
    и брать их в произвольном порядке значит собрать разрез вверх ногами.
    Поэтому порядок берётся из дерева слоёв, а не из словаря ролей.
    """
    roles = read(project)
    if not roles:
        return []
    want = {k for k, v in roles.items() if v == role}
    if not want:
        return []
    out = []
    try:
        root = project.layerTreeRoot()
        for node in root.findLayers():
            lay = node.layer()
            if lay is not None and lay.id() in want:
                out.append(lay)
    except Exception:  # nosec
        return []
    return out


def merge(old, new):
    """Слияние: новые роли поверх старых, чужие роли не теряются.

    Инструмент, который не знает роли из другого модуля, обязан её
    сохранить: манифест общий, и вычищать незнакомое значит ломать
    соседям работу.
    """
    out = dict(old or {})
    out.update(new or {})
    return out
