package ru.yandex.webmaster3.core.regions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.collections4.ComparatorUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.webmaster3.core.data.W3RegionInfo;

/**
 * @author avhaliullin
 */
public class W3RegionsTreeService {
    private static final Logger log = LoggerFactory.getLogger(W3RegionsTreeService.class);

    private W3GeobaseService w3geobaseService;
    private final Map<Integer, Integer> regionId2VisibleParentId = new HashMap<>();
    private final Map<Integer, Set<Integer>> regionId2VisibleChildrenId = new HashMap<>();
    private final Map<Integer, W3RegionInfo> regionId2RegionInfo = new HashMap<>();

    public void init() {
        long startedAt = System.currentTimeMillis();
        log.info("W3RegionsTreeService initialization started");
        w3geobaseService.loadRegionsInfo(regionInfo -> {
            regionId2RegionInfo.put(regionInfo.getId(), regionInfo);
            if (regionInfo.getParentId() != null && !regionInfo.getParentId().equals(regionInfo.getId())) {
                regionId2VisibleParentId.put(regionInfo.getId(), regionInfo.getParentId());
                regionId2VisibleChildrenId.computeIfAbsent(regionInfo.getParentId(), x -> new TreeSet<>(ComparatorUtils.naturalComparator())).add(regionInfo.getId());
            }
        });

        log.info("W3RegionsService initialization finished in {} ms", System.currentTimeMillis() - startedAt);
    }

    @Nullable
    public W3RegionInfo getExactRegionInfo(int regionId) {
        W3RegionInfo result = regionId2RegionInfo.get(regionId);
        if (result == null) {
            return null;
        }
        if (!RegionUtils.RegionTypes.NON_HIDDEN_REGION_TYPES.contains(result.getType()) &&
                regionId != RegionUtils.NOT_SPECIFIED_REGION_ID) {
            return null;
        }
        return result;
    }

    public W3RegionInfo getVisibleParent(int regionId, Predicate<W3RegionInfo> predicate) {
        Integer parentId = regionId2VisibleParentId.get(regionId);
        if (parentId == null) {
            return null;
        }
        W3RegionInfo parent = regionId2RegionInfo.get(parentId);
        if (predicate.test(parent)) {
            return parent;
        } else {
            return getVisibleParent(parentId, predicate);
        }
    }

    public W3RegionInfo getFirstParentByPredicate(int regionId, Predicate<W3RegionInfo> predicate) {
        Integer curId = regionId2VisibleParentId.get(regionId);
        while (curId != null) {
            W3RegionInfo curRegion = regionId2RegionInfo.get(curId);
            if (predicate.test(curRegion)) {
                return curRegion;
            }
            curId = curRegion.getParentId();
        }
        return null;
    }

    public List<W3RegionInfo> getVisibleParentsChain(int regionId, Predicate<W3RegionInfo> predicate) {
        List<W3RegionInfo> result = new ArrayList<>();
        Integer curId = regionId2VisibleParentId.get(regionId);
        while (curId != null) {
            W3RegionInfo curRegion = regionId2RegionInfo.get(curId);
            if (predicate == null || predicate.test(curRegion)) {
                result.add(curRegion);
            }
            curId = curRegion.getParentId();
        }
        return result;
    }

    @Nullable
    /**
     * Возвращает регион или его ближайшего родителя, удовлетворяющего предикату
     */
    public W3RegionInfo getVisibleRegionOrParent(int regionId, Predicate<W3RegionInfo> predicate) {
        W3RegionInfo regionInfo = regionId2RegionInfo.get(regionId);
        if (regionInfo == null) {
            return null;
        }
        if (!predicate.test(regionInfo)) {
            Integer parentId = regionInfo.getParentId();
            if (parentId == null) {
                return null;
            } else {
                return getVisibleRegionOrParent(parentId, predicate);
            }
        } else {
            return regionInfo;
        }
    }

    public Stream<W3RegionInfo> getAllVisibleRegions(Predicate<W3RegionInfo> predicate) {
        return regionId2RegionInfo.values().stream().filter(predicate);
    }

    public Set<W3RegionInfo> getVisibleChildren(int regionId, Predicate<W3RegionInfo> predicate) {
        Set<Integer> directChildren = regionId2VisibleChildrenId.get(regionId);
        if (directChildren == null) {
            return Collections.emptySet();
        }
        Set<W3RegionInfo> result = directChildren.stream().map(regionId2RegionInfo::get).collect(Collectors.toSet());
        Set<W3RegionInfo> grandChildren = new HashSet<>(0);
        for (Iterator<W3RegionInfo> it = result.iterator(); it.hasNext(); ) {
            W3RegionInfo cur = it.next();
            if (!predicate.test(cur)) {
                it.remove();
                grandChildren.addAll(getVisibleChildren(cur.getId(), predicate));
            }
        }
        result.addAll(grandChildren);
        return result;
    }

    @Required
    public void setW3geobaseService(W3GeobaseService geobaseService) {
        this.w3geobaseService = geobaseService;
    }

}
