/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.math.HashHelper;
import net.ranides.assira.text.LexicalCast;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 * @todo (migration) #8 Grid
 */
public class Grid<T> {
    
    private Header header;
    private List<Row> rows;
    
    public Grid() {
        // do nothing
    }
    
    public Header header() {
        checkHeader();
        return header;
    }
    
    public Grid header(String... columns) {
        if( null != header) {
            throw new IllegalStateException("Header is already assigned");
        }
        assert null == rows : "rows should be null";
                
        this.header = new Header(columns);
        this.rows =  new ArrayList<>();
        return this;
    }
    
    public List<Row> rows() {
        checkHeader();
        return rows;
    }
    
    public Grid add(T... values) {
        checkHeader();
        if( values.length != header.items.length) {
            throw new IllegalArgumentException();
        }
        rows.add(new Row(values));
        return this;
    }
    
    public Grid sort(String column) {
        checkHeader();
        final int index = getColumnIndex(column);
        Collections.sort(rows, new Comparator<Row>() {
            @Override
            public int compare(Row row1, Row row2) {
                return ValueUtils.compare(row1.values[index], row2.values[index]);
            }
        });
        return this;
    }
    
    public Grid sort(String... columns) {
        checkHeader();
        final int[] indexes = new int[columns.length];
        for(int i=0; i<indexes.length; i++) {
            indexes[i] = getColumnIndex(columns[i]);
        }
        Collections.sort(rows, new Comparator<Row>() {
            @Override
            public int compare(Row row1, Row row2) {
                for(int i : indexes) {
                    int r = ValueUtils.compare(row1.values[i], row2.values[i]);
                    if(0 != r) {
                        return r;
                    }
                }
                return 0;
            }
        });
        return this;
    }
    
    public Column<T> column(String name) {
        checkHeader();
        return new RawColumn(getColumnIndex(name));
    }
    
    public <T> Column<T> column(String name, Class<T> type) {
        checkHeader();
        return new CastColumn<>(getColumnIndex(name), type);
    }

    @Override
    public int hashCode() {
        return HashHelper.hashValues(3, 79, header, rows);
    }

    @Override
    public boolean equals(Object object) {
        if(!(object instanceof Grid)) {
            return false;
        }
        final Grid other = (Grid)object;
        if (!ValueUtils.equals(this.header, other.header)) {
            return false;
        }
        if (!ValueUtils.equals(this.rows, other.rows)) {
            return false;
        }
        return true;
    }
    
    private void checkHeader() {
        if( null == header) {
            throw new IllegalStateException("Header is not assigned. Use #header before.");
        }
        assert null != rows : "rows should be assigned";
    }
    
    private int getColumnIndex(String name) {
        for(int i=0, n=header.items.length; i<n; i++) {
            if(header.items[i].equals(name)) {
                return i;
            }
        }
        throw new NoSuchElementException("column does not exist: " + name);
    }
    
    public static class Header extends AbstractList<String> {
        
        private final String[] items;

        Header(String... items) {
            this.items = ArrayUtils.arraycopy(items);
        }
        
        @Override
        public int size() {
            return items.length;
        }

        @Override
        public String get(int index) {
            return items[index];
        }
        
        @Override
        public String set(int index, String element) {
            String prev = items[index];
            items[index] = element;
            return prev;
        }
        
    }

    public class Row extends AbstractList<T> {
        
        private final T[] values;

        Row(T... values) {
            this.values = ArrayUtils.arraycopy(values);
        }

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

        @Override
        public T get(int index) {
            return values[index];
        }
        
        public T get(String column) {
            checkHeader();
            return get(getColumnIndex(column));
        }
        
        @Override
        public T set(int index, T value) {
            T prev = values[index];
            values[index] = value;
            return prev;
        }
        
        public T set(String column, T value) {
            checkHeader();
            return set(getColumnIndex(column), value);
        }
        
    }
    
    public interface Column<Q> extends List<Q> {
        
    }
    
    private class CastColumn<Q> extends AbstractList<Q> implements Column<Q> {

        private final int column;
        private final Class<Q> type;

        CastColumn(int column, Class<Q> type) {
            this.column = column;
            this.type = type;
        }

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

        @Override
        public Q get(int index) {
            return LexicalCast.cast(rows.get(index).get(column), type);
        }
        
    }
    
    private class RawColumn extends AbstractList<T> implements Column<T> {

        private final int column;

        RawColumn(int column) {
            this.column = column;
        }

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

        @Override
        public T get(int index) {
            return rows.get(index).get(column);
        }
        
        @Override
        public T set(int index, T value) {
            return rows.get(index).set(column, value);
        }
        
    }
}
