/*
 * Copyright 2015-present Open Networking Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.onosproject.ui.table;

import com.google.common.collect.Sets;
import org.onosproject.ui.table.cell.DefaultCellComparator;
import org.onosproject.ui.table.cell.DefaultCellFormatter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A simple model of table data.
 * <p>
 * Note that this is not a full MVC type model; the expected usage pattern
 * is to create an empty table, add rows (by consulting the business model),
 * sort rows (based on client request parameters), and finally produce the
 * sorted list of rows.
 * <p>
 * The table also provides a mechanism for defining how cell values for a
 * particular column should be formatted into strings, to help facilitate
 * the encoding of the table data into a JSON structure.
 * <p>
 * Note that it is expected that all values for a particular column will
 * be the same class.
 */
public class TableModel {

    private static final String DESC = "desc";

    private static final CellComparator DEF_CMP = DefaultCellComparator.INSTANCE;
    private static final CellFormatter DEF_FMT = DefaultCellFormatter.INSTANCE;
    private static final String EMPTY = "";

    private final String[] columnIds;
    private final Set<String> idSet;
    private final Map<String, CellComparator> comparators = new HashMap<>();
    private final Map<String, CellFormatter> formatters = new HashMap<>();
    private final List<Row> rows = new ArrayList<>();
    private final Map<String, Annot> annotations = new HashMap<>();

    /**
     * Constructs a table (devoid of data) with the given column IDs.
     *
     * @param columnIds column identifiers
     */
    public TableModel(String... columnIds) {
        checkNotNull(columnIds, "columnIds cannot be null");
        checkArgument(columnIds.length > 0, "must be at least one column");

        idSet = Sets.newHashSet(columnIds);
        if (idSet.size() != columnIds.length) {
            throw new IllegalArgumentException("duplicate column ID(s) detected");
        }

        this.columnIds = Arrays.copyOf(columnIds, columnIds.length);
    }

    private void checkId(String id) {
        checkNotNull(id, "must provide a column ID");
        if (!idSet.contains(id)) {
            throw new IllegalArgumentException("unknown column id: " + id);
        }
    }

    /**
     * Returns the number of rows in this table model.
     *
     * @return number of rows
     */
    public int rowCount() {
        return rows.size();
    }

    /**
     * Returns the number of columns in this table model.
     *
     * @return number of columns
     */
    public int columnCount() {
        return columnIds.length;
    }

    /**
     * Returns the array of column IDs for this table model.
     * <p>
     * Implementation note: we are knowingly passing you a reference to
     * our internal array to avoid copying. Don't mess with it. It's your
     * table you'll break if you do!
     *
     * @return the column identifiers
     */
    public String[] getColumnIds() {
        return columnIds;
    }

    /**
     * Returns the raw {@link Row} representation of the rows in this table.
     *
     * @return raw table rows
     */
    public Row[] getRows() {
        return rows.toArray(new Row[rows.size()]);
    }

    /**
     * Inserts a new annotation.
     *
     * @param key key of annotation
     * @param value value of annotation
     */
    public void addAnnotation(String key, Object value) {
        Annot annot = new Annot(key, value);
        annotations.put(key, annot);
    }

    /**
     * Returns the annotations in this table.
     *
     * @return annotations
     */
    public Collection<Annot> getAnnotations() {
        Collection<Annot> annots = new ArrayList<>(annotations.size());
        annotations.forEach((k, v) -> annots.add(v));
        return annots;
    }

    /**
     * Sets a cell comparator for the specified column.
     *
     * @param columnId column identifier
     * @param comparator comparator to use
     */
    public void setComparator(String columnId, CellComparator comparator) {
        checkNotNull(comparator, "must provide a comparator");
        checkId(columnId);
        comparators.put(columnId, comparator);
    }

    /**
     * Returns the cell comparator to use on values in the specified column.
     *
     * @param columnId column identifier
     * @return an appropriate cell comparator
     */
    private CellComparator getComparator(String columnId) {
        checkId(columnId);
        CellComparator cmp = comparators.get(columnId);
        return cmp == null ? DEF_CMP : cmp;
    }

    /**
     * Sets a cell formatter for the specified column.
     *
     * @param columnId column identifier
     * @param formatter formatter to use
     */
    public void setFormatter(String columnId, CellFormatter formatter) {
        checkNotNull(formatter, "must provide a formatter");
        checkId(columnId);
        formatters.put(columnId, formatter);
    }

    /**
     * Returns the cell formatter to use on values in the specified column.
     *
     * @param columnId column identifier
     * @return an appropriate cell formatter
     */
    public CellFormatter getFormatter(String columnId) {
        checkId(columnId);
        CellFormatter fmt = formatters.get(columnId);
        return fmt == null ? DEF_FMT : fmt;
    }

    /**
     * Adds a row to the table model.
     *
     * @return the row, for chaining
     */
    public Row addRow() {
        Row r = new Row();
        rows.add(r);
        return r;
    }

    /**
     * Sorts the table rows based on the specified columns, in the
     * specified directions. The second column is optional, and can be
     * disregarded by passing null into id2 and dir2.
     *
     * @param id1 first column identifier
     * @param dir1 first column sort direction
     * @param id2 second column identifier (may be null)
     * @param dir2 second column sort direction (may be null)
     */
    public void sort(String id1, SortDir dir1, String id2, SortDir dir2) {
        Collections.sort(rows, new RowComparator(id1, dir1, id2, dir2));
    }

    private boolean nullOrEmpty(String s) {
        return s == null || EMPTY.equals(s.trim());
    }

    /** Designates sorting direction. */
    public enum SortDir {
        /** Designates an ascending sort. */
        ASC,
        /** Designates a descending sort. */
        DESC
    }


    /**
     * Row comparator.
     */
    private class RowComparator implements Comparator<Row> {
        private final String id1;
        private final SortDir dir1;
        private final String id2;
        private final SortDir dir2;
        private final CellComparator cc1;
        private final CellComparator cc2;

        /**
         * Constructs a row comparator based on the specified
         * column identifiers and sort directions. Note that id2 and dir2 may
         * be null.
         *
         * @param id1 first column identifier
         * @param dir1 first column sort direction
         * @param id2 second column identifier
         * @param dir2 second column sort direction
         */
        public RowComparator(String id1, SortDir dir1, String id2, SortDir dir2) {
            this.id1 = id1;
            this.dir1 = dir1;
            this.id2 = id2;
            this.dir2 = dir2;
            cc1 = getComparator(id1);
            cc2 = nullOrEmpty(id2) ? null : getComparator(id2);
        }

        @Override
        public int compare(Row a, Row b) {
            Object cellA = a.get(id1);
            Object cellB = b.get(id1);
            int result = cc1.compare(cellA, cellB);
            result = dir1 == SortDir.ASC ? result : -result;

            if (result == 0 && cc2 != null) {
                cellA = a.get(id2);
                cellB = b.get(id2);
                result = cc2.compare(cellA, cellB);
                result = dir2 == SortDir.ASC ? result : -result;
            }
            return result;
        }
    }

    /**
     * Model of an annotation.
     */
    public class Annot {
        private final String key;
        private final Object value;

        /**
         * Constructs an annotation with the given key and value.
         *
         * @param key the key
         * @param value the value
         */
        public Annot(String key, Object value) {
            this.key = key;
            this.value = value;
        }

        /**
         * Returns the annotation's key.
         *
         * @return key
         */
        public String key() {
            return key;
        }

        /**
         * Returns the annotation's value.
         *
         * @return value
         */
        public Object value() {
            return value;
        }

        /**
         * Returns the value as a string.
         * This default implementation uses the value's toString() method.
         *
         * @return the value as a string
         */
        public String valueAsString() {
            return value.toString();
        }
    }

    /**
     * Model of a row.
     */
    public class Row {
        private final Map<String, Object> cells = new HashMap<>();

        /**
         * Sets the cell value for the given column of this row.
         *
         * @param columnId column identifier
         * @param value value to set
         * @return self, for chaining
         */
        public Row cell(String columnId, Object value) {
            checkId(columnId);
            cells.put(columnId, value);
            return this;
        }

        /**
         * Returns the value of the cell in the given column for this row.
         *
         * @param columnId column identifier
         * @return cell value
         */
        public Object get(String columnId) {
            return cells.get(columnId);
        }

        /**
         * Returns the value of the cell as a string, using the
         * formatter appropriate for the column.
         *
         * @param columnId column identifier
         * @return formatted cell value
         */
        String getAsString(String columnId) {
            return getFormatter(columnId).format(get(columnId));
        }

        /**
         * Returns the row as an array of formatted strings.
         *
         * @return the formatted row data
         */
        public String[] getAsFormattedStrings() {
            List<String> formatted = new ArrayList<>(columnCount());
            for (String c : columnIds) {
                formatted.add(getAsString(c));
            }
            return formatted.toArray(new String[formatted.size()]);
        }
    }

    /**
     * Returns the appropriate sort direction for the given string.
     * <p>
     * The expected strings are "asc" for {@link SortDir#ASC ascending} and
     * "desc" for {@link SortDir#DESC descending}. Any other value will
     * default to ascending.
     *
     * @param s sort direction string encoding
     * @return sort direction
     */
    public static SortDir sortDir(String s) {
        return !DESC.equals(s) ? SortDir.ASC : SortDir.DESC;
    }
}
