package net.ranides.assira.xls;

import net.ranides.assira.generic.Ref;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.math.MathStats;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;

public class XLSReader {

    private final Path path;
    private final XSSFWorkbook wb;
    private final XSSFSheet sh;

    private double accuracy = 0.1;
    private boolean skipHeader = false;

    public XLSReader(String path) throws IOException {
        this(Paths.get(path));
    }

    public XLSReader(Path path) throws IOException {
        this.path = path;
        this.wb = new XSSFWorkbook(new FileInputStream(path.toFile()));
        this.sh = wb.getSheetAt(0);
    }

    public static Function<Path, Optional<XLSReader>> loader(BiConsumer<Path, IOException> onError) {
        return file -> load(file, onError);
    }

    public static Optional<XLSReader> load(Path path, BiConsumer<Path, IOException> onError) {
        try {
            return Optional.of(new XLSReader(path));
        } catch (IOException ex) {
            onError.accept(path, ex);
            return Optional.empty();
        }
    }

    public Path path() {
        return path;
    }

    public XLSReader accuracy(double accuracy) {
        this.accuracy = accuracy;
        return this;
    }

    public int detectColumns() {
        return CQuery.from().iterable(sh)
                .skip(1)
                .filter(heuristicFilter())
                .map(Row::getLastCellNum)
                .applyThis(MathStats::mode)
                .getValue();
    }

    public boolean detectHeader() {
        CQuery<XLSUtils.RowInfo> stat = CQuery.from().iterable(sh)
                .skip(1)
                .filter(heuristicFilter())
                .map(XLSUtils::info);


        MathStats.Mode<String> back = MathStats.mode(stat.map(r -> r.get(0)).filter(Objects::nonNull).map(XLSUtils.CellInfo::getBack));
        MathStats.Mode<String> fore = MathStats.mode(stat.map(r -> r.get(0)).filter(Objects::nonNull).map(XLSUtils.CellInfo::getFore));

        XLSUtils.RowInfo head = XLSUtils.info(CQuery.from().iterable(sh).first().orElse(null));

        if(2 * back.getCount() > back.getDomain() && !head.get(0).getBack().equals(back.getValue())) {
            return true;
        }
        if(2 * fore.getCount() > fore.getDomain() && !head.get(0).getFore().equals(fore.getValue())) {
            return true;
        }

        return false;
    }

    private Predicate<Row> heuristicFilter() {
        Random rand = new Random(3);
        return v -> v.getRowNum() < 20 || rand.nextDouble()<=accuracy;
    }

    public XLSReader skipHeader() {
        return skipHeader(detectHeader());
    }

    public XLSReader skipHeader(boolean option) {
        this.skipHeader = option;
        return this;
    }

    public Ref<Row> header() {
        return header(false);
    }

    public Ref<Row> header(boolean force) {
        return force || detectHeader() ? IteratorUtils.first(sh.iterator()) : Ref.empty();
    }

    public CQuery<Row> rows(){
        return CQuery.from().iterable(sh).skip(skipHeader ? 1 : 0);
    }

    public CQuery<List<String>> cells(){
        return CQuery.from().iterable(sh).skip(skipHeader ? 1 : 0).map(XLSUtils::text);
    }

}
