/*
 * 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.testing_framework;

import org.apache.jena.graph.Node;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.rdf.model.Property;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.jena.rdf.model.Statement;
import org.apache.jena.rdf.model.StmtIterator;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.util.CollectionFactory;

import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

import org.junit.Assert;
import org.xenei.junit.contract.IProducer;

/**
 * provides useful functionality for testing models, eg building small models
 * from strings, testing equality, etc.
 * 
 * Currently this class extends GraphHelper and thus TestCase.
 */
public class ModelHelper extends GraphHelper {

	private static Model builderModel;

	static {
		builderModel = ModelFactory.createDefaultModel();
		builderModel.setNsPrefixes(PrefixMapping.Extended);
	}

	protected static final Model empty = ModelFactory.createDefaultModel();

	protected static Model extendedModel(IProducer<Model> producer) {
		Model result = producer.newInstance();
		result.setNsPrefixes(PrefixMapping.Extended);
		return result;
	}

	protected static String nice(RDFNode n) {
		return nice(n.asNode());
	}

	public static Statement statement(String fact) {
		StringTokenizer st = new StringTokenizer(fact);
		Resource sub = resource(st.nextToken());
		Property pred = property(st.nextToken());
		RDFNode obj = rdfNode(st.nextToken());
		return builderModel.createStatement(sub, pred, obj);
	}

	public static Statement statement(Resource s, Property p, RDFNode o) {
		return builderModel.createStatement(s, p, o);
	}

	public static RDFNode rdfNode(Model m, String s) {
		return m.asRDFNode(NodeCreateUtils.create(s));
	}

	public static RDFNode rdfNode(String s) {
		return rdfNode(builderModel, s);
	}

	public static <T extends RDFNode> T rdfNode(String s, Class<T> c) {
		return rdfNode(s).as(c);
	}

	public static Resource resource() {
		return ResourceFactory.createResource();
	}

	public static Resource resource(String s) {
		return (Resource) rdfNode(s);
	}

	// public static Resource resource(Model m, String s) {
	// return (Resource) rdfNode(m, s);
	// }

	public static Property property(String s) {
		return rdfNode(s).as(Property.class);
	}

	public static Property property(Model m, String s) {
		return rdfNode(m, s).as(Property.class);
	}

	public static Literal literal(String s, String lang) {
		return builderModel.createLiteral(s, lang);
	}

	public static Literal literal(String s) {
		return rdfNode(s).as(Literal.class);
	}

	// /**
	// * Create an array of Statements parsed from a semi-separated string.
	// *
	// * @param lockModel
	// * a model to serve as a statement factory
	// * @param facts
	// * a sequence of semicolon-separated "S P O" facts
	// * @return a Statement[] of the (S P O) statements from the string
	// */
	// public static Statement[] statements(Model m, String facts) {
	// ArrayList<Statement> sl = new ArrayList<Statement>();
	// StringTokenizer st = new StringTokenizer(facts, ";");
	// while (st.hasMoreTokens())
	// sl.add(statement(m, st.nextToken()));
	// return sl.toArray(new Statement[sl.size()]);
	// }

	/**
	 * Create an array of Statements parsed from a semi-separated string.
	 * 
	 * @param facts
	 *            a sequence of semicolon-separated "S P O" facts
	 * @return a Statement[] of the (S P O) statements from the string
	 */
	public static Statement[] statements(String facts) {
		ArrayList<Statement> sl = new ArrayList<>();
		StringTokenizer st = new StringTokenizer(facts, ";");
		while (st.hasMoreTokens())
			sl.add(statement(st.nextToken()));
		return sl.toArray(new Statement[sl.size()]);
	}

	/**
	 * Create an array of Resources from a whitespace-separated string
	 * 
	 * @param items
	 *            a whitespace-separated sequence to feed to resource
	 * @return a Resource[] of the parsed resources
	 */
	public static Resource[] resources(String items) {
		ArrayList<Resource> rl = new ArrayList<>();
		StringTokenizer st = new StringTokenizer(items);
		while (st.hasMoreTokens())
			rl.add(resource(st.nextToken()));
		return rl.toArray(new Resource[rl.size()]);
	}

	/**
	 * Answer the set of resources given by the space-separated
	 * <code>items</code> string. Each resource specification is interpreted as
	 * per <code>resource</code>.
	 */
	public static Set<Resource> resourceSet(String items) {
		Set<Resource> result = new HashSet<>();
		StringTokenizer st = new StringTokenizer(items);
		while (st.hasMoreTokens())
			result.add(resource(st.nextToken()));
		return result;
	}

	/**
	 * add to a model all the statements expressed by a string.
	 * 
	 * Does not do any transaction manipulation.
	 * 
	 * @param m
	 *            the model to be updated
	 * @param facts
	 *            a sequence of semicolon-separated "S P O" facts
	 * @return the updated model
	 */
	public static Model modelAdd(Model m, String facts) {
		StringTokenizer semis = new StringTokenizer(facts, ";");

		while (semis.hasMoreTokens()) {
			StringTokenizer st = new StringTokenizer(semis.nextToken());
			Resource sub = resource(st.nextToken());
			Property pred = property(st.nextToken());
			RDFNode obj = rdfNode(st.nextToken());
			m.add(sub, pred, obj);
		}

		return m;
	}

	/**
	 * create a memory based model with extended prefixes and initialises it
	 * with statements parsed from a string.
	 * 
	 * does all insertions in a transaction.
	 * 
	 * @param facts
	 * @return Model
	 */
	public static Model memModel(String facts) {
		Model model = ModelFactory.createMemModelMaker().createFreshModel();
		model.setNsPrefixes(PrefixMapping.Extended);
		txnBegin(model);
		modelAdd(model, facts);
		txnCommit(model);
		return model;
	}

	/**
	 * Creates a model with extended prefixes and initialises it with statements
	 * parsed from a string.
	 * 
	 * does all insertions in a transaction.
	 * 
	 * @param facts
	 *            a string in semicolon-separated "S P O" format
	 * @return a model containing those facts
	 */
	public static Model modelWithStatements(
			IProducer<? extends Model> producer, String facts) {
		Model m = createModel(producer);
		txnBegin(m);
		modelAdd(m, facts);
		txnCommit(m);
		return m;
	}

	/**
	 * Creates a model with extended prefixes and initialises it with statements
	 * parsed from the statement iterator.
	 * 
	 * Does all insertions in a transaction.
	 */
	public static Model modelWithStatements(
			IProducer<? extends Model> producer, final StmtIterator it) {
		Model m = createModel(producer);
		txnBegin(m);
		while (it.hasNext()) {
			m.add(it.nextStatement());
		}
		txnCommit(m);
		return m;
	}

	/**
	 * make a model and give it Extended prefixes
	 */
	public static Model createModel(IProducer<? extends Model> producer) {
		Model result = producer.newInstance();
		result.setNsPrefixes(PrefixMapping.Extended);
		return result;
	}

	/**
	 * test that two models are isomorphic and fail if they are not.
	 * 
	 * @param title
	 *            a String appearing at the beginning of the failure message
	 * @param wanted
	 *            the model value that is expected
	 * @param got
	 *            the model value to check
	 */
	public static void assertIsoModels(String title, Model wanted, Model got) {
		if (wanted.isIsomorphicWith(got) == false) {
			Map<Node, Object> map = CollectionFactory.createHashedMap();
			fail(title + ": expected " + nice(wanted.getGraph(), map)
					+ "\n but had " + nice(got.getGraph(), map));
		}
	}

	public static void assertContainsAll(final Model model, final Model model2) {
		for (final StmtIterator s = model2.listStatements(); s.hasNext();) {
			Assert.assertTrue(model.contains(s.nextStatement()));
		}
	}

	public static void assertSameStatements(final Model model,
			final Model model2) {
		assertContainsAll(model, model2);
		assertContainsAll(model2, model);
	}

	public static Property prop(final String uri) {
		return ResourceFactory.createProperty("eh:/" + uri);
	}

	public static Resource res(final String uri) {
		return ResourceFactory.createResource("eh:/" + uri);
	}

	/**
	 * Fail if the two models are not isomorphic. See
	 * assertIsoModels(String,Model,Model).
	 */
	public static void assertIsoModels(Model wanted, Model got) {
		assertIsoModels("models must be isomorphic", wanted, got);
	}

	public static final boolean tvBoolean = true;
	public static final byte tvByte = 1;
	public static final short tvShort = 2;
	public static final int tvInt = -1;
	public static final long tvLong = -2;
	public static final char tvChar = '!';
	public static final float tvFloat = (float) 123.456;
	public static final double tvDouble = -123.456;
	public static final String tvString = "test 12 string";
	public static final double dDelta = 0.000000005;

	public static final float fDelta = 0.000005f;

	public static final Object tvLitObj = new LitTestObj(1234);
	public static final LitTestObj tvObject = new LitTestObj(12345);

	public static class LitTestObj {
		protected long content;

		public LitTestObj(final long l) {
			content = l;
		}

		public LitTestObj(final String s) {
			content = Long.parseLong(s.substring(1, s.length() - 1));
		}

		@Override
		public boolean equals(final Object o) {
			return (o instanceof LitTestObj)
					&& (content == ((LitTestObj) o).content);
		}

		@Override
		public int hashCode() {
			return (int) (content ^ (content >> 32));
		}

		@Override
		public String toString() {
			return "[" + Long.toString(content) + "]";
		}

		public long getContent() {
			return content;
		}
	}

	/**
	 * Begin a transaction on the model if transactions are supported.
	 * 
	 * @param m
	 */
	public static Model txnBegin(Model m) {
		if (m.supportsTransactions()) {
			return m.begin();
		}
		return m;
	}

	/**
	 * Commit the transaction on the model if transactions are supported.
	 * 
	 * @param m
	 */
	public static Model txnCommit(Model m) {
		if (m.supportsTransactions()) {
			return m.commit();
		}
		return m;
	}

	/**
	 * Rollback (abort) the transaction on the model if transactions are
	 * supported.
	 * 
	 * @param m
	 */
	public static Model txnRollback(Model m) {
		if (m.supportsTransactions()) {
			return m.abort();
		}
		return m;
	}
}
