package net.ranides.assira.collection;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.collection.lists.IntList;
import net.ranides.assira.collection.lists.IntListUtils;
import net.ranides.assira.collection.lists.ListUtils;
import net.ranides.assira.collection.lookups.OpenLookup;
import net.ranides.assira.collection.lookups.Lookup;
import net.ranides.assira.collection.maps.AMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.collection.query.CQueryAbstract;
import net.ranides.assira.collection.sets.SetUtils;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.reflection.BeanModel;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

/**
 * Grid represents two dimensional table of values: list of cells of type T.
 *
 * The whole idea is to allow processing of data in columns, rows or as a list of records
 * Grid can contain header which maps column names into column indexes.
 *
 * Using header you can view every column as a field with specified name,
 * especially you can view list of rows as a list of records represented by maps.
 *
 * @param <T> type of values
 */
public class Grid<T> {

    private final List<String> head;

    private final Lookup<String> headmap;

    private final List<List<T>> data;

    private final GridHeader header;

    private final GridRows rows;

    private final GridColumns columns;

    private Grid(List<String> head, Lookup<String> headmap, List<List<T>> data) {
        this.head = head;
        this.headmap = headmap;
        this.data = data;
        this.header = new GridHeader(null);
        this.rows = new GridRows(null);
        this.columns = new GridColumns(null);
    }

    private Grid(List<String> head, List<List<T>> data) {
        this(head, initHead(head), data);
    }

    private static Lookup<String> initHead(List<String> head) {
        OpenLookup<String> out = new OpenLookup<>();
        for(int i=0, n=head.size(); i<n; i++) {
            out.put(head.get(i), i);
        }
        return out;
    }

    /**
     * Creates new grid from records. Every records should have the same structure.
     * Header is created from list of keys inside records.
     * List of cells is created from maps (using already generated header)
     *
     * @param records records
     * @param <T> type of cell
     * @return grid
     */
    public static <T> Grid<T> fromMap(List<? extends Map<String, T>> records) {
        List<String> head = records.stream()
            .flatMap(map -> map.keySet().stream())
            .distinct()
            .collect(Collectors.toList());

        List<List<T>> data = records.stream()
            .map((Map<String, T> r) -> head.stream().map(r::get).collect(Collectors.toList()))
            .collect(Collectors.toList());

        return new Grid<>(head, data);
    }

    /**
     * Creates new grid from records. Every records should have the same structure.
     * Every object is converted into record using BeanModel.
     * Header is created from list of keys inside records.
     * List of cells is created from maps (using already generated header)
     *
     * @param rows rows
     * @return grid
     */
    public static Grid<Object> fromObjects(List<?> rows) {
        return fromMap(rows.stream().map(v -> BeanModel.typefor(v).fluent(v)).collect(toList()));
    }

    /**
     * Creates new grid from cells.
     * It uses provided header to define mapping between column indexes and column names.
     *
     * @param header header
     * @param rows rows
     * @param <T> type of cell
     * @return grid
     */
    public static <T> Grid<T> fromRows(List<String> header, List<List<T>> rows) {
        return new Grid<>(header, rows);
    }

    @Override
    public String toString() {
        return "Grid{ header =" + head +  ", data=" + data + "}";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Grid)) {
            return false;
        }
        Grid<?> grid = (Grid<?>) o;
        return Objects.equals(head, grid.head) && Objects.equals(data, grid.data);
    }

    @Override
    public int hashCode() {
        return Objects.hash(head, data);
    }

    /**
     * Returns header defined for this grid.
     * Header can be used as a query with column names, to process them sequentialy.
     * Header implements fast lookup for names: "indexOf" returns result in O(1) time.
     *
     * @return header
     */
    public GridHeader header() {
        return header;
    }

    /**
     * Returns cells organized in rows.
     * Effectively, returned query is backed by list generated once.
     *
     * @return rows
     */
    public GridRows rows() {
        return rows;
    }

    /**
     * Returns cells organized in columns.
     * Effectively, returned query is backed by list generated once.
     *
     * @return columns
     */
    public GridColumns columns() {
        return columns;
    }

    /**
     * Returns new grid with selected rows.
     *
     * @param selector list of row indexes to select
     * @return grid
     */
    public Grid<T> rows(IntList selector) {
        return new Grid<>(head, headmap, IntListUtils.map(selector, data::get));
    }

    /**
     * Returns new grid with selected columns
     *
     * @param selector list of column indexes to select
     * @return grid
     */
    public Grid<T> columns(IntList selector) {
        return new Grid<>(IntListUtils.map(selector, head::get), data.stream().map(row -> IntListUtils.map(selector, row::get)).collect(toList()));
    }

    /**
     * Returns cells as list of lists.
     *
     * @return cells
     */
    public List<List<T>> list() {
        return data;
    }

    /**
     * Returns all cells as stream. Every cell is returned as GridCursor:
     * cursor gives access to cell value and its position (row and column index).
     *
     * @return stream with cells
     */
    public CQuery<GridCursor> stream() {
        return rows().flat(r -> r);
    }

    /**
     * Returns all rows as records (maps with keys defined by header).
     *
     * @return list of rows
     */
    public List<Map<String, T>> records() {
        return ListUtils.map(data, GridRecord::new);
    }

    /**
     * Returns cell at specified position.
     *
     * @param row row
     * @param column column
     * @return cell value
     */
    public T get(int row, int column) {
        return data.get(row).get(column);
    }

    /**
     * Returns cell at specified position.
     *
     * @param row row
     * @param column column
     * @return cell value
     */
    public T get(int row, String column) {
        return data.get(row).get(headmap.getInt(column));
    }

    /**
     * Changes cell value at specified position
     * @param row row
     * @param column column
     * @param value value
     */
    public void set(int row, int column, T value) {
        data.get(row).set(column, value);
    }

    /**
     * Changes cell value at specified position
     * @param row row
     * @param column column
     * @param value value
     */
    public void set(int row, String column, T value) {
        data.get(row).set(headmap.getInt(column), value);
    }

    /**
     * Returns new grid with sorted rows by specified columns.
     * Order of columns inside selector is important and defines comparision priority.
     *
     * @param selector list of columns to use in sort
     * @return grid
     */
    public Grid<T> sort(IntList selector) {
        return new Grid<>(head, headmap, data.stream().sorted((a, b) -> rowcmp(selector, a, b)).collect(toList()));
    }

    /**
     * Transform every cell into new value.
     * Mapping function gets GridCursor, which means you can use not only cell value, but cell position too.
     *
     * @param mapper mapper
     * @param <R> new cell type
     * @return grid
     */
    public <R> Grid<R> map(Function<GridCursor, R> mapper) {
        List<List<R>> out = new ArrayList<>(data.size());
        for(int r=0, R=rows.size(); r<R; r++) {
            List<T> irow = data.get(r);
            List<R> orow = new ArrayList<>(irow.size());
            for(int c=0, C=irow.size(); c<C; c++) {
                orow.add( mapper.apply(new GridCursor(r,c)));
            }
            out.add(orow);
        }
        return new Grid<>(head, headmap, out);
    }

    private int rowcmp(IntList indexes, List<T> a, List<T> b) {
        for(int i : indexes) {
            int c = CompareUtils.cmp(a.get(i), b.get(i));
            if(c != 0) {
                return c;
            }
        }
        return 0;
    }

    private boolean isValid(int row, int column) {
        return row < data.size() && column < data.get(row).size();
    }

    @RequiredArgsConstructor
    private class GridRecord extends AMap<String, T> {

        private final List<T> row;

        @Override
        public Set<Entry<String, T>> entrySet() {
            return SetUtils.map(headmap.keySet(), key -> new SimpleImmutableEntry<>(key, row.get(headmap.getInt(key))) );
        }

        @Override
        public int size() {
            return head.size();
        }

        @Override
        public boolean containsKey(Object key) {
            return headmap.containsKey(key);
        }

        @Override
        public boolean containsValue(Object value) {
            return row.contains(value);
        }

        @Override
        public T get(Object key) {
            return row.get(headmap.get(key));
        }
    }

    /**
     * GridHeader is essentially {@code CQuery<String>} backed by list, but with opitimized lookup by name.
     * Method "indexOf(name)" will return in constant time.
     */
    public class GridHeader extends CQueryAbstract<String> {

        private final Supplier<? extends RuntimeException> message;

        private GridHeader(Supplier<? extends RuntimeException> message) {
            this.message = message;
        }

        @Override
        public Supplier<? extends RuntimeException> message() {
            return message;
        }

        @Override
        public CQuery<String> message(Supplier<? extends RuntimeException> message) {
            return new GridHeader(message);
        }

        @Override
        public CQuery<String> parallel() {
            return this;
        }

        @Override
        public CQuery<String> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public int size() {
            return head.size();
        }

        @Override
        public Stream<String> stream() {
            return head.stream();
        }

        @Override
        public Iterator<String> iterator() {
            return IteratorUtils.withCause(head.iterator(), message);
        }

        @Override
        public int indexOf(String value) {
            return headmap.get(value);
        }

        @Override
        public int lastIndexOf(String value) {
            return headmap.get(value);
        }
    }

    /**
     * GridColumns is essentially CQuery backed by list of columns.
     */
    public class GridColumns extends CQueryAbstract<GridColumn> {

        private final List<GridColumn> list;

        private final Supplier<? extends RuntimeException> message;

        private GridColumns(Supplier<? extends RuntimeException> message) {
            this.message = message;
            this.list = IntStream.range(0, size()).mapToObj(i -> new GridColumn(i, null)).collect(toList());
        }

        @Override
        public Supplier<? extends RuntimeException> message() {
            return message;
        }

        @Override
        public CQuery<GridColumn> message(Supplier<? extends RuntimeException> message) {
            return new GridColumns(message);
        }

        @Override
        public CQuery<GridColumn> parallel() {
            return this;
        }

        @Override
        public CQuery<GridColumn> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public int size() {
            return head.size();
        }

        @Override
        public boolean hasFastList() {
            return true;
        }

        @Override
        public Stream<GridColumn> stream() {
            return list.stream();
        }

        @Override
        public Iterator<GridColumn> iterator() {
            return IteratorUtils.withCause(list.iterator(), message);
        }

        @Override
        public List<GridColumn> list() {
            return list;
        }

        /**
         * Returns column at specified index.
         *
         * @param index index
         * @return column
         * @throws IndexOutOfBoundsException if there is no such column
         */
        public GridColumn get(int index) {
            return list.get(index);
        }
    }

    /**
     * GridColumn is essentially CQuery not backed by any in-memory structure.
     * GridColumn is a view and reads cells in realtime, by just using column index as bound argument.
     *
     * It allows iteration through all rows and extract cell in specified column.
     * It allows fast read/write to cell using row index.
     */
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public class GridColumn extends CQueryAbstract<GridCursor> {

        private final int index;

        private final Supplier<? extends RuntimeException> message;

        @Override
        public Supplier<? extends RuntimeException> message() {
            return message;
        }

        @Override
        public CQuery<GridCursor> message(Supplier<? extends RuntimeException> message) {
            return new GridColumn(index, message);
        }

        @Override
        public CQuery<GridCursor> parallel() {
            return this;
        }

        @Override
        public CQuery<GridCursor> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public int size() {
            return data.size();
        }

        @Override
        public Stream<GridCursor> stream() {
            return IntStream.range(0, size()).mapToObj(row -> new GridCursor(row, index));
        }

        @Override
        public Iterator<GridCursor> iterator() {
            return IteratorUtils.withCause(stream().iterator(), message);
        }

        @Override
        public Ref<GridCursor> at(int row) {
            return isValid(row, index) ? Ref.of(new GridCursor(row, index), message) : Ref.empty(message);
        }

        /**
         * Fast access to cell data inside column.
         * @param row row
         * @return cell value
         */
        public T get(int row) {
            return data.get(row).get(index);
        }

        /**
         * Fast access to cell data inside column.
         * @param row row
         * @param value new value
         * @return old value
         */
        public T set(int row, T value) {
            return data.get(row).set(index, value);
        }
    }

    /**
     * GridRows is essentially CQuery backed by list of rows.
     */
    public class GridRows extends CQueryAbstract<GridRow> {

        private final List<GridRow> list = IntStream.range(0, size())
            .mapToObj(i -> new GridRow(i)).collect(Collectors.toList());

        private final Supplier<? extends RuntimeException> message;

        private GridRows(Supplier<? extends RuntimeException> message) {
            this.message = message;
        }

        @Override
        public Supplier<? extends RuntimeException> message() {
            return message;
        }

        @Override
        public CQuery<GridRow> message(Supplier<? extends RuntimeException> message) {
            return new GridRows(message);
        }

        @Override
        public CQuery<GridRow> parallel() {
            return this;
        }

        @Override
        public CQuery<GridRow> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public boolean hasFastList() {
            return true;
        }

        @Override
        public int size() {
            return data.size();
        }

        @Override
        public Stream<GridRow> stream() {
            return list.stream();
        }

        @Override
        public Iterator<GridRow> iterator() {
            return IteratorUtils.withCause(list.iterator(), message);
        }

        @Override
        public List<GridRow> list() {
            return list;
        }

        /**
         * Returns row at specifed index
         * @param index index
         * @return row
         * @throws IndexOutOfBoundsException if there is not such row
         */
        public GridRow get(int index) {
            return list.get(index);
        }

    }

    /**
     * GridRow is essentially CQuery not backed by any in-memory structure.
     * GridRow is a view and reads cells in realtime, by just using rows index as bound argument.
     *
     * It allows iteration through all columns and extract cells in specified row.
     * It allows fast read/write to cell using column index.
     */
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public class GridRow extends CQueryAbstract<GridCursor> {

        private final int index;

        private Supplier<? extends RuntimeException> message;

        @Override
        public Supplier<? extends RuntimeException> message() {
            return message;
        }

        @Override
        public CQuery<GridCursor> message(Supplier<? extends RuntimeException> message) {
            this.message = message;
            return this;
        }

        @Override
        public CQuery<GridCursor> parallel() {
            return this;
        }

        @Override
        public CQuery<GridCursor> sequential() {
            return this;
        }

        @Override
        public boolean isParallel() {
            return false;
        }

        @Override
        public int size() {
            return head.size();
        }

        @Override
        public Stream<GridCursor> stream() {
            return IntStream.range(0, size()).mapToObj(column -> new GridCursor(index, column));
        }

        @Override
        public Iterator<GridCursor> iterator() {
            return stream().iterator();
        }

        @Override
        public Ref<GridCursor> at(int column) {
            return isValid(index, column) ? Ref.of(new GridCursor(index, column)) : Ref.empty();
        }

        /**
         * Fast access to cell data inside row.
         *
         * @param column column
         * @return value
         */
        public T get(int column) {
            return data.get(index).get(column);
        }

        /**
         * Fast access to cell data inside row.
         *
         * @param column column
         * @return value
         */
        public T get(String column) {
            return data.get(index).get(headmap.getInt(column));
        }

        /**
         * Fast access to cell data inside row.
         *
         * @param column column
         * @param value new value
         * @return old value
         */
        public T set(int column, T value) {
            return data.get(index).set(column, value);
        }

        /**
         * Fast access to cell data inside row.
         *
         * @param column column
         * @param value new value
         * @return old value
         */
        public T set(String column, T value) {
            return data.get(index).set(headmap.getInt(column), value);
        }
    }


    /**
     * It is simple view exposing cell's value and its position
     */
    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public class GridCursor {

        private final int row;

        private final int column;

        /**
         * Returns cell's position
         *
         * @return row index
         */
        public int row() {
            return row;
        }

        /**
         * Returns cell's position
         *
         * @return column index
         */
        public int column() {
            return column;
        }

        /**
         * Fast access to cell data at cursor position.
         *
         * @return value
         */
        public T get() {
            return data.get(row).get(column);
        }

        /**
         * Fast access to cell data at cursor position.
         *
         * @param value new value
         * @return old value
         */
        public T set(T value) {
            return data.get(row).set(column, value);
        }

    }

}
