/*
 * Copyright (c) 2013 Villu Ruusmann
 *
 * This file is part of JPMML-Evaluator
 *
 * JPMML-Evaluator is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JPMML-Evaluator is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with JPMML-Evaluator.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jpmml.evaluator;

import java.util.List;
import java.util.Map;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableRangeMap;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.Table;
import com.google.common.collect.TreeRangeMap;
import org.dmg.pmml.DataType;
import org.dmg.pmml.Discretize;
import org.dmg.pmml.DiscretizeBin;
import org.dmg.pmml.InlineTable;
import org.dmg.pmml.Interval;
import org.dmg.pmml.MapValues;
import org.dmg.pmml.TableLocator;
import org.jpmml.manager.InvalidFeatureException;
import org.jpmml.manager.UnsupportedFeatureException;

public class DiscretizationUtil {

	private DiscretizationUtil(){
	}

	static
	public FieldValue discretize(Discretize discretize, FieldValue value){
		String result = discretize(discretize, (value.asNumber()).doubleValue());

		return FieldValueUtil.create(discretize.getDataType(), null, result);
	}

	static
	public String discretize(Discretize discretize, double value){
		RangeMap<Double, String> binRanges = CacheUtil.getValue(discretize, DiscretizationUtil.binRangeCache);

		Map.Entry<Range<Double>, String> entry = binRanges.getEntry(value);
		if(entry != null){
			return entry.getValue();
		}

		return discretize.getDefaultValue();
	}

	static
	public FieldValue mapValue(MapValues mapValues, Map<String, FieldValue> values){
		DataType dataType = mapValues.getDataType();

		TableLocator tableLocator = mapValues.getTableLocator();
		if(tableLocator != null){
			throw new UnsupportedFeatureException(tableLocator);
		}

		InlineTable inlineTable = mapValues.getInlineTable();
		if(inlineTable != null){
			Table<Integer, String, String> table = InlineTableUtil.getContent(inlineTable);

			Map<String, String> row = InlineTableUtil.match(table, values);
			if(row != null){
				String result = row.get(mapValues.getOutputColumn());
				if(result == null){
					throw new EvaluationException(mapValues);
				}

				return FieldValueUtil.create(dataType, null, result);
			}
		}

		return FieldValueUtil.create(dataType, null, mapValues.getDefaultValue());
	}

	static
	public Range<Double> toRange(Interval interval){
		Double leftMargin = interval.getLeftMargin();
		Double rightMargin = interval.getRightMargin();

		// "The attributes leftMargin and rightMargin are optional but at least one value must be defined"
		if(leftMargin == null && rightMargin == null){
			throw new InvalidFeatureException(interval);
		}

		Interval.Closure closure = interval.getClosure();
		switch(closure){
			case OPEN_OPEN:
				{
					if(leftMargin == null){
						return Range.lessThan(rightMargin);
					} else

					if(rightMargin == null){
						return Range.greaterThan(leftMargin);
					}

					return Range.open(leftMargin, rightMargin);
				}
			case OPEN_CLOSED:
				{
					if(leftMargin == null){
						return Range.atMost(rightMargin);
					} else

					if(rightMargin == null){
						return Range.greaterThan(leftMargin);
					}

					return Range.openClosed(leftMargin, rightMargin);
				}
			case CLOSED_OPEN:
				{
					if(leftMargin == null){
						return Range.lessThan(rightMargin);
					} else

					if(rightMargin == null){
						return Range.atLeast(leftMargin);
					}

					return Range.closedOpen(leftMargin, rightMargin);
				}
			case CLOSED_CLOSED:
				{
					if(leftMargin == null){
						return Range.atMost(rightMargin);
					} else

					if(rightMargin == null){
						return Range.atLeast(leftMargin);
					}

					return Range.closed(leftMargin, rightMargin);
				}
			default:
				throw new UnsupportedFeatureException(interval, closure);
		}
	}

	static
	private RangeMap<Double, String> parseBinRanges(Discretize discretize){
		RangeMap<Double, String> result = TreeRangeMap.create();

		List<DiscretizeBin> discretizeBins = discretize.getDiscretizeBins();
		for(DiscretizeBin discretizeBin : discretizeBins){
			Interval interval = discretizeBin.getInterval();
			String binValue = discretizeBin.getBinValue();

			if(interval == null || binValue == null){
				throw new InvalidFeatureException(discretizeBin);
			}

			Range<Double> range = toRange(interval);

			result.put(range, binValue);
		}

		return result;
	}

	private static final LoadingCache<Discretize, RangeMap<Double, String>> binRangeCache = CacheBuilder.newBuilder()
		.weakKeys()
		.build(new CacheLoader<Discretize, RangeMap<Double, String>>(){

			@Override
			public RangeMap<Double, String> load(Discretize discretize){
				return ImmutableRangeMap.copyOf(parseBinRanges(discretize));
			}
		});
}
@@ -3348,24 +3348,179 @@
 terval);%0A%09%09%7D
+ // End if%0A%0A%09%09if(leftMargin != null && rightMargin != null && (leftMargin).compareTo(rightMargin) %3E 0)%7B%0A%09%09%09throw new InvalidFeatureException(interval);%0A%09%09%7D
 %0A%0A%09%09Interval

Validating the order of interval margins