package leigh.conflux;

import leigh.eo.EO;
import leigh.eo.EOLoop;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.math.IntIntervals;
import leigh.mecha.sort.SortPolicy;

import java.util.*;

/**
 * A DataFrame provides a filterable, sortable view into a originalData graph.
 *
 * @author Alex Leigh
 * @since mk14 mod10 (CRYSTAL)
 */
public class DataFrameImpl implements DataFrame {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(DataFrameImpl.class);
    private final IntIntervals selection = new IntIntervals();
    private final HashMap<String, String> filters = new HashMap<>();
    private List<EO> view;
    private EOLoop data;
    private Locale locale = Locale.ENGLISH;
    private SortPolicy<EO> sortPolicy;

    public DataFrameImpl() {
        this(new EOLoop());
    }

    /**
     * Create a new frame which wraps the provided {@link EOLoop}.
     */
    public DataFrameImpl(EOLoop data) {
        assert (data != null);
        view = new ArrayList<>();
        this.data = data;
        // By default, the view is everything in no particular order.
        for (EO eo : data) {
            view.add(eo);
        }
    }

    /**
     * Return the {@link Locale} associated with this dataframe.
     */
    public Locale getLocale() {
        return locale;
    }

    @Override
    public void deleteSelected() {
        logger.debug("deleteSelected() called. [selected]: {}", selection);

        HashSet<EO> toRemove = new HashSet<>();
        for (Integer i : selection.getIntegers()) {
            toRemove.add(view.get(i));
        }

        logger.info("Found objects to delete: {}", toRemove);

        data.remove(toRemove);
        view.remove(toRemove);
        selection.clear();
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    /**
     * Return the size of the frame in rows. If rows have been filtered or otherwise manipulated, this is the
     * number of rows visible in the view, not the total number of rows in the underlying originalData.
     */
    @Override
    public int size() {
        return view.size();
    }

    public List<EO> getView() {
        return Collections.unmodifiableList(view);
    }

    @Override
    public EO create(String type) {
        logger.debug("Creating... [data: {}]", data.hashCode());
        EO obj = new EO(type);
        add(obj);
        return obj;
    }

    public void add(EO obj) {
        data.add(obj);
        // We hot-wire the object into the view, because as a new object it may not match the previous sort or
        // filter operations, but we want it to appear to the user anyways.
        view.add(obj);
    }

    /**
     * logger.debug the object at the given view position. Note that this will side-effect the view indicies of any
     * objects after this object from changing. For example, given 1 2 3, if you remove 2, 3 will become the new
     * 2.
     */
    @Override
    public void delete(int viewIdx) {
        logger.debug("Attempting to remove object. [idx: {}] [viewSz: {}] [dataSz: {}]", viewIdx,
                view.size(), data.size());

        EO old = view.remove(viewIdx);

        try {
            data.remove(data.indexOf(old));
        } catch (IndexOutOfBoundsException e) {
            logger.warn("delete(): {}", e);
        }

        logger.debug("Frame size now: {}", data.size());
    }

    @Override
    public List<EO> getSelected() {
        // TODO: Performance sucks.
        ArrayList<EO> selected = new ArrayList<>();
        for (int i = 0; i < view.size(); i++) {
            if (selection.contains(i)) {
                selected.add(view.get(i));
            }
        }

        logger.debug("Returning selected objects. [selection: {}] [found: {}]", selection, selected);

        return selected;
    }

    /**
     * Return the row at the given index, or null if there is no such row.
     */
    @Override
    public EO get(int idx) {
        return view.get(idx);
    }

    @Override
    public IntIntervals getSelection() {
        // TODO: Return a read-only copy
        return selection;
    }

    /**
     * Replace the originalData contained in this frame with the provided loop. This does not reset the filters
     * or selections. This implementation retains the provided {@link EOLoop} object, releasing the previously
     * assigned loop.
     */
    @Override
    public void replaceData(EOLoop data) {
        this.data = data;
        this.process();
    }

    /**
     * Add the provided objects to the current selection, if they happen to be present. Note that this does not
     * reset the existing selection state. the given objects in the view, if they happen to be present.
     *
     * @param selected The objects to select.
     */
    private void select(List<EO> selected) {
        logger.debug("select() called. [old: {}] [new: {}]", selection, selected);
        // TODO: Performance sucks
        for (EO obj : selected) {
            int idx = view.indexOf(obj);
            if (idx == -1) {
                continue;
            }
            selection.add(idx, idx);
        }
        selection.compact();
    }


    private void sort() {
        logger.debug("sort() called. [policy: {}] [view: {}]", sortPolicy, view);

        if (sortPolicy == null || sortPolicy.getFields().size() == 0) return;

        List<EO> oldSelected = getSelected();
        Comparator<EO> comp = sortPolicy.buildComparator();
        view.sort(comp);
        logger.debug("Sorted. [view: {}]", view);

        selection.clear();
        select(oldSelected);
    }

    @Override
    public void setSortPolicy(SortPolicy<EO> policy) {
        this.sortPolicy = policy;
    }

    /**
     * Set a filter string for one of the frame fields. Setting the filter does not apply it, to do that,
     * call <code>applyFilters()</code>. The filter consists of a series of terms, all of which must be present
     * in the originalData row value in order for the filter to match. The matching is case-insensitive.
     * <p>
     * To remove a filter, call setFilter() with a null filter.
     */
    @Override
    public void setQuickFilter(String key, String filter) {
        logger.debug("Setting filters. [key: {}] [filter: {}]", key, filter);

        if (filter == null) {
            filters.remove(key);
        } else {
            filters.put(key, filter.toLowerCase(locale));
        }
    }

    public void clearFilters() {
        filters.clear();
    }

    /**
     * Apply the filters (if any) which have been set with the <code>setFilter()</code> method. This will produce
     * the view from the underlying data. If a sort policy has been set via the <code>setSortPolicy()</code>
     * method, the view will be sorted according to that policy.
     * <p>
     * If there was a selection prior to calling <code>applyFilters()</code>, the selection will be updated to
     * contain any objects remaining in the view, in their new locations.
     */
    @Override
    public EOLoop applyFilters(EOLoop secondary) {
        // TODO This method contains a legacy for when the view could be foreign - simplify
        List<EO> oldSelected = getSelected();

        EOLoop filtered = new EOLoop();
        EOLoop secondaryFiltered = null;
        if (secondary != null) {
            secondaryFiltered = new EOLoop();
        }

        int viewIdx = 0;
        for (int dataIdx = 0; dataIdx < data.size(); dataIdx++) {
            boolean retain = true;
            EO row = data.get(dataIdx);

            for (Map.Entry<String, String> filter : filters.entrySet()) {
                String value = row.getValueString(filter.getKey());
                if (value == null) {
                    retain = false;
                    break;
                }

                value = value.toLowerCase();

                String term = filter.getValue();
                // Term is already lower-case
                if (!value.toLowerCase(locale).contains(term)) {
                    retain = false;
                    break;
                }
                if (!retain)
                    break;
            }

            if (retain) {
                filtered.add(row);
                viewIdx++;
                if (secondaryFiltered != null) {
                    secondaryFiltered.add(secondary.get(dataIdx));
                }
            }
        }

        // We do this because we may have been passed a foreign list implementation, and we need to preserve it
        view.clear();
        for (EO eo : filtered) {
            view.add(eo);
        }

        // Performing the sort() would try to fix the selections, but there is no need here, so we are sure to
        // clear the selection before the call.
        selection.clear();
        sort();
        select(oldSelected);

        return secondaryFiltered;
    }

    @Override
    public void process() {
        applyFilters(null);
    }
}
