package com.github.martincooper.datatable;

import io.vavr.collection.Seq;
import io.vavr.collection.Stream;
import io.vavr.control.Try;

import static io.vavr.API.$;
import static io.vavr.API.Case;
import static io.vavr.API.Match;
import static io.vavr.Patterns.$Failure;
import static io.vavr.Patterns.$Success;

/**
 * DataRowCollectionModifiable. Handles a collection of DataRows
 * Created by Martin Cooper on 17/07/2017.
 */
public class DataRowCollectionModifiable extends DataRowCollectionBase {

    /**
     * Private DataRow constructor.
     * Use 'build' to create instance.
     *
     * @param table The DataTable the DataRow is pointing to.
     * @param rows The DataRows.
     */
    private DataRowCollectionModifiable(DataTable table, Iterable<DataRow> rows) {
        super(table, rows);
    }

    /**
     * Returns a new DataTable with the additional row appended.
     *
     * @param rowValues The values to append to the row.
     * @return Returns a new DataTable with the row appended.
     */
    public Try<DataTable> addValues(Object ... rowValues) {
        return add(rowValues);
    }

    /**
     * Returns a new DataTable with the additional row appended.
     *
     * @param rowValues The values to append to the row.
     * @return Returns a new DataTable with the row appended.
     */
    public Try<DataTable> add(Object[] rowValues) {
        return Match(mapValuesToColumns(Stream.of(rowValues))).of(
                Case($Success($()), this::addRow),
                Case($Failure($()), Try::failure)
        );
    }

    /**
     * Returns a new DataTable with the additional row inserted at the specified index.
     *
     * @param idx The row index.
     * @param rowValues The values to insert into the row.
     * @return Returns a new DataTable with the row inserted.
     */
    public Try<DataTable> insertValues(int idx, Object ... rowValues) {
        return insert(idx, rowValues);
    }

    /**
     * Returns a new DataTable with the additional row inserted at the specified index.
     *
     * @param idx The row index.
     * @param rowValues The values to insert into the row.
     * @return Returns a new DataTable with the row inserted.
     */
    public Try<DataTable> insert(int idx, Object[] rowValues) {
        return Match(mapValuesToColumns(Stream.of(rowValues))).of(
                Case($Success($()), values -> insertRow(idx, values)),
                Case($Failure($()), Try::failure)
        );
    }

    /**
     * Returns a new DataTable with the data replaced at the specified index.
     *
     * @param idx The row index.
     * @param rowValues The new values to replaced the old ones.
     * @return Returns a new DataTable with the row inserted.
     */
    public Try<DataTable> replaceValues(int idx, Object ... rowValues) {
        return replace(idx, rowValues);
    }

    /**
     * Returns a new DataTable with the data replaced at the specified index.
     *
     * @param idx The row index.
     * @param rowValues The new values to replaced the old ones.
     * @return Returns a new DataTable with the row inserted.
     */
    public Try<DataTable> replace(int idx, Object[] rowValues) {
        return Match(mapValuesToColumns(Stream.of(rowValues))).of(
                Case($Success($()), values -> replaceRow(idx, values)),
                Case($Failure($()), Try::failure)
        );
    }

    /**
     * Returns a new DataTable with the specified row removed.
     *
     * @param idx The row index.
     * @return Returns a new DataTable with the row removed.
     */
    public Try<DataTable> remove(int idx) {
        return removeRow(idx);
    }

    private Try<DataTable> addRow(Seq<ColumnValuePair> values) {
        Try<Seq<IDataColumn>> newCols = toSequence(values.map(val -> val.column().add(val.value())));
        return buildTable(newCols);
    }

    private Try<DataTable> insertRow(int idx, Seq<ColumnValuePair> values) {
        Try<Seq<IDataColumn>> newCols = toSequence(values.map(val -> val.column().insert(idx, val.value())));
        return buildTable(newCols);
    }

    private Try<DataTable> replaceRow(int idx, Seq<ColumnValuePair> values) {
        Try<Seq<IDataColumn>> newCols = toSequence(values.map(val -> val.column().replace(idx, val.value())));
        return buildTable(newCols);
    }

    private Try<DataTable> removeRow(int idx) {
        Try<Seq<IDataColumn>> cols = toSequence(table.columns().map(col -> col.remove(idx)));
        return buildTable(cols);
    }

    private Try<DataTable> buildTable(Try<Seq<IDataColumn>> columns) {
        return Match(columns).of(
                Case($Success($()), cols -> DataTable.build(table.name(), cols)),
                Case($Failure($()), Try::failure)
        );
    }

    /**
     * Validates the number of values equals the number of columns in the table.
     *
     * @param values The values.
     * @return Returns a sequence of ColumnValuePairs if valid.
     */
    private Try<Seq<ColumnValuePair>> mapValuesToColumns(Seq<Object> values) {
        return values.length() != table.columns().count()
                ? DataTableException.tryError("Number of values does not match number of columns.")
                : Try.success(createIndexedColumnValuePair(values));
    }

    /**
     * Maps each value to a corresponding column.
     *
     * @param values The list of values.
     * @return Returns a sequence of column to value mappings.
     */
    private Seq<ColumnValuePair> createIndexedColumnValuePair(Seq<Object> values) {
        return values.zipWithIndex((value, index) -> new ColumnValuePair(table.column(index), value));
    }

    /**
     * Converts a Seq<Try<IDataColumn>> into a Try<Seq<IDataColumn>>
     *
     * @param items The values to convert.
     * @return Returns the converted items.
     */
    private Try<Seq<IDataColumn>> toSequence(Seq<Try<IDataColumn>> items) {
        return Try.sequence(items);
    }

    /**
     * Builds a new DataRowCollection for the specified DataTable.
     *
     * @param table The table to build the DataRowCollection for.
     * @return Returns the DataRowCollection.
     */
    public static DataRowCollectionModifiable build(DataTable table) {
        return buildRowCollection(table, DataRowCollectionModifiable::new);
    }
}