/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.jena.propertytable.impl;

import java.util.* ;
import java.util.Map.Entry;

import org.apache.jena.atlas.iterator.Iter;
import org.apache.jena.atlas.iterator.IteratorConcat;
import org.apache.jena.ext.com.google.common.collect.HashMultimap;
import org.apache.jena.ext.com.google.common.collect.SetMultimap ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.propertytable.Column;
import org.apache.jena.propertytable.PropertyTable;
import org.apache.jena.propertytable.Row;
import org.apache.jena.util.iterator.ExtendedIterator ;
import org.apache.jena.util.iterator.NullIterator ;
import org.apache.jena.util.iterator.WrappedIterator ;

/**
 * A PropertyTable Implementation using HashMap.
 * It contains PSO and POS indexes.
 * 
 */
public class PropertyTableHashMapImpl implements PropertyTable {

	private Map<Node, Column> columnIndex; // Maps property Node key to Column
	private List<Column> columnList; // Stores the list of columns in the table
	private Map<Node, Row> rowIndex; // Maps the subject Node key to Row.
	private List<Row> rowList; // Stores the list of rows in the table

	// PSO index
	// Maps column Node to (subject Node, value) pairs
	private Map<Node, Map<Node, Node>> valueIndex; 
	// POS index
	// Maps column Node to (value, subject Node) pairs
	private Map<Node, SetMultimap<Node, Node>> valueReverseIndex; 

	PropertyTableHashMapImpl() {
		columnIndex = new HashMap<Node, Column>();
		columnList = new ArrayList<Column>();
		rowIndex = new HashMap<Node, Row>();
		rowList = new ArrayList<Row>();
		valueIndex = new HashMap<Node, Map<Node, Node>>();
		valueReverseIndex = new HashMap<Node, SetMultimap<Node, Node>>();
	}

	@Override
	public ExtendedIterator<Triple> getTripleIterator() {
		
		// use PSO index to scan all the table (slow)
		IteratorConcat<Triple> iter = new IteratorConcat<Triple>();
		for (Column column : getColumns()) {
			iter.add(getTripleIterator(column));
		}
		return WrappedIterator.create(Iter.distinct(iter));
	}

	@Override
	public ExtendedIterator<Triple> getTripleIterator(Column column) {
		
		// use PSO index directly (fast)
		
		if (column == null || column.getColumnKey() == null)
			throw new NullPointerException("column is null");
		
		ArrayList<Triple> triples = new ArrayList<Triple>();
		Map<Node, Node> values = valueIndex.get(column.getColumnKey());

		for (Entry<Node, Node> entry : values.entrySet()) {
			Node subject = entry.getKey();
			Node value = entry.getValue();
			triples.add(Triple.create(subject, column.getColumnKey(), value));
		}
		return WrappedIterator.create(triples.iterator());
	}

	@Override
	public ExtendedIterator<Triple> getTripleIterator(Node value) {
		
		// use POS index ( O(n), n= column count )
		
		if (value == null)
			throw new NullPointerException("value is null");
		
		IteratorConcat<Triple> iter = new IteratorConcat<Triple>();
		for (Column column : this.getColumns()) {
			ExtendedIterator<Triple> eIter = getTripleIterator(column,value);
			iter.add(eIter);
		}
		return WrappedIterator.create(Iter.distinct(iter));
	}

	@Override
	public ExtendedIterator<Triple> getTripleIterator(Column column, Node value) {
		
		// use POS index directly (fast)
		
		if (column == null || column.getColumnKey() == null)
			throw new NullPointerException("column is null");
		
		if (value == null)
			throw new NullPointerException("value is null");
		
		
		Node p = column.getColumnKey();
		final SetMultimap<Node, Node> valueToSubjectMap = valueReverseIndex.get(p);
		if ( valueToSubjectMap == null ) 
		    return NullIterator.instance() ;
		final Set<Node> subjects = valueToSubjectMap.get(value);
		ArrayList<Triple> triples = new ArrayList<Triple>();
		for (Node subject : subjects) {
		    triples.add(Triple.create(subject, p, value));
		}
		return WrappedIterator.create(triples.iterator());
	}


	@Override
	public ExtendedIterator<Triple> getTripleIterator(Row row) {
		// use PSO index ( O(n), n= column count )
		
		if (row == null || row.getRowKey() == null)
			throw new NullPointerException("row is null");
		
		ArrayList<Triple> triples = new ArrayList<Triple>();
		for (Column column : getColumns()) {
			Node value = row.getValue(column);
			triples.add(Triple.create(row.getRowKey(), column.getColumnKey(), value));
		}
		return WrappedIterator.create(triples.iterator());
	}

	@Override
	public Collection<Column> getColumns() {
		return columnList;
	}

	@Override
	public Column getColumn(Node p) {
		if (p == null)
			throw new NullPointerException("column node is null");
		return columnIndex.get(p);
	}

	@Override
	public Column createColumn(Node p) {
		if (p == null)
			throw new NullPointerException("column node is null");

		if (columnIndex.containsKey(p))
			throw new IllegalArgumentException("column already exists: '"
					+ p.toString());

		columnIndex.put(p, new ColumnImpl(this, p));
		columnList.add(columnIndex.get(p));
		valueIndex.put(p, new HashMap<Node, Node>());
		valueReverseIndex.put(p, HashMultimap.create());
		return getColumn(p);
	}

	@Override
	public Row getRow(final Node s) {
		if (s == null)
			throw new NullPointerException("subject node is null");
		Row row = rowIndex.get(s);
		return row;

	}
	
	@Override
	public Row createRow(final Node s){
		Row row = this.getRow(s);
		if (row != null)
			return row;

		row = new InternalRow(s);
		rowIndex.put(s, row);
		rowList.add(row);

		return row;
	}
	
	@Override
	public List<Row> getAllRows() {
		return rowList;
	}

	
	
	@Override
	public List<Node> getColumnValues(Column column) {
		if (column == null || column.getColumnKey() == null)
			throw new NullPointerException("column is null");
		
		Map<Node, Node> values = valueIndex.get(column.getColumnKey());

		List<Node> list = new ArrayList<Node>(values.size());
		list.addAll(values.values());
		return list;
	}
	
	@Override
	public Collection<Row> getMatchingRows(Column column, Node value) {
		if (column == null || column.getColumnKey() == null)
			throw new NullPointerException("column is null");
		
		if (value == null)
			throw new NullPointerException("value is null");
		
		
		Node p = column.getColumnKey();
		final SetMultimap<Node, Node> valueToSubjectMap = valueReverseIndex.get(p);
		if ( valueToSubjectMap == null )
		    return Collections.emptyList() ;
		final Set<Node> subjects = valueToSubjectMap.get(value);
		if ( subjects == null )
		    return Collections.emptyList() ;
		final ArrayList<Row> matchingRows = new ArrayList<Row>();
		for (Node subject : subjects) {
		    matchingRows.add(this.getRow(subject));
		}
		return matchingRows;
	}

	private final void setX(final Node s, final Node p, final Node value) {
		if (p == null)
			throw new NullPointerException("column Node must not be null.");
		if (value == null)
			throw new NullPointerException("value must not be null.");

		Map<Node, Node> subjectToValueMap = valueIndex.get(p);
		if (!columnIndex.containsKey(p) || subjectToValueMap == null)
			throw new IllegalArgumentException("column: '" + p
					+ "' does not yet exist.");

		Node oldValue = subjectToValueMap.get(s);
		subjectToValueMap.put(s, value);
		addToReverseMap(p, s, oldValue, value);
	}

	private void addToReverseMap(final Node p, final Node s, final Node oldValue, final Node value) {

		final SetMultimap<Node, Node> valueToSubjectMap = valueReverseIndex.get(p);
		if ( valueToSubjectMap == null )
            return ; 
		valueToSubjectMap.remove(oldValue, s);
		valueToSubjectMap.put(value, s);
	}

	private void unSetX(final Node s, final Node p) {

		final Map<Node, Node> subjectToValueMap = valueIndex.get(p);
		if (!columnIndex.containsKey(p) || subjectToValueMap == null)
			throw new IllegalArgumentException("column: '" + p
					+ "' does not yet exist.");

		final Node value = subjectToValueMap.get(s);
		if (value == null)
			return;

		subjectToValueMap.remove(s);
		removeFromReverseMap(p, s, value);
	}

	private void removeFromReverseMap(final Node p, final Node s,
			final Node value) {
		final SetMultimap<Node, Node> valueTokeysMap = valueReverseIndex.get(p);
		if ( valueTokeysMap == null )
		    return ;
		valueTokeysMap.remove(s, value);
	}

	private Node getX(final Node s, final Node p) {
		final Map<Node, Node> subjectToValueMap = valueIndex.get(p);
		if (!columnIndex.containsKey(p) || subjectToValueMap == null)
			throw new IllegalArgumentException("column: '" + p
					+ "' does not yet exist.");
		return subjectToValueMap.get(s);
	}

	private final class InternalRow implements Row {
		private final Node key;

		InternalRow(final Node key) {
			this.key = key;
		}

		@Override
		public void setValue(Column column, Node value) {
			if (value == null)
				unSetX(key, column.getColumnKey());
			else
				setX(key, column.getColumnKey(), value);
		}

		@Override
		public Node getValue(Column column) {
			return getX(key, column.getColumnKey());
		}
		
		@Override
		public Node getValue(Node columnKey) {
			return getX(key, columnKey);
		}

		@Override
		public PropertyTable getTable() {
			return PropertyTableHashMapImpl.this;
		}

		@Override
		public Node getRowKey() {
			return key;
		}

		@Override
		public Collection<Column> getColumns() {
			// TODO Auto-generated method stub
			return PropertyTableHashMapImpl.this.getColumns();
		}

		@Override
		public ExtendedIterator<Triple> getTripleIterator() {
			ArrayList<Triple> triples = new ArrayList<Triple>();
			for (Column column : getColumns()) {
				Node value = this.getValue(column);
				triples.add(Triple.create(key, column.getColumnKey(), value));
			}
			return WrappedIterator.create(triples.iterator());
		}

	}

}
