package ru.yandex.webmaster3.core.digest.graphics.draw;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.yandex.webmaster3.core.digest.graphics.draw.data.PointLong;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Created by ifilippov5 on 19.09.17.
 */
public class ComputeBordersUtil {
    private static final Logger log = LoggerFactory.getLogger(ComputeBordersUtil.class);

    public static FrameBoundaries computeBorders(List<PointLong> points, int sectorsCount) {
        long minX = points.stream().map(PointLong::getX).min(Long::compare).get();
        long maxX = points.stream().map(PointLong::getX).max(Long::compare).get();
        long minY = points.stream().map(PointLong::getY).min(Long::compare).get();
        long maxY = points.stream().map(PointLong::getY).max(Long::compare).get();

        int maxBeautyCoefficient = 0;
        BeautyNumberIterator topBoundaryModIterator = new BeautyNumberIterator();
        long bestTopBoundaryMod = 0;
        long bestSectorHeightMod = 0;
        while (topBoundaryModIterator.hasNext()) {
            long topBoundaryMod = topBoundaryModIterator.next();
            BeautyNumberIterator sectorHeightModIterator = new BeautyNumberIterator();
            while (sectorHeightModIterator.hasNext()) {
                long sectorHeightMod = sectorHeightModIterator.next();
                List<Long> sequence = calculateSequence(maxY, minY, sectorsCount,
                        topBoundaryMod, sectorHeightMod);
                if (frameIsFill(minY, sequence.get(0), maxY, sequence.get(sequence.size() - 1))) {
                    int beautyCoefficient = calculateBeautyCoefficient(sequence.subList(1, sequence.size()));
                    if (beautyCoefficient > maxBeautyCoefficient) {
                        maxBeautyCoefficient = beautyCoefficient;
                        bestTopBoundaryMod = topBoundaryMod;
                        bestSectorHeightMod = sectorHeightMod;
                    }
                }
            }
        }
        List<Long> sequence;
        if (bestTopBoundaryMod == 0 || bestSectorHeightMod == 0) {
            sequence = simpleDivision(maxY, minY, sectorsCount);
        } else {
            log.info("bestTopBoundaryMod = {} bestSectorHeightMod = {}", bestTopBoundaryMod, bestSectorHeightMod);
            sequence = calculateSequence(maxY, minY, sectorsCount,
                    bestTopBoundaryMod, bestSectorHeightMod);
        }

        return new FrameBoundaries(minX, maxX, sequence.get(0), sequence.get(sequence.size() - 1),
                                   sequence);
    }

    private static List<Long> simpleDivision(long maxY, long minY, int sectorsCount) {
        log.info("Simple solution");
        maxY++;
        minY--;
        if (minY < 0) {
            minY = 0;
        }
        long sectorHeight = (maxY - minY) / sectorsCount;
        while (sectorHeight * sectorsCount != maxY - minY) {
            maxY++;
            sectorHeight = (maxY - minY) / sectorsCount;
        }

        final long partHeight = sectorHeight;
        return Stream.iterate(minY, division -> division + partHeight)
                .limit(sectorsCount + 1)
                .collect(Collectors.toList());
    }

    // Достаточно ли много места на графике занимают серии
    private static boolean frameIsFill(long minY, Long borderMinY, long maxY, Long borderMaxY) {
        if (borderMinY < 0) {
            return false;
        }
        long diffMax = (borderMaxY - borderMinY) / 5;
        return (borderMaxY - maxY <= diffMax && minY - borderMinY <= diffMax);
        //return (double)(borderMaxY - borderMinY) / (maxY - minY) <= 2.0;
    }

    //Округляем вверх до ближайшего числа, делящегося на mod
    //Если mod = 0, то вверх до ближайшего, оканчивающегося на 0
    private static long toBeautyUp(long value, long mod) {
        if (mod == 0) {
            mod = 10;
        }
        value += mod - value % mod;
        return value;
    }

    private static List<Long> calculateSequence(long maxY, long minY, int sectorsCount, long topBoundaryMod, long sectorHeightMod) {
        maxY = toBeautyUp(maxY, topBoundaryMod);
        long sectorHeight = (maxY - minY) / sectorsCount;
        sectorHeight = toBeautyUp(sectorHeight, sectorHeightMod);
        minY = maxY - sectorsCount * sectorHeight;

        final long partHeight = sectorHeight;
        return Stream.iterate(minY, division -> division + partHeight)
                        .limit(sectorsCount + 1)
                        .collect(Collectors.toList());
    }

    //Вычисляем суммарную "красивость" получившихся меток по оси Y
    //Если хотя бы одна метка не заканчивается ни на 0, ни на 5, то она полностью портит общую "красивость"
    //Иначе общая "красивость" = сумме "красивости" всех меток
    //Красивость метки = (количество 5 в суффиксе красивого числа) + 2 * (количество 0 в суффиксе красивого числа)
    private static int calculateBeautyCoefficient(List<Long> labels) {
        Pattern beauty = Pattern.compile("(5*)(0*)$");

        int result = 0;
        for (Long label : labels) {
            Matcher matcher = beauty.matcher(Long.toString(label));
            if (matcher.find()) {
                int fiveLength = matcher.group(1).length();
                int zeroLength = matcher.group(2).length();
                if (fiveLength + zeroLength == 0) {
                    return -100;
                } else {
                    result += fiveLength + 2 * zeroLength;
                }
            } else {
                throw new RuntimeException("Invalid beauty label : " + Long.toString(label));
            }
        }
        return result;
    }

    //Итерируемся по "красивым" числам
    //Красивое число имеет вид "555000"
    public static class BeautyNumberIterator implements Iterator<Long> {
        private int MAX_MOD_LENGTH = 10;
        private int zeroCount = 0;
        private int fiveCount = 0;

        @Override
        public boolean hasNext() {
            return zeroCount < MAX_MOD_LENGTH;
        }

        @Override
        public Long next() {
            if (zeroCount + fiveCount < MAX_MOD_LENGTH) {
                fiveCount++;
                return toNumber();
            } else {
                zeroCount++;
                fiveCount = 0;
                return toNumber();
            }
        }

        private long toNumber() {
            if (fiveCount + zeroCount == 0) {
                throw new RuntimeException("Error in generator of beauty numbers");
            }
            String fives = String.join("", Collections.nCopies(fiveCount, "5"));
            if (fiveCount == 0) {
                fives = "1";
            }
            String zeroes = String.join("", Collections.nCopies(zeroCount, "0"));
            return Long.valueOf(fives + zeroes);
        }
    }

    public static class FrameBoundaries {
        final long left;
        final long right;
        final long bottom;
        final long top;
        final List<Long> axisYDivisions;

        public FrameBoundaries(long left, long right, long bottom, long top, List<Long> axisYDivisions) {
            this.left = left;
            this.right = right;
            this.bottom = bottom;
            this.top = top;
            this.axisYDivisions = axisYDivisions;
        }

        public long getLeft() {
            return left;
        }

        public long getRight() {
            return right;
        }

        public long getBottom() {
            return bottom;
        }

        public long getTop() {
            return top;
        }

        public List<Long> getAxisYDivisions() {
            return axisYDivisions;
        }
    }
}
