package ru.yandex.webmaster3.viewer.http.searchquery.regions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import ru.yandex.webmaster3.core.data.W3RegionInfo;
import ru.yandex.webmaster3.core.regions.RegionUtils;
import ru.yandex.webmaster3.core.regions.W3RegionsTreeService;
import ru.yandex.webmaster3.storage.searchquery.HostSearchQueryRegionInfo;
import ru.yandex.webmaster3.viewer.regions.SearchQueryRegionsIndexService;

/**
 * @author avhaliullin
 */
public class RegionsTreeBuilder {
    private static final int EXPAND_EXACT_MATCHES = 3;

    private final Comparator<RegionViewBuilder> SHOWS_COMPARATOR = (o1, o2) -> Long.compare(o2.showsCount, o1.showsCount);
    private final Comparator<RegionViewBuilder> PRIORITY_COMPARATOR = (o1, o2) -> Integer.compare(o2.priority, o1.priority);

    private final SearchQueryRegionsIndexService searchQueryRegionsIndexService;
    private final W3RegionsTreeService w3RegionsTreeService;
    private final String filter;
    private final RegionViewBuilder rootBuilder;

    public RegionsTreeBuilder(SearchQueryRegionsIndexService searchQueryRegionsIndexService,
                              W3RegionsTreeService w3RegionsTreeService, String filter, int rootRegionId) {
        this.searchQueryRegionsIndexService = searchQueryRegionsIndexService;
        this.w3RegionsTreeService = w3RegionsTreeService;
        this.filter = filter;
        this.rootBuilder = new RegionViewBuilder(rootRegionId, null);
    }

    public void addRegion(HostSearchQueryRegionInfo region) {
        W3RegionInfo regionOrParent = w3RegionsTreeService.getVisibleRegionOrParent(region.getRegionId(), RegionUtils.VISIBLE_OR_WHOLE_WORLD_PREDICATE);
        if (regionOrParent != null) {
            region = new HostSearchQueryRegionInfo(regionOrParent.getId(), region.getShowsCount());
            W3RegionInfo parent = w3RegionsTreeService.getVisibleParent(region.getRegionId(), RegionUtils.VISIBLE_OR_WHOLE_WORLD_PREDICATE);
            if (parent != null) {
                rootBuilder.addChild(makeInfo(region.getRegionId(), parent.getId(), region.getShowsCount()));
            }
        }
    }

    public void addRegion(int id) {
        W3RegionInfo regionOrParent = w3RegionsTreeService.getVisibleRegionOrParent(id, RegionUtils.VISIBLE_OR_WHOLE_WORLD_PREDICATE);
        if (regionOrParent != null) {
            id = regionOrParent.getId();
            W3RegionInfo parent = w3RegionsTreeService.getVisibleParent(id, RegionUtils.VISIBLE_OR_WHOLE_WORLD_PREDICATE);
            if (parent != null) {
                rootBuilder.addChild(makeInfo(id, parent.getId(), 1));
            }
        }
    }

    private RegionView makeRegion(RegionViewBuilder builder, List<RegionView> children) {
        return new RegionView(builder.regionId, builder.haveVisibleChildren, builder.showsCount > 0, children);
    }

    public List<RegionView> build() {
        RegionViewBuilder onlyChild = rootBuilder;

        while ((onlyChild = onlyChild.onlyVisibleChild()) != null) {
            onlyChild.shouldExpand();
        }
        if (!rootBuilder.exactMatchesInSubtree.isEmpty()) {
            List<RegionViewBuilder> expandLeafs = rootBuilder.exactMatchesInSubtree
                    .values()
                    .stream()
                    .sorted(SHOWS_COMPARATOR)
                    .limit(EXPAND_EXACT_MATCHES)
                    .collect(Collectors.toList());
            long prevShows = 0;
            int priority = EXPAND_EXACT_MATCHES + 1;
            for (int i = 0; i < expandLeafs.size(); i++) {
                // При точном совпадении мы соритируем регионы (вместе с их путями до корня)
                // по убыванию показов самого совпавшего региона. При совпадении показов у таких регионов, хочется
                // таки не поломать оригинальную сортировку родителей
                RegionViewBuilder cur = expandLeafs.get(i);
                if (cur.showsCount != prevShows) {
                    priority--;
                }
                prevShows = cur.showsCount;
                cur.setPriority(priority);
                cur = cur.parent;
                while (cur != null) {
                    cur.setPriority(priority);
                    cur.shouldExpand();
                    cur = cur.parent;
                }
            }
        }
        rootBuilder.shouldExpand();
        RegionView rootView = buildRegion(rootBuilder);

        return rootView.getChildren();
    }

    private RegionView buildRegion(RegionViewBuilder region) {
        List<RegionView> children;
        if (region.shouldExpand) {
            List<RegionViewBuilder> childrenBuilders = new ArrayList<>();
            for (RegionViewBuilder child : region.children.values()) {
                if (child.shouldShow) {
                    childrenBuilders.add(child);
                }
            }
            Collections.sort(childrenBuilders, PRIORITY_COMPARATOR.thenComparing(SHOWS_COMPARATOR));
            children = childrenBuilders.stream().map(this::buildRegion).collect(Collectors.toList());
        } else {
            children = Collections.emptyList();
        }
        return makeRegion(region, children);
    }

    private RegionInfo makeInfo(int regionId, int parentId, long showsCount) {
        SearchQueryRegionsIndexService.NameMatchType matchType;
        if (filter == null) {
            matchType = SearchQueryRegionsIndexService.NameMatchType.MATCH;
        } else {
            matchType = searchQueryRegionsIndexService.match(filter, regionId);
        }
        return new RegionInfo(regionId, parentId, matchType.isMatch(), matchType.isExactMatch(), showsCount);
    }

    private class RegionViewBuilder {
        final Map<Integer, Integer> child2DirectChild;
        final int regionId;
        final RegionViewBuilder parent;
        boolean exactMatch;
        long showsCount;
        boolean shouldShow;
        boolean haveVisibleChildren;
        Map<Integer, RegionViewBuilder> children = new HashMap<>();
        Map<Integer, RegionViewBuilder> exactMatchesInSubtree = new HashMap<>(1);
        boolean shouldExpand;
        int priority;

        public RegionViewBuilder(int regionId, RegionViewBuilder parent) {
            this.regionId = regionId;
            this.parent = parent;
            this.child2DirectChild = searchQueryRegionsIndexService.getChild2ParentsDirectChild(regionId);
        }

        public void setPriority(int priority) {
            if (this.priority < priority) {
                this.priority = priority;
            }
        }

        public void shouldExpand() {
            this.shouldExpand = true;
        }

        void addDescendant(RegionViewBuilder region, long showsCount) {
            this.showsCount += showsCount;
            if (region.exactMatch) {
                exactMatchesInSubtree.put(region.regionId, region);
            }
            if (region.shouldShow) {
                this.shouldShow = true;
                haveVisibleChildren = true;
            }
            if (parent != null) {
                parent.addDescendant(region, showsCount);
            }
        }

        RegionViewBuilder onlyVisibleChild() {
            if (!haveVisibleChildren) {
                return null;
            }
            RegionViewBuilder result = null;
            for (RegionViewBuilder child : children.values()) {
                if (child.shouldShow) {
                    if (result == null) {
                        result = child;
                    } else {
                        return null;
                    }
                }
            }
            return result;
        }

        RegionViewBuilder addChild(RegionInfo region) {
            RegionViewBuilder result;
            if (region.parentId == regionId) {
                result = children.get(region.regionId);
                if (result == null) {
                    result = new RegionViewBuilder(region.regionId, this);
                    children.put(region.regionId, result);
                }
                result.showsCount += region.showsCount;
                result.shouldShow |= region.matchesFilter;
                result.exactMatch = region.exactMatch;

                addDescendant(result, region.showsCount);
            } else {
                Integer parentId = child2DirectChild.get(region.regionId);
                if (parentId == null) {
                    return null;
                }
                RegionViewBuilder parentBuilder = children.get(parentId);
                if (parentBuilder == null) {
                    parentBuilder = addChild(makeInfo(parentId, regionId, 0L));
                }
                result = parentBuilder.addChild(region);
            }
            return result;
        }
    }

    private static class RegionInfo {
        final int regionId;
        final int parentId;
        final boolean matchesFilter;
        final boolean exactMatch;
        final long showsCount;

        public RegionInfo(int regionId, int parentId, boolean matchesFilter, boolean exactMatch, long showsCount) {
            this.regionId = regionId;
            this.parentId = parentId;
            this.matchesFilter = matchesFilter;
            this.exactMatch = exactMatch;
            this.showsCount = showsCount;
        }
    }
}
